From 5ef435472348eb746a9bf7c2e24fa59475caf41e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 28 Jan 2021 22:50:13 +0100 Subject: [PATCH] providers/saml: make NameID configurable using a Property Mapping --- authentik/providers/saml/api.py | 1 + authentik/providers/saml/forms.py | 5 +++- .../0011_samlprovider_name_id_mapping.py | 27 +++++++++++++++++++ authentik/providers/saml/models.py | 15 +++++++++++ .../providers/saml/processors/assertion.py | 17 ++++++++++++ .../stages/email/tests/test_templates.py | 3 +++ swagger.yaml | 6 +++++ 7 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py index 8443a6bf7..eaf31f176 100644 --- a/authentik/providers/saml/api.py +++ b/authentik/providers/saml/api.py @@ -22,6 +22,7 @@ class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer): "assertion_valid_not_on_or_after", "session_valid_not_on_or_after", "property_mappings", + "name_id_mapping", "digest_algorithm", "signature_algorithm", "signing_kp", diff --git a/authentik/providers/saml/forms.py b/authentik/providers/saml/forms.py index 9518f6494..d70b27b61 100644 --- a/authentik/providers/saml/forms.py +++ b/authentik/providers/saml/forms.py @@ -42,6 +42,7 @@ class SAMLProviderForm(forms.ModelForm): "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", @@ -84,7 +85,9 @@ class SAMLPropertyMappingForm(forms.ModelForm): "saml_name": mark_safe( _( "URN OID used by SAML. This is optional. " - 'Reference' + 'Reference.' + " If this property mapping is used for NameID Property, " + "this field is discarded." ) ), } diff --git a/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py b/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py new file mode 100644 index 000000000..7425f6ca4 --- /dev/null +++ b/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.4 on 2021-01-28 21:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_providers_saml", "0010_auto_20201230_2112"), + ] + + operations = [ + migrations.AddField( + model_name="samlprovider", + name="name_id_mapping", + field=models.ForeignKey( + blank=True, + default=None, + help_text="Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be considered", + null=True, + verbose_name="NameID Property Mapping", + on_delete=django.db.models.deletion.SET_DEFAULT, + to="authentik_providers_saml.samlpropertymapping", + ), + ), + ] diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index 2536456b0..74edfdc96 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -65,6 +65,21 @@ class SAMLProvider(Provider): ), ) + name_id_mapping = models.ForeignKey( + "SAMLPropertyMapping", + default=None, + blank=True, + null=True, + on_delete=models.SET_DEFAULT, + verbose_name=_("NameID Property Mapping"), + help_text=_( + ( + "Configure how the NameID value will be created. When left empty, " + "the NameIDPolicy of the incoming request will be considered" + ) + ), + ) + assertion_valid_not_before = models.TextField( default="minutes=-5", validators=[timedelta_string_validator], diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py index 9efaccc75..163f8f187 100644 --- a/authentik/providers/saml/processors/assertion.py +++ b/authentik/providers/saml/processors/assertion.py @@ -139,13 +139,30 @@ class AssertionProcessor: audience.text = self.provider.audience return conditions + # pylint: disable=too-many-return-statements def get_name_id(self) -> Element: """Get NameID Element""" name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID") name_id.attrib["Format"] = self.auth_n_request.name_id_policy + # persistent is used as a fallback, so always generate it persistent = sha256( f"{self.http_request.user.id}-{settings.SECRET_KEY}".encode("ascii") ).hexdigest() + name_id.text = persistent + # If name_id_mapping is set, we override the value, regardless of what the SP asks for + if self.provider.name_id_mapping: + try: + value = self.provider.name_id_mapping.evaluate( + user=self.http_request.user, + request=self.http_request, + provider=self.provider, + ) + if value is not None: + name_id.text = value + return name_id + except PropertyMappingExpressionException as exc: + LOGGER.warning(str(exc)) + return name_id if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL: name_id.text = self.http_request.user.email return name_id diff --git a/authentik/stages/email/tests/test_templates.py b/authentik/stages/email/tests/test_templates.py index 3c2c06c93..a13344752 100644 --- a/authentik/stages/email/tests/test_templates.py +++ b/authentik/stages/email/tests/test_templates.py @@ -1,8 +1,10 @@ """email tests""" from os import unlink from pathlib import Path +from sys import platform from tempfile import gettempdir, mkstemp from typing import Any +from unittest.case import skipUnless from django.conf import settings from django.test import TestCase @@ -17,6 +19,7 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]: return templates_setting +@skipUnless(platform.startswith("linux"), "requires local docker") class TestEmailStageTemplates(TestCase): """Email tests""" diff --git a/swagger.yaml b/swagger.yaml index 548a5e4cd..bd3b20848 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -8839,6 +8839,12 @@ definitions: type: string format: uuid uniqueItems: true + name_id_mapping: + title: NameID Property Mapping + description: Configure how the NameID value will be created. When left empty, + the NameIDPolicy of the incoming request will be considered + type: string + x-nullable: true digest_algorithm: title: Digest algorithm type: string