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", "enabled",
"authentication_flow", "authentication_flow",
"enrollment_flow", "enrollment_flow",
"verbose_name",
"verbose_name_plural",
] ]

View file

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

View file

@ -41,6 +41,17 @@
</ak-modal-button> </ak-modal-button>
</li> </li>
{% endfor %} {% 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> </ul>
</ak-dropdown> </ak-dropdown>
<button role="ak-refresh" class="pf-c-button pf-m-primary"> <button role="ak-refresh" class="pf-c-button pf-m-primary">

View file

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

View file

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

View file

@ -1,8 +1,13 @@
"""authentik SAML IDP Forms""" """authentik SAML IDP Forms"""
from xml.etree.ElementTree import ParseError # nosec
from defusedxml.ElementTree import fromstring
from django import forms 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.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.admin.fields import CodeMirrorWidget
from authentik.core.expression import PropertyMappingEvaluator 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""" """authentik SAML IDP Views"""
from typing import Optional from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render 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.decorators import method_decorator
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic.edit import FormView
from structlog import get_logger from structlog import get_logger
from authentik.core.models import Application, Provider 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.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion 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.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.metadata import MetadataProcessor 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 ( from authentik.providers.saml.processors.request_parser import (
AuthNRequest, AuthNRequest,
AuthNRequestParser, AuthNRequestParser,
@ -242,3 +250,28 @@ class DescriptorDownloadView(View):
"Content-Disposition" "Content-Disposition"
] = f'attachment; filename="{provider.name}_authentik_meta.xml"' ] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
return response 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 rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
class LDAPSourceSerializer(ModelSerializer): class LDAPSourceSerializer(ModelSerializer, MetaNameSerializer):
"""LDAP Source Serializer""" """LDAP Source Serializer"""
class Meta: class Meta:

View file

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

View file

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

View file

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

View file

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

View file

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