providers/saml: migrate saml property mappings to web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -1,7 +1,6 @@ | |||||||
| """PropertyMapping API Views""" | """PropertyMapping API Views""" | ||||||
| from json import dumps | from json import dumps | ||||||
|  |  | ||||||
| from django.urls import reverse |  | ||||||
| from drf_yasg.utils import swagger_auto_schema | from drf_yasg.utils import swagger_auto_schema | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| @ -19,6 +18,7 @@ from authentik.core.api.utils import ( | |||||||
|     PassiveSerializer, |     PassiveSerializer, | ||||||
|     TypeCreateSerializer, |     TypeCreateSerializer, | ||||||
| ) | ) | ||||||
|  | from authentik.core.expression import PropertyMappingEvaluator | ||||||
| from authentik.core.models import PropertyMapping | from authentik.core.models import PropertyMapping | ||||||
| from authentik.lib.templatetags.authentik_utils import verbose_name | from authentik.lib.templatetags.authentik_utils import verbose_name | ||||||
| from authentik.lib.utils.reflection import all_subclasses | from authentik.lib.utils.reflection import all_subclasses | ||||||
| @ -41,6 +41,12 @@ class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|         """Get object type so that we know which API Endpoint to use to get the full object""" |         """Get object type so that we know which API Endpoint to use to get the full object""" | ||||||
|         return obj._meta.object_name.lower().replace("propertymapping", "") |         return obj._meta.object_name.lower().replace("propertymapping", "") | ||||||
|  |  | ||||||
|  |     def validate_expression(self, expression: str) -> str: | ||||||
|  |         """Test Syntax""" | ||||||
|  |         evaluator = PropertyMappingEvaluator() | ||||||
|  |         evaluator.validate(expression) | ||||||
|  |         return expression | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = PropertyMapping |         model = PropertyMapping | ||||||
| @ -109,7 +115,7 @@ class PropertyMappingViewSet( | |||||||
|         if not users.exists(): |         if not users.exists(): | ||||||
|             raise PermissionDenied() |             raise PermissionDenied() | ||||||
|  |  | ||||||
|         response_data = {"successful": True} |         response_data = {"successful": True, "result": ""} | ||||||
|         try: |         try: | ||||||
|             result = mapping.evaluate( |             result = mapping.evaluate( | ||||||
|                 users.first(), |                 users.first(), | ||||||
|  | |||||||
| @ -2,8 +2,10 @@ | |||||||
| from json import dumps | from json import dumps | ||||||
|  |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | from rest_framework.serializers import ValidationError | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  | from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||||
| from authentik.core.models import PropertyMapping, User | from authentik.core.models import PropertyMapping, User | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -19,7 +21,7 @@ class TestPropertyMappingAPI(APITestCase): | |||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_test_call(self): |     def test_test_call(self): | ||||||
|         """Test Policy's test endpoint""" |         """Test PropertMappings's test endpoint""" | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_api:propertymapping-test", kwargs={"pk": self.mapping.pk} |                 "authentik_api:propertymapping-test", kwargs={"pk": self.mapping.pk} | ||||||
| @ -32,3 +34,12 @@ class TestPropertyMappingAPI(APITestCase): | |||||||
|             response.content.decode(), |             response.content.decode(), | ||||||
|             {"result": dumps({"foo": "bar"}), "successful": True}, |             {"result": dumps({"foo": "bar"}), "successful": True}, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def test_validate(self): | ||||||
|  |         """Test PropertyMappings's validation""" | ||||||
|  |         # Because the root property-mapping has no write operation, we just instantiate | ||||||
|  |         # a serializer and test inline | ||||||
|  |         expr = "return True" | ||||||
|  |         self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             print(PropertyMappingSerializer().validate_expression("/")) | ||||||
|  | |||||||
| @ -3,8 +3,8 @@ import re | |||||||
| from textwrap import indent | from textwrap import indent | ||||||
| from typing import Any, Iterable, Optional | from typing import Any, Iterable, Optional | ||||||
|  |  | ||||||
| from django.core.exceptions import ValidationError |  | ||||||
| from requests import Session | from requests import Session | ||||||
|  | from rest_framework.serializers import ValidationError | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from sentry_sdk.tracing import Span | from sentry_sdk.tracing import Span | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """evaluator tests""" | """evaluator tests""" | ||||||
| from django.core.exceptions import ValidationError |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  | from rest_framework.serializers import ValidationError | ||||||
|  |  | ||||||
| from authentik.policies.exceptions import PolicyException | from authentik.policies.exceptions import PolicyException | ||||||
| from authentik.policies.expression.evaluator import PolicyEvaluator | from authentik.policies.expression.evaluator import PolicyEvaluator | ||||||
|  | |||||||
| @ -1,25 +1,19 @@ | |||||||
| """OAuth2Provider API Views""" | """OAuth2Provider API Views""" | ||||||
| from rest_framework.serializers import ModelSerializer |  | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
| from authentik.core.api.utils import MetaNameSerializer | from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||||
| from authentik.providers.oauth2.models import ScopeMapping | from authentik.providers.oauth2.models import ScopeMapping | ||||||
|  |  | ||||||
|  |  | ||||||
| class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer): | class ScopeMappingSerializer(PropertyMappingSerializer): | ||||||
|     """ScopeMapping Serializer""" |     """ScopeMapping Serializer""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = ScopeMapping |         model = ScopeMapping | ||||||
|         fields = [ |         fields = PropertyMappingSerializer.Meta.fields + [ | ||||||
|             "pk", |  | ||||||
|             "name", |  | ||||||
|             "scope_name", |             "scope_name", | ||||||
|             "description", |             "description", | ||||||
|             "expression", |  | ||||||
|             "verbose_name", |  | ||||||
|             "verbose_name_plural", |  | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,11 +4,11 @@ from rest_framework.decorators import action | |||||||
| from rest_framework.fields import ReadOnlyField | from rest_framework.fields import ReadOnlyField | ||||||
| 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 ModelSerializer, Serializer | from rest_framework.serializers import Serializer | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
|  | 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 MetaNameSerializer |  | ||||||
| from authentik.core.models import Provider | from authentik.core.models import Provider | ||||||
| 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.views.metadata import DescriptorDownloadView | ||||||
| @ -67,20 +67,15 @@ class SAMLProviderViewSet(ModelViewSet): | |||||||
|             return Response({"metadata": ""}) |             return Response({"metadata": ""}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): | class SAMLPropertyMappingSerializer(PropertyMappingSerializer): | ||||||
|     """SAMLPropertyMapping Serializer""" |     """SAMLPropertyMapping Serializer""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = SAMLPropertyMapping |         model = SAMLPropertyMapping | ||||||
|         fields = [ |         fields = PropertyMappingSerializer.Meta.fields + [ | ||||||
|             "pk", |  | ||||||
|             "name", |  | ||||||
|             "saml_name", |             "saml_name", | ||||||
|             "friendly_name", |             "friendly_name", | ||||||
|             "expression", |  | ||||||
|             "verbose_name", |  | ||||||
|             "verbose_name_plural", |  | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,11 +6,8 @@ from defusedxml.ElementTree import fromstring | |||||||
| from django import forms | from django import forms | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.core.validators import FileExtensionValidator | from django.core.validators import FileExtensionValidator | ||||||
| from django.utils.html import mark_safe |  | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| from authentik.admin.fields import CodeMirrorWidget |  | ||||||
| from authentik.core.expression import PropertyMappingEvaluator |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider | from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider | ||||||
| @ -59,40 +56,6 @@ class SAMLProviderForm(forms.ModelForm): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLPropertyMappingForm(forms.ModelForm): |  | ||||||
|     """SAML Property Mapping form""" |  | ||||||
|  |  | ||||||
|     template_name = "providers/saml/property_mapping_form.html" |  | ||||||
|  |  | ||||||
|     def clean_expression(self): |  | ||||||
|         """Test Syntax""" |  | ||||||
|         expression = self.cleaned_data.get("expression") |  | ||||||
|         evaluator = PropertyMappingEvaluator() |  | ||||||
|         evaluator.validate(expression) |  | ||||||
|         return expression |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|  |  | ||||||
|         model = SAMLPropertyMapping |  | ||||||
|         fields = ["name", "saml_name", "friendly_name", "expression"] |  | ||||||
|         widgets = { |  | ||||||
|             "name": forms.TextInput(), |  | ||||||
|             "saml_name": forms.TextInput(), |  | ||||||
|             "friendly_name": forms.TextInput(), |  | ||||||
|             "expression": CodeMirrorWidget(mode="python"), |  | ||||||
|         } |  | ||||||
|         help_texts = { |  | ||||||
|             "saml_name": mark_safe( |  | ||||||
|                 _( |  | ||||||
|                     "URN OID used by SAML. This is optional. " |  | ||||||
|                     '<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>.' |  | ||||||
|                     " If this property mapping is used for NameID Property, " |  | ||||||
|                     "this field is discarded." |  | ||||||
|                 ) |  | ||||||
|             ), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLProviderImportForm(forms.Form): | class SAMLProviderImportForm(forms.Form): | ||||||
|     """Create a SAML Provider from SP Metadata.""" |     """Create a SAML Provider from SP Metadata.""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -192,10 +192,8 @@ class SAMLPropertyMapping(PropertyMapping): | |||||||
|     friendly_name = models.TextField(default=None, blank=True, null=True) |     friendly_name = models.TextField(default=None, blank=True, null=True) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def form(self) -> Type[ModelForm]: |     def component(self) -> str: | ||||||
|         from authentik.providers.saml.forms import SAMLPropertyMappingForm |         return "ak-property-mapping-saml-form" | ||||||
|  |  | ||||||
|         return SAMLPropertyMappingForm |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def serializer(self) -> Type[Serializer]: |     def serializer(self) -> Type[Serializer]: | ||||||
|  | |||||||
| @ -1,14 +0,0 @@ | |||||||
| {% extends "generic/form.html" %} |  | ||||||
|  |  | ||||||
| {% load i18n %} |  | ||||||
|  |  | ||||||
| {% block beneath_form %} |  | ||||||
| <div class="pf-c-form__group "> |  | ||||||
|     <label for="" class="pf-c-form__label"></label> |  | ||||||
|     <div class="c-form__horizontal-group"> |  | ||||||
|         <p> |  | ||||||
|             Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables. |  | ||||||
|         </p> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| {% endblock %} |  | ||||||
| @ -8,11 +8,11 @@ from rest_framework.decorators import action | |||||||
| from rest_framework.fields import DateTimeField | from rest_framework.fields import DateTimeField | ||||||
| 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 ModelSerializer |  | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
|  | from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||||
| from authentik.core.api.sources import SourceSerializer | from authentik.core.api.sources import SourceSerializer | ||||||
| from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -70,18 +70,13 @@ class LDAPSourceViewSet(ModelViewSet): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class LDAPPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): | class LDAPPropertyMappingSerializer(PropertyMappingSerializer): | ||||||
|     """LDAP PropertyMapping Serializer""" |     """LDAP PropertyMapping Serializer""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = LDAPPropertyMapping |         model = LDAPPropertyMapping | ||||||
|         fields = [ |         fields = PropertyMappingSerializer.Meta.fields + [ | ||||||
|             "pk", |  | ||||||
|             "name", |  | ||||||
|             "expression", |  | ||||||
|             "object_field", |             "object_field", | ||||||
|             "verbose_name", |  | ||||||
|             "verbose_name_plural", |  | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import "../../elements/forms/ProxyForm"; | |||||||
| import "./PropertyMappingTestForm"; | import "./PropertyMappingTestForm"; | ||||||
| import "./PropertyMappingScopeForm"; | import "./PropertyMappingScopeForm"; | ||||||
| import "./PropertyMappingLDAPForm"; | import "./PropertyMappingLDAPForm"; | ||||||
|  | import "./PropertyMappingSAMLForm"; | ||||||
| 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"; | ||||||
| @ -79,6 +80,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> { | |||||||
|                     .typeMap=${{ |                     .typeMap=${{ | ||||||
|                         "scopemapping": "ak-property-mapping-scope-form", |                         "scopemapping": "ak-property-mapping-scope-form", | ||||||
|                         "ldap": "ak-property-mapping-ldap-form", |                         "ldap": "ak-property-mapping-ldap-form", | ||||||
|  |                         "saml": "ak-property-mapping-saml-form", | ||||||
|                     }}> |                     }}> | ||||||
|                 </ak-proxy-form> |                 </ak-proxy-form> | ||||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary"> |                 <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||||
|  | |||||||
							
								
								
									
										82
									
								
								web/src/pages/property-mappings/PropertyMappingSAMLForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								web/src/pages/property-mappings/PropertyMappingSAMLForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | |||||||
|  | import { SAMLPropertyMapping, PropertymappingsApi } from "authentik-api"; | ||||||
|  | import { gettext } from "django"; | ||||||
|  | import { customElement, property } from "lit-element"; | ||||||
|  | import { html, TemplateResult } from "lit-html"; | ||||||
|  | import { DEFAULT_CONFIG } from "../../api/Config"; | ||||||
|  | import { Form } from "../../elements/forms/Form"; | ||||||
|  | import { ifDefined } from "lit-html/directives/if-defined"; | ||||||
|  | import "../../elements/forms/HorizontalFormElement"; | ||||||
|  |  | ||||||
|  | @customElement("ak-property-mapping-saml-form") | ||||||
|  | export class PropertyMappingLDAPForm extends Form<SAMLPropertyMapping> { | ||||||
|  |  | ||||||
|  |     set mappingUUID(value: string) { | ||||||
|  |         new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlRead({ | ||||||
|  |             pmUuid: value, | ||||||
|  |         }).then(mapping => { | ||||||
|  |             this.mapping = mapping; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     mapping?: SAMLPropertyMapping; | ||||||
|  |  | ||||||
|  |     getSuccessMessage(): string { | ||||||
|  |         if (this.mapping) { | ||||||
|  |             return gettext("Successfully updated mapping."); | ||||||
|  |         } else { | ||||||
|  |             return gettext("Successfully created mapping."); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     send = (data: SAMLPropertyMapping): Promise<SAMLPropertyMapping> => { | ||||||
|  |         if (this.mapping) { | ||||||
|  |             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlUpdate({ | ||||||
|  |                 pmUuid: this.mapping.pk || "", | ||||||
|  |                 data: data | ||||||
|  |             }); | ||||||
|  |         } else { | ||||||
|  |             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlCreate({ | ||||||
|  |                 data: data | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     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" value="${ifDefined(this.mapping?.name)}" class="pf-c-form-control" required> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal | ||||||
|  |                 label=${gettext("SAML Attribute Name")} | ||||||
|  |                 ?required=${true} | ||||||
|  |                 name="samlName"> | ||||||
|  |                 <input type="text" value="${ifDefined(this.mapping?.samlName)}" class="pf-c-form-control" required> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     ${gettext("Attribute name used for SAML Assertions. Can be a URN OID, a schema reference, or a any other string. If this property mapping is used for NameID Property, this field is discarded.")} | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal | ||||||
|  |                 label=${gettext("Friendly Name")} | ||||||
|  |                 name="friendlyName"> | ||||||
|  |                 <input type="text" value="${ifDefined(this.mapping?.friendlyName)}" class="pf-c-form-control"> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     ${gettext("Optionally set the `FriendlyName` value of the Assertion attribute.")} | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal | ||||||
|  |                 label=${gettext("Expression")} | ||||||
|  |                 name="expression"> | ||||||
|  |                 <ak-codemirror mode="python" value="${this.mapping?.expression}"> | ||||||
|  |                 </ak-codemirror> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables. | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |         </form>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer