From a6ac82c492c6134e1bdc43a444cf225fb8b3f075 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 6 Feb 2021 16:56:21 +0100 Subject: [PATCH] *: rewrite managed objects, use nullable text flag instead of boolean as uid (#533) --- authentik/core/api/propertymappings.py | 2 +- authentik/core/migrations/0017_managed.py | 12 ++++--- authentik/managed/manager.py | 23 ++++++------- authentik/managed/models.py | 8 +++-- authentik/outposts/models.py | 2 +- authentik/providers/oauth2/managed.py | 6 ++-- .../oauth2/migrations/0011_managed.py | 10 +++++- authentik/providers/proxy/managed.py | 2 +- authentik/providers/saml/managed.py | 31 ++++++++++-------- .../providers/saml/migrations/0012_managed.py | 14 ++++++-- authentik/sources/ldap/managed.py | 32 ++++++++++++------- .../sources/ldap/migrations/0008_managed.py | 11 ++++++- authentik/sources/ldap/tests/test_sync.py | 28 ++++++++-------- swagger.yaml | 2 +- .../PropertyMappingListPage.ts | 2 +- 15 files changed, 115 insertions(+), 70 deletions(-) diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index 753167a56..b87865c86 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -42,7 +42,7 @@ class PropertyMappingViewSet(ReadOnlyModelViewSet): search_fields = [ "name", ] - filterset_fields = ["managed"] + filterset_fields = {"managed": ["isnull"]} ordering = ["name"] def get_queryset(self): diff --git a/authentik/core/migrations/0017_managed.py b/authentik/core/migrations/0017_managed.py index d508ab211..e5aa723fe 100644 --- a/authentik/core/migrations/0017_managed.py +++ b/authentik/core/migrations/0017_managed.py @@ -13,19 +13,23 @@ class Migration(migrations.Migration): migrations.AddField( model_name="propertymapping", name="managed", - field=models.BooleanField( - default=False, + field=models.TextField( + default=None, help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.", + null=True, verbose_name="Managed by authentik", + unique=True, ), ), migrations.AddField( model_name="token", name="managed", - field=models.BooleanField( - default=False, + field=models.TextField( + default=None, help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.", + null=True, verbose_name="Managed by authentik", + unique=True, ), ), ] diff --git a/authentik/managed/manager.py b/authentik/managed/manager.py index e03927af5..17b88c791 100644 --- a/authentik/managed/manager.py +++ b/authentik/managed/manager.py @@ -12,12 +12,12 @@ class EnsureOp: """Ensure operation, executed as part of an ObjectManager run""" _obj: Type[ManagedModel] - _match_fields: tuple[str, ...] + _managed_uid: str _kwargs: dict - def __init__(self, obj: Type[ManagedModel], *match_fields: str, **kwargs) -> None: + def __init__(self, obj: Type[ManagedModel], managed_uid: str, **kwargs) -> None: self._obj = obj - self._match_fields = match_fields + self._managed_uid = managed_uid self._kwargs = kwargs def run(self): @@ -29,16 +29,13 @@ class EnsureExists(EnsureOp): """Ensure object exists, with kwargs as given values""" def run(self): - update_kwargs = { - "managed": True, - "defaults": self._kwargs, - } - for field in self._match_fields: - value = self._kwargs.get(field, None) - if value: - update_kwargs[field] = value - self._kwargs.setdefault("managed", True) - self._obj.objects.update_or_create(**update_kwargs) + self._kwargs.setdefault("managed", self._managed_uid) + self._obj.objects.update_or_create( + **{ + "managed": self._managed_uid, + "defaults": self._kwargs, + } + ) class ObjectManager: diff --git a/authentik/managed/models.py b/authentik/managed/models.py index f863baf1a..d6dca21e6 100644 --- a/authentik/managed/models.py +++ b/authentik/managed/models.py @@ -7,8 +7,9 @@ from django.utils.translation import gettext_lazy as _ class ManagedModel(models.Model): """Model which can be managed by authentik exclusively""" - managed = models.BooleanField( - default=False, + managed = models.TextField( + default=None, + null=True, verbose_name=_("Managed by authentik"), help_text=_( ( @@ -18,11 +19,12 @@ class ManagedModel(models.Model): "to be overwritten in a later update." ) ), + unique=True, ) def managed_objects(self) -> QuerySet: """Get all objects which are managed""" - return self.objects.filter(managed=True) + return self.objects.exclude(managed__isnull=True) class Meta: diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 07885759f..be9a359fb 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -363,7 +363,7 @@ class Outpost(models.Model): intent=TokenIntents.INTENT_API, description=f"Autogenerated by authentik for Outpost {self.name}", expiring=False, - managed=True, + managed="goauthentik.io/outpost", ) def get_required_objects(self) -> Iterable[models.Model]: diff --git a/authentik/providers/oauth2/managed.py b/authentik/providers/oauth2/managed.py index 876aca483..61e9efd04 100644 --- a/authentik/providers/oauth2/managed.py +++ b/authentik/providers/oauth2/managed.py @@ -34,14 +34,14 @@ class ScopeMappingManager(ObjectManager): return [ EnsureExists( ScopeMapping, - "scope_name", + "goauthentik.io/providers/oauth2/scope-openid", name="authentik default OAuth Mapping: OpenID 'openid'", scope_name="openid", expression=SCOPE_OPENID_EXPRESSION, ), EnsureExists( ScopeMapping, - "scope_name", + "goauthentik.io/providers/oauth2/scope-email", name="authentik default OAuth Mapping: OpenID 'email'", scope_name="email", description="Email address", @@ -49,7 +49,7 @@ class ScopeMappingManager(ObjectManager): ), EnsureExists( ScopeMapping, - "scope_name", + "goauthentik.io/providers/oauth2/scope-profile", name="authentik default OAuth Mapping: OpenID 'profile'", scope_name="profile", description="General Profile Information", diff --git a/authentik/providers/oauth2/migrations/0011_managed.py b/authentik/providers/oauth2/migrations/0011_managed.py index f7bc5b276..9920131a9 100644 --- a/authentik/providers/oauth2/migrations/0011_managed.py +++ b/authentik/providers/oauth2/migrations/0011_managed.py @@ -3,6 +3,13 @@ from django.apps.registry import Apps from django.db import migrations +scope_uid_map = { + "openid": "goauthentik.io/providers/oauth2/scope-openid", + "email": "goauthentik.io/providers/oauth2/scope-email", + "profile": "goauthentik.io/providers/oauth2/scope-profile", + "ak_proxy": "goauthentik.io/providers/proxy/scope-proxy", +} + def set_managed_flag(apps: Apps, schema_editor): ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping") @@ -10,13 +17,14 @@ def set_managed_flag(apps: Apps, schema_editor): for mapping in ScopeMapping.objects.using(db_alias).filter( name__startswith="Autogenerated " ): - mapping.managed = True + mapping.managed = scope_uid_map[mapping.scope_name] mapping.save() class Migration(migrations.Migration): dependencies = [ + ("authentik_core", "0017_managed"), ("authentik_providers_oauth2", "0010_auto_20201227_1804"), ] diff --git a/authentik/providers/proxy/managed.py b/authentik/providers/proxy/managed.py index 75bb6e48a..44c59af2d 100644 --- a/authentik/providers/proxy/managed.py +++ b/authentik/providers/proxy/managed.py @@ -20,7 +20,7 @@ class ProxyScopeMappingManager(ObjectManager): return [ EnsureExists( ScopeMapping, - "scope_name", + "goauthentik.io/providers/proxy/scope-proxy", name="authentik default OAuth Mapping: proxy outpost", scope_name=SCOPE_AK_PROXY, expression=SCOPE_AK_PROXY_EXPRESSION, diff --git a/authentik/providers/saml/managed.py b/authentik/providers/saml/managed.py index 5b1817e53..d20467ec1 100644 --- a/authentik/providers/saml/managed.py +++ b/authentik/providers/saml/managed.py @@ -2,6 +2,11 @@ from authentik.managed.manager import EnsureExists, ObjectManager from authentik.providers.saml.models import SAMLPropertyMapping +GROUP_EXPRESSION = """ +for group in user.ak_groups.all(): + yield group.name +""" + class SAMLProviderManager(ObjectManager): """SAML Provider managed objects""" @@ -10,53 +15,53 @@ class SAMLProviderManager(ObjectManager): return [ EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/upn", name="authentik default SAML Mapping: UPN", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", expression="return user.attributes.get('upn', user.email)", ), EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/name", name="authentik default SAML Mapping: Name", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", expression="return user.name", ), EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/email", name="authentik default SAML Mapping: Email", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", expression="return user.email", ), EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/username", name="authentik default SAML Mapping: Username", saml_name="http://schemas.goauthentik.io/2021/02/saml/username", expression="return user.username", ), EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/uid", name="authentik default SAML Mapping: User ID", saml_name="http://schemas.goauthentik.io/2021/02/saml/uid", expression="return user.pk", ), EnsureExists( SAMLPropertyMapping, - "saml_name", + "goauthentik.io/providers/saml/groups", + name="authentik default SAML Mapping: Groups", + saml_name="http://schemas.xmlsoap.org/claims/Group", + expression=GROUP_EXPRESSION, + ), + EnsureExists( + SAMLPropertyMapping, + "goauthentik.io/providers/saml/ms-windowsaccountname", name="authentik default SAML Mapping: WindowsAccountname (Username)", saml_name=( "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" ), expression="return user.username", ), - EnsureExists( - SAMLPropertyMapping, - "saml_name", - name="authentik default SAML Mapping: Groups", - saml_name="http://schemas.xmlsoap.org/claims/Group", - expression="for group in user.ak_groups.all():\n yield group.name", - ), ] diff --git a/authentik/providers/saml/migrations/0012_managed.py b/authentik/providers/saml/migrations/0012_managed.py index 59f175971..c290b42c8 100644 --- a/authentik/providers/saml/migrations/0012_managed.py +++ b/authentik/providers/saml/migrations/0012_managed.py @@ -6,6 +6,7 @@ saml_name_map = { "http://schemas.xmlsoap.org/claims/CommonName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", "member-of": "http://schemas.xmlsoap.org/claims/Group", + "http://schemas.xmlsoap.org/claims/Group": "http://schemas.xmlsoap.org/claims/Group", "urn:oid:0.9.2342.19200300.100.1.1": "http://schemas.goauthentik.io/2021/02/saml/uid", "urn:oid:0.9.2342.19200300.100.1.3": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "urn:oid:1.3.6.1.4.1.5923.1.1.1.6": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", @@ -13,6 +14,16 @@ saml_name_map = { "urn:oid:2.5.4.3": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", } +saml_name_uid_map = { + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "goauthentik.io/providers/saml/upn", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "goauthentik.io/providers/saml/name", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "goauthentik.io/providers/saml/email", + "http://schemas.goauthentik.io/2021/02/saml/username": "goauthentik.io/providers/saml/username", + "http://schemas.goauthentik.io/2021/02/saml/uid": "goauthentik.io/providers/saml/uid", + "http://schemas.xmlsoap.org/claims/Group": "goauthentik.io/providers/saml/groups", + "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname": "goauthentik.io/providers/saml/ms-windowsaccountname", +} + def add_managed_update(apps, schema_editor): """Create default SAML Property Mappings""" @@ -23,15 +34,14 @@ def add_managed_update(apps, schema_editor): for pm in SAMLPropertyMapping.objects.using(db_alias).filter( name__startswith="Autogenerated " ): - pm.managed = True if pm.saml_name not in saml_name_map: - pm.save() continue new_name = saml_name_map[pm.saml_name] if not new_name: pm.delete() continue pm.saml_name = new_name + pm.managed = saml_name_uid_map[new_name] pm.save() diff --git a/authentik/sources/ldap/managed.py b/authentik/sources/ldap/managed.py index 49fb289b2..1038930df 100644 --- a/authentik/sources/ldap/managed.py +++ b/authentik/sources/ldap/managed.py @@ -10,15 +10,14 @@ class LDAPProviderManager(ObjectManager): return [ EnsureExists( LDAPPropertyMapping, - "object_field", - "expression", - name="authentik default LDAP Mapping: name", + "goauthentik.io/sources/ldap/default-name", + name="authentik default LDAP Mapping: Name", object_field="name", expression="return ldap.get('name')", ), EnsureExists( LDAPPropertyMapping, - "object_field", + "goauthentik.io/sources/ldap/default-mail", name="authentik default LDAP Mapping: mail", object_field="email", expression="return ldap.get('mail')", @@ -26,32 +25,43 @@ class LDAPProviderManager(ObjectManager): # Active Directory-specific mappings EnsureExists( LDAPPropertyMapping, - "object_field", - "expression", + "goauthentik.io/sources/ldap/ms-samaccountname", name="authentik default Active Directory Mapping: sAMAccountName", object_field="username", expression="return ldap.get('sAMAccountName')", ), EnsureExists( LDAPPropertyMapping, - "object_field", + "goauthentik.io/sources/ldap/ms-userprincipalname", name="authentik default Active Directory Mapping: userPrincipalName", object_field="attributes.upn", expression="return ldap.get('userPrincipalName')", ), + EnsureExists( + LDAPPropertyMapping, + "goauthentik.io/sources/ldap/ms-givenName", + name="authentik default Active Directory Mapping: givenName", + object_field="attributes.givenName", + expression="return ldap.get('givenName')", + ), + EnsureExists( + LDAPPropertyMapping, + "goauthentik.io/sources/ldap/ms-sn", + name="authentik default Active Directory Mapping: sn", + object_field="attributes.sn", + expression="return ldap.get('sn')", + ), # OpenLDAP specific mappings EnsureExists( LDAPPropertyMapping, - "object_field", - "expression", + "goauthentik.io/sources/ldap/openldap-uid", name="authentik default OpenLDAP Mapping: uid", object_field="username", expression="return ldap.get('uid')", ), EnsureExists( LDAPPropertyMapping, - "object_field", - "expression", + "goauthentik.io/sources/ldap/openldap-cn", name="authentik default OpenLDAP Mapping: cn", object_field="name", expression="return ldap.get('cn')", diff --git a/authentik/sources/ldap/migrations/0008_managed.py b/authentik/sources/ldap/migrations/0008_managed.py index 84cb61f02..8264c05ff 100644 --- a/authentik/sources/ldap/migrations/0008_managed.py +++ b/authentik/sources/ldap/migrations/0008_managed.py @@ -9,16 +9,25 @@ def set_managed_flag(apps: Apps, schema_editor): "authentik_sources_ldap", "LDAPPropertyMapping" ) db_alias = schema_editor.connection.alias + field_to_uid = { + "name": "goauthentik.io/sources/ldap/default-name", + "email": "goauthentik.io/sources/ldap/default-mail", + "username": "goauthentik.io/sources/ldap/ms-samaccountname", + "attributes.upn": "goauthentik.io/sources/ldap/ms-userprincipalname", + "first_name": "goauthentik.io/sources/ldap/ms-givenName", + "last_name": "goauthentik.io/sources/ldap/ms-sn", + } for mapping in LDAPPropertyMapping.objects.using(db_alias).filter( name__startswith="Autogenerated " ): - mapping.managed = True + mapping.managed = field_to_uid.get(mapping.object_field) mapping.save() class Migration(migrations.Migration): dependencies = [ + ("authentik_core", "0017_managed"), ("authentik_sources_ldap", "0007_ldapsource_sync_users_password"), ] diff --git a/authentik/sources/ldap/tests/test_sync.py b/authentik/sources/ldap/tests/test_sync.py index 18b80ccb9..02e18dc47 100644 --- a/authentik/sources/ldap/tests/test_sync.py +++ b/authentik/sources/ldap/tests/test_sync.py @@ -35,8 +35,8 @@ class LDAPSyncTests(TestCase): """Test user sync""" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default Active Directory Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/ms") ) ) self.source.save() @@ -52,8 +52,8 @@ class LDAPSyncTests(TestCase): self.source.object_uniqueness_field = "uid" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default OpenLDAP Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") ) ) self.source.save() @@ -68,13 +68,13 @@ class LDAPSyncTests(TestCase): """Test group sync""" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default Active Directory Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/ms") ) ) self.source.property_mappings_group.set( LDAPPropertyMapping.objects.filter( - name="authentik default LDAP Mapping: name" + managed="goauthentik.io/sources/ldap/default-name" ) ) self.source.save() @@ -93,13 +93,13 @@ class LDAPSyncTests(TestCase): self.source.group_object_filter = "(objectClass=groupOfNames)" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default OpenLDAP Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") ) ) self.source.property_mappings_group.set( LDAPPropertyMapping.objects.filter( - name="authentik default OpenLDAP Mapping: cn" + managed="goauthentik.io/sources/ldap/openldap-cn" ) ) self.source.save() @@ -116,8 +116,8 @@ class LDAPSyncTests(TestCase): """Test Scheduled tasks""" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default Active Directory Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/ms") ) ) self.source.save() @@ -131,8 +131,8 @@ class LDAPSyncTests(TestCase): self.source.group_object_filter = "(objectClass=groupOfNames)" self.source.property_mappings.set( LDAPPropertyMapping.objects.filter( - Q(name__startswith="authentik default LDAP Mapping") - | Q(name__startswith="authentik default OpenLDAP Mapping") + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") ) ) self.source.save() diff --git a/swagger.yaml b/swagger.yaml index ce6844513..9f4ca7d58 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -3597,7 +3597,7 @@ paths: operationId: propertymappings_all_list description: PropertyMapping Viewset parameters: - - name: managed + - name: managed__isnull in: query description: '' required: false diff --git a/web/src/pages/property-mappings/PropertyMappingListPage.ts b/web/src/pages/property-mappings/PropertyMappingListPage.ts index d895f4594..579e673b7 100644 --- a/web/src/pages/property-mappings/PropertyMappingListPage.ts +++ b/web/src/pages/property-mappings/PropertyMappingListPage.ts @@ -35,7 +35,7 @@ export class PropertyMappingListPage extends TablePage { ordering: this.order, page: page, search: this.search || "", - managed: this.hideManaged ? false : null, + managed__isnull: this.hideManaged, }); }