Compare commits

...
This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.

4 commits

Author SHA1 Message Date
Jens Langhammer 2a6479062f
don't overwrite connections for other sources
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-09-27 12:54:33 +02:00
Jens Langhammer 52463b8f96
add group connection
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-09-27 12:44:45 +02:00
Jens Langhammer 330f639a7e
ldap source connection
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-09-27 12:43:39 +02:00
Jens Langhammer 85ea4651e4
minor cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-09-27 12:43:36 +02:00
17 changed files with 258 additions and 47 deletions

View file

@ -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")},
},
),
]

View file

@ -575,6 +575,23 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
unique_together = (("user", "source"),) 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): class ExpiringModel(models.Model):
"""Base Model which can expire, and is automatically cleaned up.""" """Base Model which can expire, and is automatically cleaned up."""

View file

View file

@ -0,0 +1,40 @@
"""Property mapping API Views"""
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.ldap.models import LDAPPropertyMapping
class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""
class Meta:
model = LDAPPropertyMapping
fields = PropertyMappingSerializer.Meta.fields + [
"object_field",
]
class LDAPPropertyMappingFilter(FilterSet):
"""Filter for LDAPPropertyMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = LDAPPropertyMapping
fields = "__all__"
class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset"""
queryset = LDAPPropertyMapping.objects.all()
serializer_class = LDAPPropertyMappingSerializer
filterset_class = LDAPPropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View file

@ -0,0 +1,32 @@
"""LDAP Source Serializer"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.sources import UserSourceConnectionSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.ldap.models import LDAPUserSourceConnection
class LDAPUserSourceConnectionSerializer(UserSourceConnectionSerializer):
"""LDAP Source Serializer"""
class Meta:
model = LDAPUserSourceConnection
fields = ["pk", "user", "source", "unique_identifier"]
extra_kwargs = {
"access_token": {"write_only": True},
}
class LDAPUserSourceConnectionViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = LDAPUserSourceConnection.objects.all()
serializer_class = LDAPUserSourceConnectionSerializer
filterset_fields = ["source__slug"]
search_fields = ["source__slug"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug"]

View file

@ -1,10 +1,7 @@
"""Source API Views""" """Source API Views"""
from typing import Any from typing import Any
from django_filters.filters import AllValuesMultipleFilter from drf_spectacular.utils import extend_schema, inline_serializer
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import DictField, ListField from rest_framework.fields import DictField, ListField
@ -14,12 +11,11 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.tasks import TaskSerializer from authentik.admin.api.tasks import TaskSerializer
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.monitored_tasks import TaskInfo from authentik.events.monitored_tasks import TaskInfo
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.tasks import SYNC_CLASSES from authentik.sources.ldap.tasks import SYNC_CLASSES
@ -154,33 +150,3 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
obj.pop("raw_dn", None) obj.pop("raw_dn", None)
all_objects[class_name].append(obj) all_objects[class_name].append(obj)
return Response(data=all_objects) return Response(data=all_objects)
class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""
class Meta:
model = LDAPPropertyMapping
fields = PropertyMappingSerializer.Meta.fields + [
"object_field",
]
class LDAPPropertyMappingFilter(FilterSet):
"""Filter for LDAPPropertyMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = LDAPPropertyMapping
fields = "__all__"
class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset"""
queryset = LDAPPropertyMapping.objects.all()
serializer_class = LDAPPropertyMappingSerializer
filterset_class = LDAPPropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View file

@ -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",),
),
]

View file

@ -10,7 +10,13 @@ from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult, LDAPSchemaError from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult, LDAPSchemaError
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from authentik.core.models import Group, PropertyMapping, Source from authentik.core.models import (
Group,
GroupSourceConnection,
PropertyMapping,
Source,
UserSourceConnection,
)
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import DomainlessURLValidator from authentik.lib.models import DomainlessURLValidator
@ -113,7 +119,7 @@ class LDAPSource(Source):
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import LDAPSourceSerializer from authentik.sources.ldap.api.sources import LDAPSourceSerializer
return LDAPSourceSerializer return LDAPSourceSerializer
@ -202,7 +208,7 @@ class LDAPPropertyMapping(PropertyMapping):
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import LDAPPropertyMappingSerializer from authentik.sources.ldap.api.property_mappings import LDAPPropertyMappingSerializer
return LDAPPropertyMappingSerializer return LDAPPropertyMappingSerializer
@ -212,3 +218,35 @@ class LDAPPropertyMapping(PropertyMapping):
class Meta: class Meta:
verbose_name = _("LDAP Property Mapping") verbose_name = _("LDAP Property Mapping")
verbose_name_plural = _("LDAP Property Mappings") verbose_name_plural = _("LDAP Property Mappings")
class LDAPUserSourceConnection(UserSourceConnection):
"""Connection between an authentik user 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 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")

View file

@ -7,6 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import Group from authentik.core.models import Group
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.sources.ldap.models import LDAPGroupSourceConnection
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
@ -63,7 +64,13 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
}, },
defaults, 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: except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,

View file

@ -7,6 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction 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.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory 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( ak_user, created = self.update_or_create_attributes(
User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults 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: except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
@ -72,6 +80,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
else: else:
self._logger.debug("Synced User", user=ak_user.username, created=created) self._logger.debug("Synced User", user=ak_user.username, created=created)
user_count += 1 user_count += 1
# TODO: Optimise vendor sync to not create a new connection
MicrosoftActiveDirectory(self._source).sync(attributes, ak_user, created) MicrosoftActiveDirectory(self._source).sync(attributes, ak_user, created)
FreeIPA(self._source).sync(attributes, ak_user, created) FreeIPA(self._source).sync(attributes, ak_user, created)
return user_count return user_count

View file

@ -2,7 +2,7 @@
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.lib.generators import generate_key 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 from authentik.sources.ldap.models import LDAPSource
LDAP_PASSWORD = generate_key() LDAP_PASSWORD = generate_key()

View file

@ -1,7 +1,10 @@
"""API URLs""" """API URLs"""
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, 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 = [ api_urlpatterns = [
("propertymappings/ldap", LDAPPropertyMappingViewSet), ("propertymappings/ldap", LDAPPropertyMappingViewSet),
("sources/user_connections/ldap", LDAPUserSourceConnectionViewSet),
("sources/ldap", LDAPSourceViewSet), ("sources/ldap", LDAPSourceViewSet),
] ]

View file

@ -68,7 +68,7 @@ class OAuthSource(Source):
# we're using Type[] instead of type[] here since type[] interferes with the property above # we're using Type[] instead of type[] here since type[] interferes with the property above
@property @property
def serializer(self) -> Type[Serializer]: def serializer(self) -> Type[Serializer]:
from authentik.sources.oauth.api.source import OAuthSourceSerializer from authentik.sources.oauth.api.sources import OAuthSourceSerializer
return OAuthSourceSerializer return OAuthSourceSerializer
@ -234,7 +234,7 @@ class UserOAuthSourceConnection(UserSourceConnection):
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.sources.oauth.api.source_connection import ( from authentik.sources.oauth.api.source_connections import (
UserOAuthSourceConnectionSerializer, UserOAuthSourceConnectionSerializer,
) )

View file

@ -3,7 +3,7 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from requests_mock import Mocker from requests_mock import Mocker
from authentik.sources.oauth.api.source import OAuthSourceSerializer from authentik.sources.oauth.api.sources import OAuthSourceSerializer
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource

View file

@ -2,8 +2,8 @@
from django.urls import path from django.urls import path
from authentik.sources.oauth.api.source import OAuthSourceViewSet from authentik.sources.oauth.api.source_connections import UserOAuthSourceConnectionViewSet
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet from authentik.sources.oauth.api.sources import OAuthSourceViewSet
from authentik.sources.oauth.types.registry import RequestKind from authentik.sources.oauth.types.registry import RequestKind
from authentik.sources.oauth.views.dispatcher import DispatcherView from authentik.sources.oauth.views.dispatcher import DispatcherView