providers/saml: Metadata Import (#432)

* providers/saml: add basic metadata parser

* providers/saml: add importer for Singing certificate, validate signature, add tests

* providers/saml: add provider name to form,

* web: don't use trailing slash for spa URLs

* providers/saml: formatting fixes

* sources/*: add verbose_name to source serializers

* admin: add button launch import modal
This commit is contained in:
Jens L 2020-12-27 22:38:04 +01:00 committed by GitHub
parent 5797a3743a
commit a6d0c8c26c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 349 additions and 19 deletions

View file

@ -14,4 +14,6 @@ SOURCE_SERIALIZER_FIELDS = [
"enabled",
"authentication_flow",
"enrollment_flow",
"verbose_name",
"verbose_name_plural",
]

View file

@ -53,7 +53,7 @@
{% for flow in object_list %}
<tr role="row">
<th role="columnheader">
<a href="/flows/{{ flow.slug }}/">
<a href="/flows/{{ flow.slug }}">
<div><code>{{ flow.slug }}</code></div>
<small>{{ flow.name }}</small>
</a>

View file

@ -41,6 +41,17 @@
</ak-modal-button>
</li>
{% endfor %}
<li>
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{% trans 'SAML Provider from Metadata' %}<br>
<small>
{% trans "Create a SAML Provider by importing its Metadata." %}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
</ul>
</ak-dropdown>
<button role="ak-refresh" class="pf-c-button pf-m-primary">

View file

@ -22,6 +22,7 @@ from authentik.admin.views import (
tokens,
users,
)
from authentik.providers.saml.views import MetadataImportView
urlpatterns = [
path(
@ -116,6 +117,11 @@ urlpatterns = [
providers.ProviderCreateView.as_view(),
name="provider-create",
),
path(
"providers/create/saml/from-metadata/",
MetadataImportView.as_view(),
name="provider-saml-from-metadata",
),
path(
"providers/<int:pk>/update/",
providers.ProviderUpdateView.as_view(),

View file

@ -3,10 +3,11 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source
class SourceSerializer(ModelSerializer):
class SourceSerializer(ModelSerializer, MetaNameSerializer):
"""Source Serializer"""
__type__ = SerializerMethodField(method_name="get_type")

View file

@ -1,8 +1,13 @@
"""authentik SAML IDP Forms"""
from xml.etree.ElementTree import ParseError # nosec
from defusedxml.ElementTree import fromstring
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils.html import mark_safe
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from authentik.admin.fields import CodeMirrorWidget
from authentik.core.expression import PropertyMappingEvaluator
@ -83,3 +88,22 @@ class SAMLPropertyMappingForm(forms.ModelForm):
)
),
}
class SAMLProviderImportForm(forms.Form):
"""Create a SAML Provider from SP Metadata."""
provider_name = forms.CharField()
metadata = forms.FileField(
validators=[FileExtensionValidator(allowed_extensions=["xml"])]
)
def clean_metadata(self):
"""Check if the flow is valid XML"""
metadata = self.cleaned_data["metadata"].read()
try:
fromstring(metadata)
except ParseError:
raise ValidationError(_("Invalid XML Syntax"))
self.cleaned_data["metadata"].seek(0)
return self.cleaned_data["metadata"]

View file

@ -0,0 +1,143 @@
"""SAML ServiceProvider Metadata Parser and dataclass"""
from dataclasses import dataclass
from typing import Optional
import xmlsec
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import load_pem_x509_certificate
from lxml import etree # nosec
from structlog import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.utils.encoding import PEM_FOOTER, PEM_HEADER
from authentik.sources.saml.processors.constants import (
NS_MAP,
NS_SAML_METADATA,
SAML_BINDING_POST,
SAML_BINDING_REDIRECT,
)
LOGGER = get_logger()
def format_pem_certificate(unformatted_cert: str) -> str:
"""Format single, inline certificate into PEM Format"""
chunks, chunk_size = len(unformatted_cert), 64
lines = [PEM_HEADER]
for i in range(0, chunks, chunk_size):
lines.append(unformatted_cert[i : i + chunk_size]) # noqa: E203
lines.append(PEM_FOOTER)
return "\n".join(lines)
@dataclass
class ServiceProviderMetadata:
"""SP Metadata Dataclass"""
entity_id: str
acs_binding: str
acs_location: str
auth_n_request_signed: bool
assertion_signed: bool
signing_keypair: Optional[CertificateKeyPair] = None
def to_provider(self, name: str) -> SAMLProvider:
"""Create a SAMLProvider instance from the details. `name` is required,
as depending on the metadata CertificateKeypairs might have to be created."""
provider = SAMLProvider(name=name)
provider.issuer = self.entity_id
provider.sp_binding = self.acs_binding
provider.acs_url = self.acs_location
if self.signing_keypair:
self.signing_keypair.name = f"Provider {name} - SAML Signing Certificate"
self.signing_keypair.save()
provider.signing_kp = self.signing_keypair
return provider
class ServiceProviderMetadataParser:
"""Service-Provider Metadata Parser"""
def get_signing_cert(self, root: etree.Element) -> Optional[CertificateKeyPair]:
"""Extract X509Certificate from metadata, when given."""
signing_certs = root.xpath(
'//md:SPSSODescriptor/md:KeyDescriptor[@use="signing"]//ds:X509Certificate/text()',
namespaces=NS_MAP,
)
if len(signing_certs) < 1:
return None
raw_cert = format_pem_certificate(signing_certs[0])
# sanity check, make sure the certificate is valid.
load_pem_x509_certificate(raw_cert.encode("utf-8"), default_backend())
return CertificateKeyPair(
certificate_data=raw_cert,
)
def check_signature(self, root: etree.Element, keypair: CertificateKeyPair):
"""If Metadata is signed, check validity of signature"""
xmlsec.tree.add_ids(root, ["ID"])
signature_nodes = root.xpath(
"/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP
)
if len(signature_nodes) != 1:
# No Signature
return
signature_node = signature_nodes[0]
if signature_node is not None:
try:
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
keypair.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
None,
)
ctx.key = key
ctx.verify(signature_node)
except xmlsec.VerificationError as exc:
raise ValueError("Failed to verify Metadata signature") from exc
def parse(self, raw_xml: str) -> ServiceProviderMetadata:
"""Parse raw XML to ServiceProviderMetadata"""
root = etree.fromstring(raw_xml) # nosec
entity_id = root.attrib["entityID"]
sp_sso_descriptors = root.findall(f"{{{NS_SAML_METADATA}}}SPSSODescriptor")
if len(sp_sso_descriptors) < 1:
raise ValueError("no SPSSODescriptor objects found.")
# For now we'll only look at the first descriptor.
# Even if multiple descriptors exist, we can only configure one
descriptor = sp_sso_descriptors[0]
auth_n_request_signed = descriptor.attrib["AuthnRequestsSigned"]
assertion_signed = descriptor.attrib["WantAssertionsSigned"]
acs_services = descriptor.findall(
f"{{{NS_SAML_METADATA}}}AssertionConsumerService"
)
if len(acs_services) < 1:
raise ValueError("No AssertionConsumerService found.")
acs_service = acs_services[0]
acs_binding = {
SAML_BINDING_REDIRECT: SAMLBindings.REDIRECT,
SAML_BINDING_POST: SAMLBindings.POST,
}[acs_service.attrib["Binding"]]
acs_location = acs_service.attrib["Location"]
signing_keypair = self.get_signing_cert(root)
if signing_keypair:
self.check_signature(root, signing_keypair)
return ServiceProviderMetadata(
entity_id=entity_id,
acs_binding=acs_binding,
acs_location=acs_location,
auth_n_request_signed=auth_n_request_signed,
assertion_signed=assertion_signed,
signing_keypair=signing_keypair,
)

View file

@ -0,0 +1,13 @@
{% extends base_template|default:"generic/form.html" %}
{% load i18n %}
{% block above_form %}
<h1>
{% trans 'Import SAML Metadata' %}
</h1>
{% endblock %}
{% block action %}
{% trans 'Import Metadata' %}
{% endblock %}

View file

@ -0,0 +1,94 @@
"""Test Service-Provider Metadata Parser"""
# flake8: noqa
from django.test import TestCase
from authentik.providers.saml.models import SAMLBindings
from authentik.providers.saml.processors.metadata_parser import (
ServiceProviderMetadataParser,
)
METADATA_SIMPLE = """<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2020-12-24T15:14:47Z"
cacheDuration="PT604800S"
entityID="http://localhost:8080/saml/metadata">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>"""
METADATA_CERT = """<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2020-12-24T16:25:48Z" cacheDuration="PT604800S" entityID="http://localhost:8080/apps/user_saml/saml/metadata" ID="pfx67a2a913-dd26-d65e-64c2-503fcd5eea47"><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#pfx67a2a913-dd26-d65e-64c2-503fcd5eea47"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>dPEwDuMayrdAgjhYvtiy3GTcP0n+BDi3sX+aistTDac=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>MMspOlqURPLT2ELAD5RRHw4mBIWBGWAEedwMM6Dh7kzhJE/1z+dNgobM8e1Lr9Ztd/ygW7oUfwfbUqS8OVCxdI+fJtzai0Jm08/rq8VpNEZqTfALNQqlGDD2Uma63/8iFycrhYkaY8feoyT0ZfHTNuMDxHjtl1l52+q+1mx6ax5w9z/WzuSKBTndg+Gjh0XTN0ynI136MgXrJg4dBFtkIKq9u6PlJW42C+uqAoRoi29Vil6mT/dgctS2ZB118nGFeryN4oi2zgM9lkmWW6E5YtPdQxggKJqR8Zl+XnyHt3nh1X7uWN+691lXO6LG1YXtagD/BSMeUnfMV/dLCptv3w==</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDBzCCAe+gAwIBAgIQR8PqIN59R5mRygzU9JfD2TANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBwYXNzYm9vayBTZWxmLXNpZ25lZCBDZXJ0aWZpY2F0ZTAeFw0yMDA3MDUxMTEzMTRaFw0yMTA3MDYxMTEzMTRaMFQxKTAnBgNVBAMMIHBhc3Nib29rIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMREwDwYDVQQKDAhwYXNzYm9vazEUMBIGA1UECwwLU2VsZi1zaWduZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC89aKl+gUw7X0d25LY5PQ0DuOlrIQf1iaY/X4uM1FNAaMfAnd653EE+ugerZWvAtRGke2QElVgsvvGfMN6XfH/zSWxkklvXPsuM2HfW6Yv8FcaQRTB2jXBUzU6wNjwF8V8TdFPpbVzQxXOvJyP8Z2YhUNRjy0OQzTV5bD21b2fhpWcNCCSJxIeZQxKjwQNpt6xiGt0JXODgE8hp7psrKBMYa2O3MAmdCu1b0ixbfkam0Vm9vDHUyjRi2N9S7lkiIP2fYEDi0v0zRq9/yV/0MEel4towY+WcNnLvKTsww80vFrgUI4K0r4bS9uBWZaClvycE0ZSMYHZG6TZBCUtEd2JAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFMbcuq8oL2tfvBFMktv722r2FbbRXs7ky4IE2m42eaGEuhgdmrLsHaH4m3X+1TZgCufMGgoF8iVKQmpeaPSCDPKGW+yue+HLqCk5PIeQISDrEUwWNLx8lza7tm9Xdr1B3Q8/jxv1qtokhzhaBkCvYy92gvIgio5QaFKaFOSIp2Crrhh+R+uvmtronKe8RPx6XEk4EaAvXAfgGUV+xoQ9b54mt8gBwmZuLz86vIQBFSjOmPXEWYHe3FXAsB6i5eJMXtdBF7C3VbL3wBqOqddBPQ6+ojY+cUmNqVYbtXmAcjIveoJDi8Rs4F5pmlhGahnMgW8mqYtbHlUY7ytSUTowXA=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDBzCCAe+gAwIBAgIQR8PqIN59R5mRygzU9JfD2TANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBwYXNzYm9vayBTZWxmLXNpZ25lZCBDZXJ0aWZpY2F0ZTAeFw0yMDA3MDUxMTEzMTRaFw0yMTA3MDYxMTEzMTRaMFQxKTAnBgNVBAMMIHBhc3Nib29rIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMREwDwYDVQQKDAhwYXNzYm9vazEUMBIGA1UECwwLU2VsZi1zaWduZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC89aKl+gUw7X0d25LY5PQ0DuOlrIQf1iaY/X4uM1FNAaMfAnd653EE+ugerZWvAtRGke2QElVgsvvGfMN6XfH/zSWxkklvXPsuM2HfW6Yv8FcaQRTB2jXBUzU6wNjwF8V8TdFPpbVzQxXOvJyP8Z2YhUNRjy0OQzTV5bD21b2fhpWcNCCSJxIeZQxKjwQNpt6xiGt0JXODgE8hp7psrKBMYa2O3MAmdCu1b0ixbfkam0Vm9vDHUyjRi2N9S7lkiIP2fYEDi0v0zRq9/yV/0MEel4towY+WcNnLvKTsww80vFrgUI4K0r4bS9uBWZaClvycE0ZSMYHZG6TZBCUtEd2JAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFMbcuq8oL2tfvBFMktv722r2FbbRXs7ky4IE2m42eaGEuhgdmrLsHaH4m3X+1TZgCufMGgoF8iVKQmpeaPSCDPKGW+yue+HLqCk5PIeQISDrEUwWNLx8lza7tm9Xdr1B3Q8/jxv1qtokhzhaBkCvYy92gvIgio5QaFKaFOSIp2Crrhh+R+uvmtronKe8RPx6XEk4EaAvXAfgGUV+xoQ9b54mt8gBwmZuLz86vIQBFSjOmPXEWYHe3FXAsB6i5eJMXtdBF7C3VbL3wBqOqddBPQ6+ojY+cUmNqVYbtXmAcjIveoJDi8Rs4F5pmlhGahnMgW8mqYtbHlUY7ytSUTowXA=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/apps/user_saml/saml/acs" index="1"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>"""
CERT = """-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIQR8PqIN59R5mRygzU9JfD2TANBgkqhkiG9w0BAQsFADAr
MSkwJwYDVQQDDCBwYXNzYm9vayBTZWxmLXNpZ25lZCBDZXJ0aWZpY2F0ZTAeFw0y
MDA3MDUxMTEzMTRaFw0yMTA3MDYxMTEzMTRaMFQxKTAnBgNVBAMMIHBhc3Nib29r
IFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMREwDwYDVQQKDAhwYXNzYm9vazEUMBIG
A1UECwwLU2VsZi1zaWduZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC89aKl+gUw7X0d25LY5PQ0DuOlrIQf1iaY/X4uM1FNAaMfAnd653EE+ugerZWv
AtRGke2QElVgsvvGfMN6XfH/zSWxkklvXPsuM2HfW6Yv8FcaQRTB2jXBUzU6wNjw
F8V8TdFPpbVzQxXOvJyP8Z2YhUNRjy0OQzTV5bD21b2fhpWcNCCSJxIeZQxKjwQN
pt6xiGt0JXODgE8hp7psrKBMYa2O3MAmdCu1b0ixbfkam0Vm9vDHUyjRi2N9S7lk
iIP2fYEDi0v0zRq9/yV/0MEel4towY+WcNnLvKTsww80vFrgUI4K0r4bS9uBWZaC
lvycE0ZSMYHZG6TZBCUtEd2JAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFMbcuq8
oL2tfvBFMktv722r2FbbRXs7ky4IE2m42eaGEuhgdmrLsHaH4m3X+1TZgCufMGgo
F8iVKQmpeaPSCDPKGW+yue+HLqCk5PIeQISDrEUwWNLx8lza7tm9Xdr1B3Q8/jxv
1qtokhzhaBkCvYy92gvIgio5QaFKaFOSIp2Crrhh+R+uvmtronKe8RPx6XEk4EaA
vXAfgGUV+xoQ9b54mt8gBwmZuLz86vIQBFSjOmPXEWYHe3FXAsB6i5eJMXtdBF7C
3VbL3wBqOqddBPQ6+ojY+cUmNqVYbtXmAcjIveoJDi8Rs4F5pmlhGahnMgW8mqYt
bHlUY7ytSUTowXA=
-----END CERTIFICATE-----"""
class TestServiceProviderMetadataParser(TestCase):
"""Test ServiceProviderMetadataParser parsing and creation of SAML Provider"""
def test_simple(self):
"""Test simple metadata without Singing"""
metadata = ServiceProviderMetadataParser().parse(METADATA_SIMPLE)
provider = metadata.to_provider("test")
self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
def test_with_signing_cert(self):
"""Test Metadata with signing cert"""
metadata = ServiceProviderMetadataParser().parse(METADATA_CERT)
provider = metadata.to_provider("test")
self.assertEqual(
provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs"
)
self.assertEqual(
provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata"
)
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
self.assertEqual(provider.signing_kp.certificate_data, CERT)
def test_with_signing_cert_invalid_signature(self):
"""Test Metadata with signing cert (invalid signature)"""
with self.assertRaises(ValueError):
ServiceProviderMetadataParser().parse(
METADATA_CERT.replace("/apps/user_saml", "")
)

View file

@ -1,14 +1,18 @@
"""authentik SAML IDP Views"""
from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.validators import URLValidator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls.base import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.edit import FormView
from structlog import get_logger
from authentik.core.models import Application, Provider
@ -25,9 +29,13 @@ from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.forms import SAMLProviderImportForm
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import (
ServiceProviderMetadataParser,
)
from authentik.providers.saml.processors.request_parser import (
AuthNRequest,
AuthNRequestParser,
@ -242,3 +250,28 @@ class DescriptorDownloadView(View):
"Content-Disposition"
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
return response
class MetadataImportView(LoginRequiredMixin, FormView):
"""Import Metadata from XML, and create provider"""
form_class = SAMLProviderImportForm
template_name = "providers/saml/import.html"
success_url = reverse_lazy("authentik_admin:providers")
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
try:
metadata = ServiceProviderMetadataParser().parse(
form.cleaned_data["metadata"].read().decode()
)
provider = metadata.to_provider(form.cleaned_data["provider_name"])
provider.save()
messages.success(self.request, _("Successfully created Provider"))
except ValueError:
messages.error(self.request, _("Failed to import Metadata."))
return super().form_valid(form)

View file

@ -3,10 +3,11 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
class LDAPSourceSerializer(ModelSerializer):
class LDAPSourceSerializer(ModelSerializer, MetaNameSerializer):
"""LDAP Source Serializer"""
class Meta:

View file

@ -3,10 +3,11 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.oauth.models import OAuthSource
class OAuthSourceSerializer(ModelSerializer):
class OAuthSourceSerializer(ModelSerializer, MetaNameSerializer):
"""OAuth Source Serializer"""
class Meta:

View file

@ -3,10 +3,11 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.saml.models import SAMLSource
class SAMLSourceSerializer(ModelSerializer):
class SAMLSourceSerializer(ModelSerializer, MetaNameSerializer):
"""SAMLSource Serializer"""
class Meta:

View file

@ -5,16 +5,16 @@ import { SLUG_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface";
export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Library", "/library/"),
new SidebarItem("Library", "/library"),
new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview/"),
new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Events", "/events/log/"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Administration").children(
new SidebarItem("Applications", "/applications/").activeWhen(
new SidebarItem("Applications", "/applications").activeWhen(
`^/applications/(?<slug>${SLUG_REGEX})/$`
),
new SidebarItem("Sources", "/administration/sources/").activeWhen(
@ -31,7 +31,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Flows").children(
new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})/$`),
new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})$`),
new SidebarItem("Stages", "/administration/stages/"),
new SidebarItem("Prompts", "/administration/stages/prompts/"),
new SidebarItem("Invitations", "/administration/stages/invitations/"),

View file

@ -9,7 +9,7 @@ import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
@customElement("ak-application-list")
export class ApplicationList extends TablePage<Application> {
export class ApplicationListPage extends TablePage<Application> {
searchEnabled(): boolean {
return true;
}

View file

@ -10,18 +10,18 @@ import "./pages/flows/FlowViewPage";
export const ROUTES: Route[] = [
// Prevent infinite Shell loops
new Route(new RegExp("^/$")).redirect("/library/"),
new Route(new RegExp("^#.*")).redirect("/library/"),
new Route(new RegExp("^/library/$"), html`<ak-library></ak-library>`),
new Route(new RegExp("^/administration/overview/$"), html`<ak-admin-overview></ak-admin-overview>`),
new Route(new RegExp("^/applications/$"), html`<ak-application-list></ak-application-list>`),
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
new Route(new RegExp("^/$")).redirect("/library"),
new Route(new RegExp("^#.*")).redirect("/library"),
new Route(new RegExp("^/library$"), html`<ak-library></ak-library>`),
new Route(new RegExp("^/administration/overview$"), html`<ak-admin-overview></ak-admin-overview>`),
new Route(new RegExp("^/applications$"), html`<ak-application-list></ak-application-list>`),
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-application-view .args=${args}></ak-application-view>`;
}),
new Route(new RegExp(`^/sources/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
new Route(new RegExp(`^/sources/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-source-view .args=${args}></ak-source-view>`;
}),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`;
}),
];