providers/saml: migrate import to API, add API tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
4e3701ca8d
commit
5eb9b95ab5
|
@ -2,7 +2,6 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.admin.views import policies, providers, sources, stages
|
from authentik.admin.views import policies, providers, sources, stages
|
||||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Sources
|
# Sources
|
||||||
|
@ -25,11 +24,6 @@ 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(),
|
||||||
|
|
|
@ -1,17 +1,33 @@
|
||||||
"""SAMLProvider API Views"""
|
"""SAMLProvider API Views"""
|
||||||
|
from xml.etree.ElementTree import ParseError # nosec
|
||||||
|
|
||||||
|
from defusedxml.ElementTree import fromstring
|
||||||
|
from django.http.response import HttpResponse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import CharField, FileField, ReadOnlyField
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||||
from authentik.providers.saml.views.metadata import DescriptorDownloadView
|
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||||
|
from authentik.providers.saml.processors.metadata_parser import (
|
||||||
|
ServiceProviderMetadataParser,
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderSerializer(ProviderSerializer):
|
class SAMLProviderSerializer(ProviderSerializer):
|
||||||
|
@ -33,19 +49,26 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||||
"signature_algorithm",
|
"signature_algorithm",
|
||||||
"signing_kp",
|
"signing_kp",
|
||||||
"verification_kp",
|
"verification_kp",
|
||||||
|
"sp_binding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SAMLMetadataSerializer(Serializer):
|
class SAMLMetadataSerializer(PassiveSerializer):
|
||||||
"""SAML Provider Metadata serializer"""
|
"""SAML Provider Metadata serializer"""
|
||||||
|
|
||||||
metadata = ReadOnlyField()
|
metadata = ReadOnlyField()
|
||||||
|
|
||||||
def create(self, request: Request) -> Response:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
class SAMLProviderImportSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
"""Import saml provider from XML Metadata"""
|
||||||
|
|
||||||
|
name = CharField(required=True)
|
||||||
|
# Using SlugField because https://github.com/OpenAPITools/openapi-generator/issues/3278
|
||||||
|
authorization_flow = SlugRelatedField(
|
||||||
|
queryset=Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION),
|
||||||
|
slug_field="slug",
|
||||||
|
)
|
||||||
|
file = FileField()
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderViewSet(ModelViewSet):
|
class SAMLProviderViewSet(ModelViewSet):
|
||||||
|
@ -61,11 +84,53 @@ class SAMLProviderViewSet(ModelViewSet):
|
||||||
"""Return metadata as XML string"""
|
"""Return metadata as XML string"""
|
||||||
provider = self.get_object()
|
provider = self.get_object()
|
||||||
try:
|
try:
|
||||||
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
|
||||||
|
if "download" in request._request.GET:
|
||||||
|
response = HttpResponse(metadata, content_type="application/xml")
|
||||||
|
response[
|
||||||
|
"Content-Disposition"
|
||||||
|
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
|
||||||
|
return response
|
||||||
return Response({"metadata": metadata})
|
return Response({"metadata": metadata})
|
||||||
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
||||||
return Response({"metadata": ""})
|
return Response({"metadata": ""})
|
||||||
|
|
||||||
|
@permission_required(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
"authentik_providers_saml.add_samlprovider",
|
||||||
|
"authentik_crypto.add_certificatekeypair",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=SAMLProviderImportSerializer(),
|
||||||
|
responses={204: "Successfully imported provider", 400: "Bad request"},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||||
|
def import_metadata(self, request: Request) -> Response:
|
||||||
|
"""Create provider from SAML Metadata"""
|
||||||
|
data = SAMLProviderImportSerializer(data=request.data)
|
||||||
|
if not data.is_valid():
|
||||||
|
raise ValidationError(data.errors)
|
||||||
|
file = data.validated_data["file"]
|
||||||
|
# Validate syntax first
|
||||||
|
try:
|
||||||
|
fromstring(file.read())
|
||||||
|
except ParseError:
|
||||||
|
raise ValidationError(_("Invalid XML Syntax"))
|
||||||
|
file.seek(0)
|
||||||
|
try:
|
||||||
|
metadata = ServiceProviderMetadataParser().parse(file.read().decode())
|
||||||
|
metadata.to_provider(
|
||||||
|
data.validated_data["name"], data.validated_data["authorization_flow"]
|
||||||
|
)
|
||||||
|
except ValueError as exc: # pragma: no cover
|
||||||
|
LOGGER.warning(str(exc))
|
||||||
|
return ValidationError(
|
||||||
|
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
|
||||||
|
)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
|
class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
|
||||||
"""SAMLPropertyMapping Serializer"""
|
"""SAMLPropertyMapping Serializer"""
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
"""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.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
|
||||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderForm(forms.ModelForm):
|
|
||||||
"""SAML Provider form"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["authorization_flow"].queryset = Flow.objects.filter(
|
|
||||||
designation=FlowDesignation.AUTHORIZATION
|
|
||||||
)
|
|
||||||
self.fields["property_mappings"].queryset = SAMLPropertyMapping.objects.all()
|
|
||||||
self.fields["signing_kp"].queryset = CertificateKeyPair.objects.exclude(
|
|
||||||
key_data__iexact=""
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = SAMLProvider
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"authorization_flow",
|
|
||||||
"acs_url",
|
|
||||||
"issuer",
|
|
||||||
"sp_binding",
|
|
||||||
"audience",
|
|
||||||
"signing_kp",
|
|
||||||
"verification_kp",
|
|
||||||
"property_mappings",
|
|
||||||
"name_id_mapping",
|
|
||||||
"assertion_valid_not_before",
|
|
||||||
"assertion_valid_not_on_or_after",
|
|
||||||
"session_valid_not_on_or_after",
|
|
||||||
"digest_algorithm",
|
|
||||||
"signature_algorithm",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"audience": forms.TextInput(),
|
|
||||||
"issuer": forms.TextInput(),
|
|
||||||
"assertion_valid_not_before": forms.TextInput(),
|
|
||||||
"assertion_valid_not_on_or_after": forms.TextInput(),
|
|
||||||
"session_valid_not_on_or_after": forms.TextInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderImportForm(forms.Form):
|
|
||||||
"""Create a SAML Provider from SP Metadata."""
|
|
||||||
|
|
||||||
provider_name = forms.CharField()
|
|
||||||
authorization_flow = forms.ModelChoiceField(
|
|
||||||
queryset=Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION)
|
|
||||||
)
|
|
||||||
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"]
|
|
|
@ -3,7 +3,6 @@ from typing import Optional, Type
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
@ -171,10 +170,8 @@ class SAMLProvider(Provider):
|
||||||
return SAMLProviderSerializer
|
return SAMLProviderSerializer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def component(self) -> str:
|
||||||
from authentik.providers.saml.forms import SAMLProviderForm
|
return "ak-provider-saml-form"
|
||||||
|
|
||||||
return SAMLProviderForm
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"SAML Provider {self.name}"
|
return f"SAML Provider {self.name}"
|
||||||
|
|
115
authentik/providers/saml/tests/test_api.py
Normal file
115
authentik/providers/saml/tests/test_api.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
"""SAML Provider API Tests"""
|
||||||
|
from tempfile import TemporaryFile
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Application, User
|
||||||
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
from authentik.providers.saml.tests.test_metadata import METADATA_SIMPLE
|
||||||
|
|
||||||
|
|
||||||
|
class TestSAMLProviderAPI(APITestCase):
|
||||||
|
"""SAML Provider API Tests"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.get(username="akadmin")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_metadata(self):
|
||||||
|
"""Test metadata export (normal)"""
|
||||||
|
provider = SAMLProvider.objects.create(
|
||||||
|
name="test",
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Application.objects.create(name="test", provider=provider, slug="test")
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
def test_metadata_download(self):
|
||||||
|
"""Test metadata export (download)"""
|
||||||
|
provider = SAMLProvider.objects.create(
|
||||||
|
name="test",
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Application.objects.create(name="test", provider=provider, slug="test")
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
|
||||||
|
+ "?download",
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertIn("Content-Disposition", response)
|
||||||
|
|
||||||
|
def test_metadata_invalid(self):
|
||||||
|
"""Test metadata export (invalid)"""
|
||||||
|
# Provider without application
|
||||||
|
provider = SAMLProvider.objects.create(
|
||||||
|
name="test",
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
def test_import_success(self):
|
||||||
|
"""Test metadata import (success case)"""
|
||||||
|
with TemporaryFile() as metadata:
|
||||||
|
metadata.write(METADATA_SIMPLE.encode())
|
||||||
|
metadata.seek(0)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:samlprovider-import-metadata"),
|
||||||
|
{
|
||||||
|
"file": metadata,
|
||||||
|
"name": "test",
|
||||||
|
"authorization_flow": Flow.objects.filter(
|
||||||
|
designation=FlowDesignation.AUTHORIZATION
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
.pk,
|
||||||
|
},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
# We don't test the actual object being created here, that has its own tests
|
||||||
|
|
||||||
|
def test_import_failed(self):
|
||||||
|
"""Test metadata import (invalid xml)"""
|
||||||
|
with TemporaryFile() as metadata:
|
||||||
|
metadata.write(b"invalid")
|
||||||
|
metadata.seek(0)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:samlprovider-import-metadata"),
|
||||||
|
{
|
||||||
|
"file": metadata,
|
||||||
|
"name": "test",
|
||||||
|
"authorization_flow": Flow.objects.filter(
|
||||||
|
designation=FlowDesignation.AUTHORIZATION
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
.pk,
|
||||||
|
},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(400, response.status_code)
|
||||||
|
|
||||||
|
def test_import_invalid(self):
|
||||||
|
"""Test metadata import (invalid input)"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:samlprovider-import-metadata"),
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(400, response.status_code)
|
|
@ -1,7 +1,7 @@
|
||||||
"""authentik SAML IDP URLs"""
|
"""authentik SAML IDP URLs"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.providers.saml.views import metadata, sso
|
from authentik.providers.saml.views import sso
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# SSO Bindings
|
# SSO Bindings
|
||||||
|
@ -21,9 +21,4 @@ urlpatterns = [
|
||||||
sso.SAMLSSOBindingInitView.as_view(),
|
sso.SAMLSSOBindingInitView.as_view(),
|
||||||
name="sso-init",
|
name="sso-init",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"<slug:application_slug>/metadata/",
|
|
||||||
metadata.DescriptorDownloadView.as_view(),
|
|
||||||
name="metadata",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
"""authentik SAML IDP Views"""
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.lib.views import bad_request_message
|
|
||||||
from authentik.providers.saml.forms import SAMLProviderImportForm
|
|
||||||
from authentik.providers.saml.models import SAMLProvider
|
|
||||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
|
||||||
from authentik.providers.saml.processors.metadata_parser import (
|
|
||||||
ServiceProviderMetadataParser,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptorDownloadView(View):
|
|
||||||
"""Replies with the XML Metadata IDSSODescriptor."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
|
|
||||||
"""Return rendered XML Metadata"""
|
|
||||||
return MetadataProcessor(provider, request).build_entity_descriptor()
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
|
||||||
"""Replies with the XML Metadata IDSSODescriptor."""
|
|
||||||
application = get_object_or_404(Application, slug=application_slug)
|
|
||||||
provider: SAMLProvider = get_object_or_404(
|
|
||||||
SAMLProvider, pk=application.provider_id
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
|
||||||
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
|
||||||
return bad_request_message(
|
|
||||||
request, "Provider is not assigned to an application."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = HttpResponse(metadata, content_type="application/xml")
|
|
||||||
response[
|
|
||||||
"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 = "/"
|
|
||||||
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
metadata.to_provider(
|
|
||||||
form.cleaned_data["provider_name"],
|
|
||||||
form.cleaned_data["authorization_flow"],
|
|
||||||
)
|
|
||||||
messages.success(self.request, _("Successfully created Provider"))
|
|
||||||
except ValueError as exc:
|
|
||||||
LOGGER.warning(str(exc))
|
|
||||||
messages.error(
|
|
||||||
self.request,
|
|
||||||
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
|
|
||||||
)
|
|
||||||
return super().form_invalid(form)
|
|
||||||
return super().form_valid(form)
|
|
36
swagger.yaml
36
swagger.yaml
|
@ -9227,6 +9227,42 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- providers
|
- providers
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/providers/saml/import_metadata/:
|
||||||
|
post:
|
||||||
|
operationId: providers_saml_import_metadata
|
||||||
|
description: Create provider from SAML Metadata
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
in: formData
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
- name: authorization_flow
|
||||||
|
in: formData
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
format: slug
|
||||||
|
pattern: ^[-a-zA-Z0-9_]+$
|
||||||
|
- name: file
|
||||||
|
in: formData
|
||||||
|
required: true
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Successfully imported provider
|
||||||
|
'400':
|
||||||
|
description: Invalid input.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ValidationError'
|
||||||
|
'403':
|
||||||
|
description: Authentication credentials were invalid, absent or insufficient.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/GenericError'
|
||||||
|
consumes:
|
||||||
|
- multipart/form-data
|
||||||
|
tags:
|
||||||
|
- providers
|
||||||
|
parameters: []
|
||||||
/providers/saml/{id}/:
|
/providers/saml/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: providers_saml_read
|
operationId: providers_saml_read
|
||||||
|
|
|
@ -12,6 +12,7 @@ const resources = [
|
||||||
|
|
||||||
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
|
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
|
||||||
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
|
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
|
||||||
|
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css.map", dest: "dist/" },
|
||||||
{ src: "src/authentik.css", dest: "dist/" },
|
{ src: "src/authentik.css", dest: "dist/" },
|
||||||
|
|
||||||
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
|
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
|
||||||
|
|
|
@ -22,9 +22,6 @@ export class AppURLManager {
|
||||||
static sourceOAuth(slug: string, action: string): string {
|
static sourceOAuth(slug: string, action: string): string {
|
||||||
return `/source/oauth/${action}/${slug}/`;
|
return `/source/oauth/${action}/${slug}/`;
|
||||||
}
|
}
|
||||||
static providerSAML(rest: string): string {
|
|
||||||
return `/application/saml/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import "../../elements/forms/ProxyForm";
|
||||||
import "./oauth2/OAuth2ProviderForm";
|
import "./oauth2/OAuth2ProviderForm";
|
||||||
import "./proxy/ProxyProviderForm";
|
import "./proxy/ProxyProviderForm";
|
||||||
import "./saml/SAMLProviderForm";
|
import "./saml/SAMLProviderForm";
|
||||||
|
import "./saml/SAMLProviderImportForm";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { PAGE_SIZE } from "../../constants";
|
||||||
|
|
64
web/src/pages/providers/saml/SAMLProviderImportForm.ts
Normal file
64
web/src/pages/providers/saml/SAMLProviderImportForm.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { FlowDesignationEnum, FlowsApi, ProvidersApi, SAMLProvider } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import { until } from "lit-html/directives/until";
|
||||||
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
import { Form } from "../../../elements/forms/Form";
|
||||||
|
import "../../../elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
|
@customElement("ak-provider-saml-import-form")
|
||||||
|
export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
return gettext("Successfully imported provider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
send = (data: SAMLProvider): Promise<void> => {
|
||||||
|
const file = this.getFormFile();
|
||||||
|
if (!file) {
|
||||||
|
throw new Error("No form data");
|
||||||
|
}
|
||||||
|
return new ProvidersApi(DEFAULT_CONFIG).providersSamlImportMetadata({
|
||||||
|
file: file,
|
||||||
|
name: data.name,
|
||||||
|
authorizationFlow: data.authorizationFlow,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Name")}
|
||||||
|
?required=${true}
|
||||||
|
name="name">
|
||||||
|
<input type="text" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Authorization flow")}
|
||||||
|
?required=${true}
|
||||||
|
name="authorizationFlow">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||||
|
ordering: "pk",
|
||||||
|
designation: FlowDesignationEnum.Authorization,
|
||||||
|
}).then(flows => {
|
||||||
|
return flows.results.map(flow => {
|
||||||
|
return html`<option value=${ifDefined(flow.pk)}>${flow.name}</option>`;
|
||||||
|
});
|
||||||
|
}))}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Flow used when authorizing this provider.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Metadata")}
|
||||||
|
name="flow">
|
||||||
|
<input type="file" value="" class="pf-c-form-control">
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
|
||||||
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
|
||||||
import "../../../elements/buttons/ModalButton";
|
import "../../../elements/buttons/ModalButton";
|
||||||
import "../../../elements/buttons/SpinnerButton";
|
import "../../../elements/buttons/SpinnerButton";
|
||||||
|
@ -23,7 +24,6 @@ import "./SAMLProviderForm";
|
||||||
import { Page } from "../../../elements/Page";
|
import { Page } from "../../../elements/Page";
|
||||||
import { ProvidersApi, SAMLProvider } from "authentik-api";
|
import { ProvidersApi, SAMLProvider } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { AppURLManager } from "../../../api/legacy";
|
|
||||||
import { EVENT_REFRESH } from "../../../constants";
|
import { EVENT_REFRESH } from "../../../constants";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export class SAMLProviderViewPage extends Page {
|
||||||
provider?: SAMLProvider;
|
provider?: SAMLProvider;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
|
return [PFBase, PFPage, PFButton, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -153,27 +153,28 @@ export class SAMLProviderViewPage extends Page {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
${this.provider.assignedApplicationName ? html`
|
||||||
<section slot="page-3" data-tab-title="${gettext("Metadata")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section slot="page-3" data-tab-title="${gettext("Metadata")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||||
<div class="pf-u-w-75">
|
<div class="pf-u-w-75">
|
||||||
<div class="pf-c-card">
|
<div class="pf-c-card">
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
${until(
|
${until(new ProvidersApi(DEFAULT_CONFIG).providersSamlMetadata({
|
||||||
new ProvidersApi(DEFAULT_CONFIG).providersSamlMetadata({
|
|
||||||
id: this.provider.pk || 0,
|
id: this.provider.pk || 0,
|
||||||
}).then(m => {
|
}).then(m => {
|
||||||
return html`<ak-codemirror mode="xml" ?readOnly=${true} value="${ifDefined(m.metadata)}"></ak-codemirror>`;
|
return html`<ak-codemirror mode="xml" ?readOnly=${true} value="${ifDefined(m.metadata)}"></ak-codemirror>`;
|
||||||
})
|
}))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__footer">
|
<div class="pf-c-card__footer">
|
||||||
<a class="pf-c-button pf-m-primary" target="_blank" href="${AppURLManager.providerSAML(`${this.provider.assignedApplicationSlug}/metadata/`)}">
|
<a class="pf-c-button pf-m-primary" target="_blank"
|
||||||
|
href="/api/v2beta/providers/saml/${this.provider.pk}/metadata/?download">
|
||||||
${gettext("Download")}
|
${gettext("Download")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
` : html``}
|
||||||
</section>
|
</section>
|
||||||
</ak-tabs>`;
|
</ak-tabs>`;
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue