diff --git a/digid_eherkenning/oidc/admin.py b/digid_eherkenning/oidc/admin.py index 4fbd998..d6e59fa 100644 --- a/digid_eherkenning/oidc/admin.py +++ b/digid_eherkenning/oidc/admin.py @@ -90,34 +90,47 @@ def fieldsets_factory(claim_mapping_fields: Sequence[str]): @admin.register(DigiDConfig) -class OpenIDConnectConfigDigiDAdmin(SingletonModelAdmin): +class DigiDConfigAdmin(SingletonModelAdmin): form = admin_modelform_factory(DigiDConfig) - fieldsets = fieldsets_factory(claim_mapping_fields=["identifier_claim_name"]) + fieldsets = fieldsets_factory(claim_mapping_fields=["bsn_claim"]) @admin.register(EHerkenningConfig) -class OpenIDConnectConfigEHerkenningAdmin(SingletonModelAdmin): +class EHerkenningConfigAdmin(SingletonModelAdmin): form = admin_modelform_factory(EHerkenningConfig) - fieldsets = fieldsets_factory(claim_mapping_fields=["identifier_claim_name"]) + fieldsets = fieldsets_factory( + claim_mapping_fields=[ + "identifier_type_claim", + "legal_subject_claim", + "branch_number_claim", + "acting_subject_claim", + ] + ) @admin.register(DigiDMachtigenConfig) -class OpenIDConnectConfigDigiDMachtigenAdmin(SingletonModelAdmin): +class DigiDMachtigenConfigAdmin(SingletonModelAdmin): form = admin_modelform_factory(DigiDMachtigenConfig) fieldsets = fieldsets_factory( claim_mapping_fields=[ - "vertegenwoordigde_claim_name", - "gemachtigde_claim_name", + "representee_bsn_claim", + "authorizee_bsn_claim", + "mandate_service_id_claim", ] ) @admin.register(EHerkenningBewindvoeringConfig) -class OpenIDConnectConfigEHerkenningBewindvoeringAdmin(SingletonModelAdmin): +class EHerkenningBewindvoeringConfigAdmin(SingletonModelAdmin): form = admin_modelform_factory(EHerkenningBewindvoeringConfig) fieldsets = fieldsets_factory( claim_mapping_fields=[ - "vertegenwoordigde_company_claim_name", - "gemachtigde_person_claim_name", + "representee_claim", + "identifier_type_claim", + "legal_subject_claim", + "branch_number_claim", + "acting_subject_claim", + "mandate_service_id_claim", + "mandate_service_uuid_claim", ] ) diff --git a/digid_eherkenning/oidc/migrations/0004_migrate_config_to_claimfield.py b/digid_eherkenning/oidc/migrations/0004_migrate_config_to_claimfield.py new file mode 100644 index 0000000..4f9700e --- /dev/null +++ b/digid_eherkenning/oidc/migrations/0004_migrate_config_to_claimfield.py @@ -0,0 +1,258 @@ +# Generated by Django 4.2.13 on 2024-06-11 13:43 + +from typing import Any, Callable + +from django.conf import settings +from django.core.cache import caches +from django.db import migrations, models, transaction + +import mozilla_django_oidc_db.fields + + +def flush_cache(): + cache_name = getattr(settings, "SOLO_CACHE", None) + if not cache_name: + return + caches[cache_name].clear() + + +def operation_factory(model: str, mappings: dict[str, str]) -> migrations.RunPython: + + def _action_factory(transformer: Callable[[Any], None]): + def _run_python_action(apps, _) -> None: + ConfigModel = apps.get_model("digid_eherkenning_oidc_generics", model) + + # Solo model, so there's only ever one instance + config = ConfigModel.objects.first() + if config is None: + return + + transformer(config) + + config.save() + transaction.on_commit(flush_cache) + + return _run_python_action + + @_action_factory + def forward(instance) -> None: + for old, new in mappings.items(): + new_value = getattr(instance, old).split(".") + setattr(instance, new, new_value) + + @_action_factory + def reverse(instance) -> None: + for old, new in mappings.items(): + old_value = ".".join(getattr(instance, new)) + setattr(instance, old, old_value) + + return migrations.RunPython(forward, reverse) + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "digid_eherkenning_oidc_generics", + "0003_rename_openidconnectpublicconfig_digidconfig_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="digidconfig", + name="bsn_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault("bsn"), + help_text="Name of the claim holding the authenticated user's BSN.", + size=None, + verbose_name="bsn claim", + ), + ), + migrations.AddField( + model_name="digidmachtigenconfig", + name="authorizee_bsn_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:nl-eid-gdi:1.0:ActingSubjectID" + ), + help_text="Name of the claim holding the BSN of the authorized user.", + size=None, + verbose_name="authorizee bsn claim", + ), + ), + migrations.AddField( + model_name="digidmachtigenconfig", + name="representee_bsn_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:nl-eid-gdi:1.0:LegalSubjectID" + ), + help_text="Name of the claim holding the BSN of the represented user.", + size=None, + verbose_name="representee bsn claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="identifier_type_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "namequalifier" + ), + help_text="Claim that specifies how the legal subject claim must be interpreted. The expected claim value is one of: 'urn:etoegang:1.9:EntityConcernedID:KvKnr' or 'urn:etoegang:1.9:EntityConcernedID:RSIN'.", + size=None, + verbose_name="identifier type claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="legal_subject_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:LegalSubjectID" + ), + help_text="Name of the claim holding the identifier of the authenticated company.", + size=None, + verbose_name="company identifier claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="representee_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault("sel_uid"), + help_text="Name of the claim holding the BSN of the represented person.", + size=None, + verbose_name="representee identifier claim", + ), + ), + migrations.AddField( + model_name="eherkenningconfig", + name="identifier_type_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "namequalifier" + ), + help_text="Claim that specifies how the legal subject claim must be interpreted. The expected claim value is one of: 'urn:etoegang:1.9:EntityConcernedID:KvKnr' or 'urn:etoegang:1.9:EntityConcernedID:RSIN'.", + size=None, + verbose_name="identifier type claim", + ), + ), + migrations.AddField( + model_name="eherkenningconfig", + name="legal_subject_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:LegalSubjectID" + ), + help_text="Name of the claim holding the identifier of the authenticated company.", + size=None, + verbose_name="company identifier claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="acting_subject_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:ActingSubjectID" + ), + help_text="Name of the claim holding the (opaque) identifier of the user representing the authenticated company..", + size=None, + verbose_name="acting subject identifier claim", + ), + ), + migrations.AddField( + model_name="eherkenningconfig", + name="acting_subject_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:ActingSubjectID" + ), + help_text="Name of the claim holding the (opaque) identifier of the user representing the authenticated company..", + size=None, + verbose_name="acting subject identifier claim", + ), + ), + operation_factory( + "DigiDConfig", + mappings={ + "identifier_claim_name": "bsn_claim", + }, + ), + operation_factory( + "DigiDMachtigenConfig", + mappings={ + "vertegenwoordigde_claim_name": "representee_bsn_claim", + "gemachtigde_claim_name": "authorizee_bsn_claim", + }, + ), + operation_factory( + "EHerkenningConfig", + mappings={ + "identifier_claim_name": "legal_subject_claim", + }, + ), + operation_factory( + "EHerkenningBewindvoeringConfig", + mappings={ + "vertegenwoordigde_company_claim_name": "representee_claim", + "gemachtigde_person_claim_name": "acting_subject_claim", + }, + ), + migrations.RemoveField( + model_name="digidconfig", + name="identifier_claim_name", + ), + migrations.RemoveField( + model_name="digidmachtigenconfig", + name="gemachtigde_claim_name", + ), + migrations.RemoveField( + model_name="digidmachtigenconfig", + name="vertegenwoordigde_claim_name", + ), + migrations.RemoveField( + model_name="eherkenningbewindvoeringconfig", + name="gemachtigde_person_claim_name", + ), + migrations.RemoveField( + model_name="eherkenningbewindvoeringconfig", + name="vertegenwoordigde_company_claim_name", + ), + migrations.RemoveField( + model_name="eherkenningconfig", + name="identifier_claim_name", + ), + ] diff --git a/digid_eherkenning/oidc/migrations/0005_digidmachtigenconfig_mandate_service_id_claim_and_more.py b/digid_eherkenning/oidc/migrations/0005_digidmachtigenconfig_mandate_service_id_claim_and_more.py new file mode 100644 index 0000000..87ca213 --- /dev/null +++ b/digid_eherkenning/oidc/migrations/0005_digidmachtigenconfig_mandate_service_id_claim_and_more.py @@ -0,0 +1,93 @@ +# Generated by Django 4.2.13 on 2024-06-11 20:44 + +from django.db import migrations, models + +import mozilla_django_oidc_db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "digid_eherkenning_oidc_generics", + "0004_migrate_config_to_claimfield", + ), + ] + + operations = [ + migrations.AddField( + model_name="digidmachtigenconfig", + name="mandate_service_id_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:nl-eid-gdi:1.0:ServiceUUID" + ), + help_text="Name of the claim holding the service UUID for which the acting subject is authorized.", + size=None, + verbose_name="service ID claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="branch_number_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:1.9:ServiceRestriction:Vestigingsnr" + ), + help_text="Name of the claim holding the value of the branch number for the authenticated company, if such a restriction applies.", + size=None, + verbose_name="branch number claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="mandate_service_id_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:ServiceID" + ), + help_text="Name of the claim holding the service ID for which the company is authorized.", + size=None, + verbose_name="service ID claim", + ), + ), + migrations.AddField( + model_name="eherkenningbewindvoeringconfig", + name="mandate_service_uuid_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:core:ServiceUUID" + ), + help_text="Name of the claim holding the service UUID for which the company is authorized.", + size=None, + verbose_name="service UUID claim", + ), + ), + migrations.AddField( + model_name="eherkenningconfig", + name="branch_number_claim", + field=mozilla_django_oidc_db.fields.ClaimField( + base_field=models.CharField( + max_length=50, verbose_name="claim path segment" + ), + default=mozilla_django_oidc_db.fields.ClaimFieldDefault( + "urn:etoegang:1.9:ServiceRestriction:Vestigingsnr" + ), + help_text="Name of the claim holding the value of the branch number for the authenticated company, if such a restriction applies.", + size=None, + verbose_name="branch number claim", + ), + ), + ] diff --git a/digid_eherkenning/oidc/migrations/0006_alter_digidconfig_oidc_rp_scopes_list_and_more.py b/digid_eherkenning/oidc/migrations/0006_alter_digidconfig_oidc_rp_scopes_list_and_more.py new file mode 100644 index 0000000..6a62cd7 --- /dev/null +++ b/digid_eherkenning/oidc/migrations/0006_alter_digidconfig_oidc_rp_scopes_list_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.13 on 2024-06-12 06:50 + +from django.db import migrations, models + +import django_jsonform.models.fields + +import digid_eherkenning.oidc.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "digid_eherkenning_oidc_generics", + "0005_digidmachtigenconfig_mandate_service_id_claim_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="digidconfig", + name="oidc_rp_scopes_list", + field=django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + max_length=50, verbose_name="OpenID Connect scope" + ), + blank=True, + default=digid_eherkenning.oidc.models.base.get_default_scopes_bsn, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider.", + size=None, + verbose_name="OpenID Connect scopes", + ), + ), + migrations.AlterField( + model_name="digidmachtigenconfig", + name="oidc_rp_scopes_list", + field=django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + max_length=50, verbose_name="OpenID Connect scope" + ), + blank=True, + default=digid_eherkenning.oidc.models.base.get_default_scopes_bsn, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider.", + size=None, + verbose_name="OpenID Connect scopes", + ), + ), + migrations.AlterField( + model_name="eherkenningbewindvoeringconfig", + name="oidc_rp_scopes_list", + field=django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + max_length=50, verbose_name="OpenID Connect scope" + ), + blank=True, + default=digid_eherkenning.oidc.models.base.get_default_scopes_bsn, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider.", + size=None, + verbose_name="OpenID Connect scopes", + ), + ), + migrations.AlterField( + model_name="eherkenningconfig", + name="oidc_rp_scopes_list", + field=django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + max_length=50, verbose_name="OpenID Connect scope" + ), + blank=True, + default=digid_eherkenning.oidc.models.base.get_default_scopes_kvk, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider.", + size=None, + verbose_name="OpenID Connect scopes", + ), + ), + ] diff --git a/digid_eherkenning/oidc/models/digid.py b/digid_eherkenning/oidc/models/digid.py index e76b4e5..b7b676e 100644 --- a/digid_eherkenning/oidc/models/digid.py +++ b/digid_eherkenning/oidc/models/digid.py @@ -1,9 +1,10 @@ -from collections.abc import Collection +from collections.abc import Sequence from django.db import models from django.utils.translation import gettext_lazy as _ from django_jsonform.models.fields import ArrayField +from mozilla_django_oidc_db.fields import ClaimField, ClaimFieldDefault from mozilla_django_oidc_db.typing import ClaimPath from .base import OpenIDConnectBaseConfig, get_default_scopes_bsn @@ -14,11 +15,10 @@ class DigiDConfig(OpenIDConnectBaseConfig): Configuration for DigiD authentication via OpenID connect """ - identifier_claim_name = models.CharField( - _("BSN claim name"), - max_length=100, - help_text=_("The name of the claim in which the BSN of the user is stored"), - default="bsn", + bsn_claim = ClaimField( + verbose_name=_("bsn claim"), + default=ClaimFieldDefault("bsn"), + help_text=_("Name of the claim holding the authenticated user's BSN."), ) oidc_rp_scopes_list = ArrayField( verbose_name=_("OpenID Connect scopes"), @@ -27,7 +27,7 @@ class DigiDConfig(OpenIDConnectBaseConfig): blank=True, help_text=_( "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" + "These scopes are hardcoded and must be supported by the identity provider." ), ) @@ -36,27 +36,30 @@ class Meta: @property def oidcdb_username_claim(self) -> ClaimPath: - return [self.identifier_claim_name] + return self.bsn_claim class DigiDMachtigenConfig(OpenIDConnectBaseConfig): - # TODO: support periods in claim keys - vertegenwoordigde_claim_name = models.CharField( - verbose_name=_("vertegenwoordigde claim name"), - default="aanvrager.bsn", - max_length=50, - help_text=_( - "Name of the claim in which the BSN of the person being represented is stored" - ), + # TODO: these default claim names don't appear to be part of any standard... + representee_bsn_claim = ClaimField( + verbose_name=_("representee bsn claim"), + default=ClaimFieldDefault("urn:nl-eid-gdi:1.0:LegalSubjectID"), + help_text=_("Name of the claim holding the BSN of the represented user."), + ) + authorizee_bsn_claim = ClaimField( + verbose_name=_("authorizee bsn claim"), + default=ClaimFieldDefault("urn:nl-eid-gdi:1.0:ActingSubjectID"), + help_text=_("Name of the claim holding the BSN of the authorized user."), ) - gemachtigde_claim_name = models.CharField( - verbose_name=_("gemachtigde claim name"), - default="gemachtigde.bsn", - max_length=50, + mandate_service_id_claim = ClaimField( + verbose_name=_("service ID claim"), + default=ClaimFieldDefault("urn:nl-eid-gdi:1.0:ServiceUUID"), help_text=_( - "Name of the claim in which the BSN of the person representing someone else is stored" + "Name of the claim holding the service UUID for which the acting subject " + "is authorized." ), ) + oidc_rp_scopes_list = ArrayField( verbose_name=_("OpenID Connect scopes"), base_field=models.CharField(_("OpenID Connect scope"), max_length=50), @@ -64,7 +67,7 @@ class DigiDMachtigenConfig(OpenIDConnectBaseConfig): blank=True, help_text=_( "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" + "These scopes are hardcoded and must be supported by the identity provider." ), ) @@ -72,12 +75,8 @@ class Meta: verbose_name = _("OpenID Connect configuration for DigiD Machtigen") @property - def digid_eherkenning_machtigen_claims(self) -> dict[str, ClaimPath]: - return { - "vertegenwoordigde": [self.vertegenwoordigde_claim_name], - "gemachtigde": [self.gemachtigde_claim_name], - } - - @property - def oidcdb_sensitive_claims(self) -> Collection[ClaimPath]: - return list(self.digid_eherkenning_machtigen_claims.values()) + def oidcdb_sensitive_claims(self) -> Sequence[ClaimPath]: + return [ + self.representee_bsn_claim, # type: ignore + self.authorizee_bsn_claim, # type: ignore + ] diff --git a/digid_eherkenning/oidc/models/eherkenning.py b/digid_eherkenning/oidc/models/eherkenning.py index 2bec814..2dd2c15 100644 --- a/digid_eherkenning/oidc/models/eherkenning.py +++ b/digid_eherkenning/oidc/models/eherkenning.py @@ -1,9 +1,10 @@ -from collections.abc import Collection +from collections.abc import Sequence from django.db import models from django.utils.translation import gettext_lazy as _ from django_jsonform.models.fields import ArrayField +from mozilla_django_oidc_db.fields import ClaimField, ClaimFieldDefault from mozilla_django_oidc_db.typing import ClaimPath from .base import ( @@ -13,17 +14,59 @@ ) -class EHerkenningConfig(OpenIDConnectBaseConfig): +class AuthorizeeMixin(models.Model): + identifier_type_claim = ClaimField( + verbose_name=_("identifier type claim"), + # XXX: Anoigo specific default + default=ClaimFieldDefault("namequalifier"), + help_text=_( + "Claim that specifies how the legal subject claim must be interpreted. " + "The expected claim value is one of: " + "'urn:etoegang:1.9:EntityConcernedID:KvKnr' or " + "'urn:etoegang:1.9:EntityConcernedID:RSIN'." + ), + ) + # TODO: what if the claims for kvk/RSIN are different claims names? + legal_subject_claim = ClaimField( + verbose_name=_("company identifier claim"), + default=ClaimFieldDefault("urn:etoegang:core:LegalSubjectID"), + help_text=_( + "Name of the claim holding the identifier of the authenticated company." + ), + ) + acting_subject_claim = ClaimField( + verbose_name=_("acting subject identifier claim"), + default=ClaimFieldDefault("urn:etoegang:core:ActingSubjectID"), + help_text=_( + "Name of the claim holding the (opaque) identifier of the user " + "representing the authenticated company.." + ), + ) + branch_number_claim = ClaimField( + verbose_name=_("branch number claim"), + default=ClaimFieldDefault("urn:etoegang:1.9:ServiceRestriction:Vestigingsnr"), + help_text=_( + "Name of the claim holding the value of the branch number for the " + "authenticated company, if such a restriction applies." + ), + ) + + class Meta: + abstract = True + + @property + def oidcdb_sensitive_claims(self) -> Sequence[ClaimPath]: + return [ + self.legal_subject_claim, # type: ignore + self.branch_number_claim, # type: ignore + ] + + +class EHerkenningConfig(AuthorizeeMixin, OpenIDConnectBaseConfig): """ - Configuration for eHerkenning authentication via OpenID connect + Configuration for eHerkenning authentication via OpenID connect. """ - identifier_claim_name = models.CharField( - _("KVK claim name"), - max_length=100, - help_text=_("The name of the claim in which the KVK of the user is stored"), - default="kvk", - ) oidc_rp_scopes_list = ArrayField( verbose_name=_("OpenID Connect scopes"), base_field=models.CharField(_("OpenID Connect scope"), max_length=50), @@ -31,7 +74,7 @@ class EHerkenningConfig(OpenIDConnectBaseConfig): blank=True, help_text=_( "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" + "These scopes are hardcoded and must be supported by the identity provider." ), ) @@ -40,27 +83,36 @@ class Meta: @property def oidcdb_username_claim(self) -> ClaimPath: - return [self.identifier_claim_name] + return self.legal_subject_claim + +class EHerkenningBewindvoeringConfig(AuthorizeeMixin, OpenIDConnectBaseConfig): + # NOTE: Discussion with an employee from Anoigo states this will always be a BSN, + # not an RSIN or CoC number. + representee_claim = ClaimField( + verbose_name=_("representee identifier claim"), + # TODO: this is Anoigo, but could really be anything... + default=ClaimFieldDefault("sel_uid"), + help_text=_("Name of the claim holding the BSN of the represented person."), + ) -class EHerkenningBewindvoeringConfig(OpenIDConnectBaseConfig): - # TODO: support periods in claim keys - vertegenwoordigde_company_claim_name = models.CharField( - verbose_name=_("vertegenwoordigde company claim name"), - default="aanvrager.kvk", - max_length=50, + mandate_service_id_claim = ClaimField( + verbose_name=_("service ID claim"), + default=ClaimFieldDefault("urn:etoegang:core:ServiceID"), help_text=_( - "Name of the claim in which the KVK of the company being represented is stored" + "Name of the claim holding the service ID for which the company " + "is authorized." ), ) - gemachtigde_person_claim_name = models.CharField( - verbose_name=_("gemachtigde person claim name"), - default="gemachtigde.pseudoID", - max_length=50, + mandate_service_uuid_claim = ClaimField( + verbose_name=_("service UUID claim"), + default=ClaimFieldDefault("urn:etoegang:core:ServiceUUID"), help_text=_( - "Name of the claim in which the ID of the person representing a company is stored" + "Name of the claim holding the service UUID for which the company " + "is authorized." ), ) + oidc_rp_scopes_list = ArrayField( verbose_name=_("OpenID Connect scopes"), base_field=models.CharField(_("OpenID Connect scope"), max_length=50), @@ -68,7 +120,7 @@ class EHerkenningBewindvoeringConfig(OpenIDConnectBaseConfig): blank=True, help_text=_( "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" + "These scopes are hardcoded and must be supported by the identity provider." ), ) @@ -76,13 +128,8 @@ class Meta: verbose_name = _("OpenID Connect configuration for eHerkenning Bewindvoering") @property - def digid_eherkenning_machtigen_claims(self) -> dict[str, ClaimPath]: - # TODO: this nomenclature isn't entirely correct - return { - "vertegenwoordigde": [self.vertegenwoordigde_company_claim_name], - "gemachtigde": [self.gemachtigde_person_claim_name], - } - - @property - def oidcdb_sensitive_claims(self) -> Collection[ClaimPath]: - return list(self.digid_eherkenning_machtigen_claims.values()) + def oidcdb_sensitive_claims(self) -> Sequence[ClaimPath]: + base = super().oidcdb_sensitive_claims + return base + [ + self.representee_claim, # type: ignore + ] diff --git a/setup.cfg b/setup.cfg index f23e6c3..c69b8f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,7 @@ include = [options.extras_require] oidc = - mozilla-django-oidc-db >= 0.17.0 + mozilla-django-oidc-db >= 0.18.0 tests = django-test-migrations pytest diff --git a/tests/oidc/test_claim_obfuscation.py b/tests/oidc/test_claim_obfuscation.py new file mode 100644 index 0000000..2940c63 --- /dev/null +++ b/tests/oidc/test_claim_obfuscation.py @@ -0,0 +1,86 @@ +import pytest +from mozilla_django_oidc_db.models import OpenIDConnectConfigBase +from mozilla_django_oidc_db.typing import JSONObject +from mozilla_django_oidc_db.utils import obfuscate_claims + +from digid_eherkenning.oidc.models import ( + DigiDConfig, + DigiDMachtigenConfig, + EHerkenningBewindvoeringConfig, + EHerkenningConfig, +) + + +@pytest.mark.parametrize( + "config,claims,expected", + ( + ( + DigiDConfig(bsn_claim=["bsn"]), + {"bsn": "123456789", "other": "other"}, + {"bsn": "*******89", "other": "other"}, + ), + ( + DigiDMachtigenConfig( + representee_bsn_claim=["aanvrager"], + authorizee_bsn_claim=["gemachtigde"], + ), + { + "aanvrager": "123456789", + "gemachtigde": "123456789", + "other": "other", + }, + { + "aanvrager": "*******89", + "gemachtigde": "*******89", + "other": "other", + }, + ), + ( + EHerkenningConfig( + legal_subject_claim=["kvk"], + acting_subject_claim=["ActingSubject"], + branch_number_claim=["branch"], + ), + { + "kvk": "12345678", + "branch": "112233445566", + # this is already obfuscated by the broker + "ActingSubject": "1234567890@0987654321", + }, + { + "kvk": "*******8", + "branch": "**********66", + # this is already obfuscated by the broker + "ActingSubject": "1234567890@0987654321", + }, + ), + ( + EHerkenningBewindvoeringConfig( + representee_claim=["bsn"], + legal_subject_claim=["kvk"], + acting_subject_claim=["ActingSubject"], + branch_number_claim=["branch"], + ), + { + "bsn": "123456789", + "kvk": "12345678", + "branch": "112233445566", + # this is already obfuscated by the broker + "ActingSubject": "1234567890@0987654321", + }, + { + "bsn": "*******89", + "kvk": "*******8", + "branch": "**********66", + # this is already obfuscated by the broker + "ActingSubject": "1234567890@0987654321", + }, + ), + ), +) +def test_claim_obfuscation( + config: OpenIDConnectConfigBase, claims: JSONObject, expected: JSONObject +): + obfuscated = obfuscate_claims(claims, config.oidcdb_sensitive_claims) + + assert obfuscated == expected