providers/saml: migrate saml property mappings to web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-03-31 23:31:24 +02:00
parent 221e6190c8
commit d3f2f987e0
12 changed files with 119 additions and 87 deletions

View File

@ -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(),

View File

@ -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("/"))

View File

@ -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

View File

@ -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

View File

@ -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",
] ]

View File

@ -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",
] ]

View File

@ -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."""

View File

@ -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]:

View File

@ -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 %}

View File

@ -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",
] ]

View File

@ -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">

View 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>`;
}
}