sources/ldap: add LDAP Debug endpoint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-02-14 16:06:54 +01:00
parent 92b8cf1b64
commit deb91bd12b
No known key found for this signature in database
11 changed files with 160 additions and 43 deletions

View File

@ -4,9 +4,10 @@ from typing import Any
from django_filters.filters import AllValuesMultipleFilter from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_field 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.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -104,11 +105,38 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
results = [] results = []
for sync_class in SYNC_CLASSES: for sync_class in SYNC_CLASSES:
sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower() sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
task = TaskInfo.by_name(f"ldap_sync/{source.slug}_{sync_name}") task = TaskInfo.by_name(f"ldap_sync:{source.slug}:{sync_name}")
if task: if task:
results.append(task) results.append(task)
return Response(TaskSerializer(results, many=True).data) return Response(TaskSerializer(results, many=True).data)
@extend_schema(
responses={
200: inline_serializer(
"LDAPDebugSerializer",
fields={
"user": ListField(child=DictField(), read_only=True),
"group": ListField(child=DictField(), read_only=True),
"membership": ListField(child=DictField(), read_only=True),
},
),
}
)
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
def debug(self, request: Request, slug: str) -> Response:
"""Get raw LDAP data to debug"""
source = self.get_object()
all_objects = {}
for sync_class in SYNC_CLASSES:
class_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
all_objects.setdefault(class_name, [])
for obj in sync_class(source).get_objects(size_limit=10):
obj: dict
obj.pop("raw_attributes", None)
obj.pop("raw_dn", None)
all_objects[class_name].append(obj)
return Response(data=all_objects)
class LDAPPropertyMappingSerializer(PropertyMappingSerializer): class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer""" """LDAP PropertyMapping Serializer"""

View File

@ -1,8 +1,9 @@
"""authentik LDAP Authentication Backend""" """authentik LDAP Authentication Backend"""
from typing import Optional from typing import Optional
import ldap3
from django.http import HttpRequest from django.http import HttpRequest
from ldap3 import Connection
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.auth import InbuiltBackend from authentik.core.auth import InbuiltBackend
@ -57,7 +58,7 @@ class LDAPBackend(InbuiltBackend):
# Try to bind as new user # Try to bind as new user
LOGGER.debug("Attempting Binding as user", user=user) LOGGER.debug("Attempting Binding as user", user=user)
try: try:
temp_connection = ldap3.Connection( temp_connection = Connection(
source.server, source.server,
user=user.attributes.get(LDAP_DISTINGUISHED_NAME), user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
password=password, password=password,
@ -66,8 +67,8 @@ class LDAPBackend(InbuiltBackend):
) )
temp_connection.bind() temp_connection.bind()
return user return user
except ldap3.core.exceptions.LDAPInvalidCredentialsResult as exception: except LDAPInvalidCredentialsResult as exception:
LOGGER.debug("LDAPInvalidCredentialsResult", user=user, error=exception) LOGGER.debug("LDAPInvalidCredentialsResult", user=user, error=exception)
except ldap3.core.exceptions.LDAPException as exception: except LDAPException as exception:
LOGGER.warning(exception) LOGGER.warning(exception)
return None return None

View File

@ -3,7 +3,7 @@ from enum import IntFlag
from re import split from re import split
from typing import Optional from typing import Optional
import ldap3 from ldap3 import BASE
from ldap3.core.exceptions import LDAPAttributeError from ldap3.core.exceptions import LDAPAttributeError
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -64,7 +64,7 @@ class LDAPPasswordChanger:
root_attrs = self._source.connection.extend.standard.paged_search( root_attrs = self._source.connection.extend.standard.paged_search(
search_base=root_dn, search_base=root_dn,
search_filter="(objectClass=*)", search_filter="(objectClass=*)",
search_scope=ldap3.BASE, search_scope=BASE,
attributes=["pwdProperties"], attributes=["pwdProperties"],
) )
root_attrs = list(root_attrs)[0] root_attrs = list(root_attrs)[0]
@ -97,7 +97,7 @@ class LDAPPasswordChanger:
self._source.connection.extend.standard.paged_search( self._source.connection.extend.standard.paged_search(
search_base=user_dn, search_base=user_dn,
search_filter=self._source.user_object_filter, search_filter=self._source.user_object_filter,
search_scope=ldap3.BASE, search_scope=BASE,
attributes=["displayName", "sAMAccountName"], attributes=["displayName", "sAMAccountName"],
) )
) )

View File

@ -1,5 +1,5 @@
"""Sync LDAP Users and groups into authentik""" """Sync LDAP Users and groups into authentik"""
from typing import Any from typing import Any, Generator
from django.db.models.base import Model from django.db.models.base import Model
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@ -47,9 +47,16 @@ class BaseLDAPSynchronizer:
def message(self, *args, **kwargs): def message(self, *args, **kwargs):
"""Add message that is later added to the System Task and shown to the user""" """Add message that is later added to the System Task and shown to the user"""
self._messages.append(" ".join(args)) formatted_message = " ".join(args)
if "dn" in kwargs:
formatted_message += f"; DN: {kwargs['dn']}"
self._messages.append(formatted_message)
self._logger.warning(*args, **kwargs) self._logger.warning(*args, **kwargs)
def get_objects(self, **kwargs) -> Generator:
"""Get objects from LDAP, implemented in subclass"""
raise NotImplementedError()
def sync(self) -> int: def sync(self) -> int:
"""Sync function, implemented in subclass""" """Sync function, implemented in subclass"""
raise NotImplementedError() raise NotImplementedError()

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users and groups into authentik""" """Sync LDAP Users and groups into authentik"""
import ldap3 from typing import Generator
import ldap3.core.exceptions
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
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
@ -12,19 +13,24 @@ from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchroniz
class GroupLDAPSynchronizer(BaseLDAPSynchronizer): class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users and groups into authentik""" """Sync LDAP Users and groups into authentik"""
def get_objects(self, **kwargs) -> Generator:
return self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
**kwargs,
)
def sync(self) -> int: def sync(self) -> int:
"""Iterate over all LDAP Groups and create authentik_core.Group instances""" """Iterate over all LDAP Groups and create authentik_core.Group instances"""
if not self._source.sync_groups: if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source") self.message("Group syncing is disabled for this Source")
return -1 return -1
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
group_count = 0 group_count = 0
for group in groups: for group in self.get_objects():
if "attributes" not in group:
continue
attributes = group.get("attributes", {}) attributes = group.get("attributes", {})
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn")))) group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes: if self._source.object_uniqueness_field not in attributes:

View File

@ -1,9 +1,8 @@
"""Sync LDAP Users and groups into authentik""" """Sync LDAP Users and groups into authentik"""
from typing import Any, Optional from typing import Any, Generator, Optional
import ldap3
import ldap3.core.exceptions
from django.db.models import Q from django.db.models import Q
from ldap3 import SUBTREE
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
@ -20,23 +19,28 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
super().__init__(source) super().__init__(source)
self.group_cache: dict[str, Group] = {} self.group_cache: dict[str, Group] = {}
def sync(self) -> int: def get_objects(self, **kwargs) -> Generator:
"""Iterate over all Users and assign Groups using memberOf Field""" return self._source.connection.extend.standard.paged_search(
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return -1
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups, search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter, search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE, search_scope=SUBTREE,
attributes=[ attributes=[
self._source.group_membership_field, self._source.group_membership_field,
self._source.object_uniqueness_field, self._source.object_uniqueness_field,
LDAP_DISTINGUISHED_NAME, LDAP_DISTINGUISHED_NAME,
], ],
**kwargs,
) )
def sync(self) -> int:
"""Iterate over all Users and assign Groups using memberOf Field"""
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return -1
membership_count = 0 membership_count = 0
for group in groups: for group in self.get_objects():
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, []) members = group.get("attributes", {}).get(self._source.group_membership_field, [])
ak_group = self.get_group(group) ak_group = self.get_group(group)
if not ak_group: if not ak_group:

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users into authentik""" """Sync LDAP Users into authentik"""
import ldap3 from typing import Generator
import ldap3.core.exceptions
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
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
@ -14,19 +15,24 @@ from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
class UserLDAPSynchronizer(BaseLDAPSynchronizer): class UserLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users into authentik""" """Sync LDAP Users into authentik"""
def get_objects(self, **kwargs) -> Generator:
return self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
**kwargs,
)
def sync(self) -> int: def sync(self) -> int:
"""Iterate over all LDAP Users and create authentik_core.User instances""" """Iterate over all LDAP Users and create authentik_core.User instances"""
if not self._source.sync_users: if not self._source.sync_users:
self.message("User syncing is disabled for this Source") self.message("User syncing is disabled for this Source")
return -1 return -1
users = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
user_count = 0 user_count = 0
for user in users: for user in self.get_objects():
if "attributes" not in user:
continue
attributes = user.get("attributes", {}) attributes = user.get("attributes", {})
user_dn = self._flatten(user.get("entryDN", user.get("dn"))) user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes: if self._source.object_uniqueness_field not in attributes:

View File

@ -1,6 +1,6 @@
"""FreeIPA specific""" """FreeIPA specific"""
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any, Generator
from pytz import UTC from pytz import UTC
@ -11,6 +11,9 @@ from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
class FreeIPA(BaseLDAPSynchronizer): class FreeIPA(BaseLDAPSynchronizer):
"""FreeIPA-specific LDAP""" """FreeIPA-specific LDAP"""
def get_objects(self, **kwargs) -> Generator:
yield None
def sync(self, attributes: dict[str, Any], user: User, created: bool): def sync(self, attributes: dict[str, Any], user: User, created: bool):
self.check_pwd_last_set(attributes, user, created) self.check_pwd_last_set(attributes, user, created)

View File

@ -1,7 +1,7 @@
"""Active Directory specific""" """Active Directory specific"""
from datetime import datetime from datetime import datetime
from enum import IntFlag from enum import IntFlag
from typing import Any from typing import Any, Generator
from pytz import UTC from pytz import UTC
@ -42,6 +42,9 @@ class UserAccountControl(IntFlag):
class MicrosoftActiveDirectory(BaseLDAPSynchronizer): class MicrosoftActiveDirectory(BaseLDAPSynchronizer):
"""Microsoft-specific LDAP""" """Microsoft-specific LDAP"""
def get_objects(self, **kwargs) -> Generator:
yield None
def sync(self, attributes: dict[str, Any], user: User, created: bool): def sync(self, attributes: dict[str, Any], user: User, created: bool):
self.ms_check_pwd_last_set(attributes, user, created) self.ms_check_pwd_last_set(attributes, user, created)
self.ms_check_uac(attributes, user) self.ms_check_uac(attributes, user)

View File

@ -44,7 +44,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
# to set the state with # to set the state with
return return
sync = path_to_class(sync_class) sync = path_to_class(sync_class)
self.set_uid(f"{source.slug}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}") self.set_uid(f"{source.slug}:{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
try: try:
sync_inst = sync(source) sync_inst = sync(source)
count = sync_inst.sync() count = sync_inst.sync()

View File

@ -16287,6 +16287,40 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/sources/ldap/{slug}/debug/:
get:
operationId: sources_ldap_debug_retrieve
description: Get raw LDAP data to debug
parameters:
- in: path
name: slug
schema:
type: string
description: Internal source name, used in URLs.
required: true
tags:
- sources
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/LDAPDebug'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/ldap/{slug}/sync_status/: /sources/ldap/{slug}/sync_status/:
get: get:
operationId: sources_ldap_sync_status_list operationId: sources_ldap_sync_status_list
@ -28618,6 +28652,31 @@ components:
- direct - direct
- cached - cached
type: string type: string
LDAPDebug:
type: object
properties:
user:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
group:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
membership:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
required:
- group
- membership
- user
LDAPOutpostConfig: LDAPOutpostConfig:
type: object type: object
description: LDAPProvider Serializer description: LDAPProvider Serializer