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.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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
59
schema.yml
59
schema.yml
|
@ -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
|
||||||
|
|
Reference in New Issue