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""" | ||||
| from json import dumps | ||||
|  | ||||
| from django.urls import reverse | ||||
| from drf_yasg.utils import swagger_auto_schema | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework import mixins | ||||
| @ -19,6 +18,7 @@ from authentik.core.api.utils import ( | ||||
|     PassiveSerializer, | ||||
|     TypeCreateSerializer, | ||||
| ) | ||||
| from authentik.core.expression import PropertyMappingEvaluator | ||||
| from authentik.core.models import PropertyMapping | ||||
| from authentik.lib.templatetags.authentik_utils import verbose_name | ||||
| 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""" | ||||
|         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: | ||||
|  | ||||
|         model = PropertyMapping | ||||
| @ -109,7 +115,7 @@ class PropertyMappingViewSet( | ||||
|         if not users.exists(): | ||||
|             raise PermissionDenied() | ||||
|  | ||||
|         response_data = {"successful": True} | ||||
|         response_data = {"successful": True, "result": ""} | ||||
|         try: | ||||
|             result = mapping.evaluate( | ||||
|                 users.first(), | ||||
|  | ||||
| @ -2,8 +2,10 @@ | ||||
| from json import dumps | ||||
|  | ||||
| from django.urls import reverse | ||||
| from rest_framework.serializers import ValidationError | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||
| from authentik.core.models import PropertyMapping, User | ||||
|  | ||||
|  | ||||
| @ -19,7 +21,7 @@ class TestPropertyMappingAPI(APITestCase): | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_test_call(self): | ||||
|         """Test Policy's test endpoint""" | ||||
|         """Test PropertMappings's test endpoint""" | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_api:propertymapping-test", kwargs={"pk": self.mapping.pk} | ||||
| @ -32,3 +34,12 @@ class TestPropertyMappingAPI(APITestCase): | ||||
|             response.content.decode(), | ||||
|             {"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 typing import Any, Iterable, Optional | ||||
|  | ||||
| from django.core.exceptions import ValidationError | ||||
| from requests import Session | ||||
| from rest_framework.serializers import ValidationError | ||||
| from sentry_sdk.hub import Hub | ||||
| from sentry_sdk.tracing import Span | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| """evaluator tests""" | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.test import TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from rest_framework.serializers import ValidationError | ||||
|  | ||||
| from authentik.policies.exceptions import PolicyException | ||||
| from authentik.policies.expression.evaluator import PolicyEvaluator | ||||
|  | ||||
| @ -1,25 +1,19 @@ | ||||
| """OAuth2Provider API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer): | ||||
| class ScopeMappingSerializer(PropertyMappingSerializer): | ||||
|     """ScopeMapping Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = ScopeMapping | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|         fields = PropertyMappingSerializer.Meta.fields + [ | ||||
|             "scope_name", | ||||
|             "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.request import Request | ||||
| 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 authentik.core.api.propertymappings import PropertyMappingSerializer | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.utils import MetaNameSerializer | ||||
| from authentik.core.models import Provider | ||||
| from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider | ||||
| from authentik.providers.saml.views.metadata import DescriptorDownloadView | ||||
| @ -67,20 +67,15 @@ class SAMLProviderViewSet(ModelViewSet): | ||||
|             return Response({"metadata": ""}) | ||||
|  | ||||
|  | ||||
| class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): | ||||
| class SAMLPropertyMappingSerializer(PropertyMappingSerializer): | ||||
|     """SAMLPropertyMapping Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = SAMLPropertyMapping | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|         fields = PropertyMappingSerializer.Meta.fields + [ | ||||
|             "saml_name", | ||||
|             "friendly_name", | ||||
|             "expression", | ||||
|             "verbose_name", | ||||
|             "verbose_name_plural", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -6,11 +6,8 @@ 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_lazy as _ | ||||
|  | ||||
| from authentik.admin.fields import CodeMirrorWidget | ||||
| from authentik.core.expression import PropertyMappingEvaluator | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.flows.models import Flow, FlowDesignation | ||||
| 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): | ||||
|     """Create a SAML Provider from SP Metadata.""" | ||||
|  | ||||
|  | ||||
| @ -192,10 +192,8 @@ class SAMLPropertyMapping(PropertyMapping): | ||||
|     friendly_name = models.TextField(default=None, blank=True, null=True) | ||||
|  | ||||
|     @property | ||||
|     def form(self) -> Type[ModelForm]: | ||||
|         from authentik.providers.saml.forms import SAMLPropertyMappingForm | ||||
|  | ||||
|         return SAMLPropertyMappingForm | ||||
|     def component(self) -> str: | ||||
|         return "ak-property-mapping-saml-form" | ||||
|  | ||||
|     @property | ||||
|     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.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||
| 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 | ||||
|  | ||||
|  | ||||
| @ -70,18 +70,13 @@ class LDAPSourceViewSet(ModelViewSet): | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class LDAPPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): | ||||
| class LDAPPropertyMappingSerializer(PropertyMappingSerializer): | ||||
|     """LDAP PropertyMapping Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = LDAPPropertyMapping | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "expression", | ||||
|         fields = PropertyMappingSerializer.Meta.fields + [ | ||||
|             "object_field", | ||||
|             "verbose_name", | ||||
|             "verbose_name_plural", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -12,6 +12,7 @@ import "../../elements/forms/ProxyForm"; | ||||
| import "./PropertyMappingTestForm"; | ||||
| import "./PropertyMappingScopeForm"; | ||||
| import "./PropertyMappingLDAPForm"; | ||||
| import "./PropertyMappingSAMLForm"; | ||||
| import { TableColumn } from "../../elements/table/Table"; | ||||
| import { until } from "lit-html/directives/until"; | ||||
| import { PAGE_SIZE } from "../../constants"; | ||||
| @ -79,6 +80,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> { | ||||
|                     .typeMap=${{ | ||||
|                         "scopemapping": "ak-property-mapping-scope-form", | ||||
|                         "ldap": "ak-property-mapping-ldap-form", | ||||
|                         "saml": "ak-property-mapping-saml-form", | ||||
|                     }}> | ||||
|                 </ak-proxy-form> | ||||
|                 <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