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.filterset import FilterSet
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.exceptions import ValidationError
from rest_framework.fields import DictField, ListField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
@ -104,11 +105,38 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
results = []
for sync_class in SYNC_CLASSES:
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:
results.append(task)
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):
"""LDAP PropertyMapping Serializer"""

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""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.query import QuerySet
@ -47,9 +47,16 @@ class BaseLDAPSynchronizer:
def message(self, *args, **kwargs):
"""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)
def get_objects(self, **kwargs) -> Generator:
"""Get objects from LDAP, implemented in subclass"""
raise NotImplementedError()
def sync(self) -> int:
"""Sync function, implemented in subclass"""
raise NotImplementedError()

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users and groups into authentik"""
import ldap3
import ldap3.core.exceptions
from typing import Generator
from django.core.exceptions import FieldError
from django.db.utils import IntegrityError
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import Group
from authentik.events.models import Event, EventAction
@ -12,19 +13,24 @@ from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchroniz
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
"""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:
"""Iterate over all LDAP Groups and create authentik_core.Group instances"""
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_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
group_count = 0
for group in groups:
for group in self.get_objects():
if "attributes" not in group:
continue
attributes = group.get("attributes", {})
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:

View File

@ -1,9 +1,8 @@
"""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 ldap3 import SUBTREE
from authentik.core.models import Group, User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
@ -20,23 +19,28 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
super().__init__(source)
self.group_cache: dict[str, Group] = {}
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
groups = self._source.connection.extend.standard.paged_search(
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=ldap3.SUBTREE,
search_scope=SUBTREE,
attributes=[
self._source.group_membership_field,
self._source.object_uniqueness_field,
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
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, [])
ak_group = self.get_group(group)
if not ak_group:

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users into authentik"""
import ldap3
import ldap3.core.exceptions
from typing import Generator
from django.core.exceptions import FieldError
from django.db.utils import IntegrityError
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import User
from authentik.events.models import Event, EventAction
@ -14,19 +15,24 @@ from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
class UserLDAPSynchronizer(BaseLDAPSynchronizer):
"""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:
"""Iterate over all LDAP Users and create authentik_core.User instances"""
if not self._source.sync_users:
self.message("User syncing is disabled for this Source")
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
for user in users:
for user in self.get_objects():
if "attributes" not in user:
continue
attributes = user.get("attributes", {})
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:

View File

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

View File

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

View File

@ -16287,6 +16287,40 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
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/:
get:
operationId: sources_ldap_sync_status_list
@ -28618,6 +28652,31 @@ components:
- direct
- cached
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:
type: object
description: LDAPProvider Serializer