sources/ldap: add LDAP Debug endpoint
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
92b8cf1b64
commit
deb91bd12b
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
59
schema.yml
59
schema.yml
|
@ -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
|
||||
|
|
Reference in New Issue