diff --git a/authentik/core/migrations/0032_groupsourceconnection.py b/authentik/core/migrations/0032_groupsourceconnection.py new file mode 100644 index 000000000..5b2967b0d --- /dev/null +++ b/authentik/core/migrations/0032_groupsourceconnection.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.5 on 2023-09-27 10:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_core", "0031_alter_user_type"), + ] + + operations = [ + migrations.CreateModel( + name="GroupSourceConnection", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group" + ), + ), + ( + "source", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="authentik_core.source" + ), + ), + ], + options={ + "unique_together": {("group", "source")}, + }, + ), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 7f4c25ca7..38b187077 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -575,6 +575,23 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel): unique_together = (("user", "source"),) +class GroupSourceConnection(SerializerModel, CreatedUpdatedModel): + """Connection between Group and Source.""" + + group = models.ForeignKey(Group, on_delete=models.CASCADE) + source = models.ForeignKey(Source, on_delete=models.CASCADE) + + objects = InheritanceManager() + + @property + def serializer(self) -> type[Serializer]: + """Get serializer for this model""" + raise NotImplementedError + + class Meta: + unique_together = (("group", "source"),) + + class ExpiringModel(models.Model): """Base Model which can expire, and is automatically cleaned up.""" diff --git a/authentik/sources/ldap/api/sources.py b/authentik/sources/ldap/api/sources.py index b0097b2f7..75411aafb 100644 --- a/authentik/sources/ldap/api/sources.py +++ b/authentik/sources/ldap/api/sources.py @@ -150,4 +150,3 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): obj.pop("raw_dn", None) all_objects[class_name].append(obj) return Response(data=all_objects) - diff --git a/authentik/sources/ldap/migrations/0004_ldapgroupsourceconnection_ldapusersourceconnection.py b/authentik/sources/ldap/migrations/0004_ldapgroupsourceconnection_ldapusersourceconnection.py new file mode 100644 index 000000000..24c0c3ed6 --- /dev/null +++ b/authentik/sources/ldap/migrations/0004_ldapgroupsourceconnection_ldapusersourceconnection.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.5 on 2023-09-27 10:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_core", "0032_groupsourceconnection"), + ("authentik_sources_ldap", "0003_ldapsource_client_certificate_ldapsource_sni_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="LDAPGroupSourceConnection", + fields=[ + ( + "groupsourceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.groupsourceconnection", + ), + ), + ("unique_identifier", models.TextField(unique=True)), + ], + options={ + "verbose_name": "LDAP Group Source Connection", + "verbose_name_plural": "LDAP Group Source Connections", + }, + bases=("authentik_core.groupsourceconnection",), + ), + migrations.CreateModel( + name="LDAPUserSourceConnection", + fields=[ + ( + "usersourceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.usersourceconnection", + ), + ), + ("unique_identifier", models.TextField(unique=True)), + ], + options={ + "verbose_name": "LDAP User Source Connection", + "verbose_name_plural": "LDAP User Source Connections", + }, + bases=("authentik_core.usersourceconnection",), + ), + ] diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index 9cf0facfd..724e9b3ce 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -10,7 +10,13 @@ from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult, LDAPSchemaError from rest_framework.serializers import Serializer -from authentik.core.models import Group, PropertyMapping, Source, UserSourceConnection +from authentik.core.models import ( + Group, + GroupSourceConnection, + PropertyMapping, + Source, + UserSourceConnection, +) from authentik.crypto.models import CertificateKeyPair from authentik.lib.config import CONFIG from authentik.lib.models import DomainlessURLValidator @@ -113,7 +119,7 @@ class LDAPSource(Source): @property def serializer(self) -> type[Serializer]: - from authentik.sources.ldap.api import LDAPSourceSerializer + from authentik.sources.ldap.api.sources import LDAPSourceSerializer return LDAPSourceSerializer @@ -202,7 +208,7 @@ class LDAPPropertyMapping(PropertyMapping): @property def serializer(self) -> type[Serializer]: - from authentik.sources.ldap.api import LDAPPropertyMappingSerializer + from authentik.sources.ldap.api.property_mappings import LDAPPropertyMappingSerializer return LDAPPropertyMappingSerializer @@ -221,12 +227,26 @@ class LDAPUserSourceConnection(UserSourceConnection): @property def serializer(self) -> Serializer: - from authentik.sources.ldap.api.source_connections import ( - LDAPUserSourceConnectionSerializer, - ) + from authentik.sources.ldap.api.source_connections import LDAPUserSourceConnectionSerializer return LDAPUserSourceConnectionSerializer class Meta: verbose_name = _("LDAP User Source Connection") verbose_name_plural = _("LDAP User Source Connections") + + +class LDAPGroupSourceConnection(GroupSourceConnection): + """Connection between an authentik group and an LDAP source.""" + + unique_identifier = models.TextField(unique=True) + + @property + def serializer(self) -> Serializer: + from authentik.sources.ldap.api.source_connections import LDAPUserSourceConnectionSerializer + + return LDAPUserSourceConnectionSerializer + + class Meta: + verbose_name = _("LDAP Group Source Connection") + verbose_name_plural = _("LDAP Group Source Connections") diff --git a/authentik/sources/ldap/sync/groups.py b/authentik/sources/ldap/sync/groups.py index 68eedcc34..4d6ebf41c 100644 --- a/authentik/sources/ldap/sync/groups.py +++ b/authentik/sources/ldap/sync/groups.py @@ -7,6 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE from authentik.core.models import Group from authentik.events.models import Event, EventAction +from authentik.sources.ldap.models import LDAPGroupSourceConnection from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer @@ -63,7 +64,13 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer): }, defaults, ) - self._logger.debug("Created group with attributes", **defaults) + LDAPGroupSourceConnection.objects.update_or_create( + defaults={ + "unique_identifier": uniq, + "source": self._source, + }, + group=ak_group, + ) except (IntegrityError, FieldError, TypeError, AttributeError) as exc: Event.new( EventAction.CONFIGURATION_ERROR, diff --git a/authentik/sources/ldap/sync/users.py b/authentik/sources/ldap/sync/users.py index 68d966022..5e6372184 100644 --- a/authentik/sources/ldap/sync/users.py +++ b/authentik/sources/ldap/sync/users.py @@ -7,6 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE from authentik.core.models import User from authentik.events.models import Event, EventAction +from authentik.sources.ldap.models import LDAPUserSourceConnection from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory @@ -58,6 +59,13 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): ak_user, created = self.update_or_create_attributes( User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults ) + LDAPUserSourceConnection.objects.update_or_create( + defaults={ + "unique_identifier": uniq, + "source": self._source, + }, + user=ak_user, + ) except (IntegrityError, FieldError, TypeError, AttributeError) as exc: Event.new( EventAction.CONFIGURATION_ERROR, @@ -72,6 +80,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): else: self._logger.debug("Synced User", user=ak_user.username, created=created) user_count += 1 + # TODO: Optimise vendor sync to not create a new connection MicrosoftActiveDirectory(self._source).sync(attributes, ak_user, created) FreeIPA(self._source).sync(attributes, ak_user, created) return user_count diff --git a/authentik/sources/ldap/tests/test_api.py b/authentik/sources/ldap/tests/test_api.py index 9618fa6ae..1a0e7c89d 100644 --- a/authentik/sources/ldap/tests/test_api.py +++ b/authentik/sources/ldap/tests/test_api.py @@ -2,7 +2,7 @@ from rest_framework.test import APITestCase from authentik.lib.generators import generate_key -from authentik.sources.ldap.api import LDAPSourceSerializer +from authentik.sources.ldap.api.sources import LDAPSourceSerializer from authentik.sources.ldap.models import LDAPSource LDAP_PASSWORD = generate_key() diff --git a/authentik/sources/ldap/urls.py b/authentik/sources/ldap/urls.py index 8e18bf11f..a207c5369 100644 --- a/authentik/sources/ldap/urls.py +++ b/authentik/sources/ldap/urls.py @@ -1,7 +1,7 @@ """API URLs""" -from authentik.sources.ldap.api.sources import LDAPSourceViewSet from authentik.sources.ldap.api.property_mappings import LDAPPropertyMappingViewSet from authentik.sources.ldap.api.source_connections import LDAPUserSourceConnectionViewSet +from authentik.sources.ldap.api.sources import LDAPSourceViewSet api_urlpatterns = [ ("propertymappings/ldap", LDAPPropertyMappingViewSet), diff --git a/authentik/sources/oauth/urls.py b/authentik/sources/oauth/urls.py index 5c3e0dde2..97d1c9777 100644 --- a/authentik/sources/oauth/urls.py +++ b/authentik/sources/oauth/urls.py @@ -2,8 +2,8 @@ from django.urls import path -from authentik.sources.oauth.api.sources import OAuthSourceViewSet from authentik.sources.oauth.api.source_connections import UserOAuthSourceConnectionViewSet +from authentik.sources.oauth.api.sources import OAuthSourceViewSet from authentik.sources.oauth.types.registry import RequestKind from authentik.sources.oauth.views.dispatcher import DispatcherView