providers/saml: make NameID configurable using a Property Mapping
This commit is contained in:
parent
66a8b52c7c
commit
5ef4354723
|
@ -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",
|
||||
|
|
|
@ -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. "
|
||||
'<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>'
|
||||
'<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."
|
||||
)
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue