Compare commits
5 Commits
patch-1
...
sources/ld
Author | SHA1 | Date | |
---|---|---|---|
57a38c93fc | |||
93b9dae178 | |||
589c123dc1 | |||
a4d9f08095 | |||
d856e403f8 |
@ -153,7 +153,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"object_uniqueness_field",
|
"object_uniqueness_field",
|
||||||
models.TextField(
|
models.TextField(
|
||||||
default="objectSid", help_text="Field which contains a unique Identifier."
|
default="entryDN", help_text="Field which contains a unique Identifier."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("sync_groups", models.BooleanField(default=True)),
|
("sync_groups", models.BooleanField(default=True)),
|
||||||
|
@ -88,7 +88,7 @@ class LDAPSource(Source):
|
|||||||
help_text=_("Consider Objects matching this filter to be Groups."),
|
help_text=_("Consider Objects matching this filter to be Groups."),
|
||||||
)
|
)
|
||||||
object_uniqueness_field = models.TextField(
|
object_uniqueness_field = models.TextField(
|
||||||
default="objectSid", help_text=_("Field which contains a unique Identifier.")
|
default="entryDN", help_text=_("Field which contains a unique Identifier.")
|
||||||
)
|
)
|
||||||
|
|
||||||
property_mappings_group = models.ManyToManyField(
|
property_mappings_group = models.ManyToManyField(
|
||||||
|
@ -47,6 +47,15 @@ class BaseLDAPSynchronizer:
|
|||||||
"""UI name for the type of object this class synchronizes"""
|
"""UI name for the type of object this class synchronizes"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_unique_identifier(self, ldap_object: dict) -> str | None:
|
||||||
|
"""Get unique identifier"""
|
||||||
|
attributes = ldap_object.get("attributes", {})
|
||||||
|
if self._source.object_uniqueness_field in attributes:
|
||||||
|
return flatten(attributes[self._source.object_uniqueness_field])
|
||||||
|
if self._source.object_uniqueness_field in ldap_object:
|
||||||
|
return flatten(ldap_object.get(self._source.object_uniqueness_field))
|
||||||
|
return None
|
||||||
|
|
||||||
def sync_full(self):
|
def sync_full(self):
|
||||||
"""Run full sync, this function should only be used in tests"""
|
"""Run full sync, this function should only be used in tests"""
|
||||||
if not settings.TEST: # noqa
|
if not settings.TEST: # noqa
|
||||||
@ -134,20 +143,22 @@ class BaseLDAPSynchronizer:
|
|||||||
cookie = None
|
cookie = None
|
||||||
yield self._connection.response
|
yield self._connection.response
|
||||||
|
|
||||||
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
def build_user_properties(self, user_dn: str, uniq: str, **kwargs) -> dict[str, Any]:
|
||||||
"""Build attributes for User object based on property mappings."""
|
"""Build attributes for User object based on property mappings."""
|
||||||
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
props = self._build_object_properties(
|
||||||
|
user_dn, self._source.property_mappings, uniq, **kwargs
|
||||||
|
)
|
||||||
props.setdefault("path", self._source.get_user_path())
|
props.setdefault("path", self._source.get_user_path())
|
||||||
return props
|
return props
|
||||||
|
|
||||||
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]:
|
def build_group_properties(self, group_dn: str, uniq: str, **kwargs) -> dict[str, Any]:
|
||||||
"""Build attributes for Group object based on property mappings."""
|
"""Build attributes for Group object based on property mappings."""
|
||||||
return self._build_object_properties(
|
return self._build_object_properties(
|
||||||
group_dn, self._source.property_mappings_group, **kwargs
|
group_dn, self._source.property_mappings_group, uniq, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def _build_object_properties(
|
def _build_object_properties(
|
||||||
self, object_dn: str, mappings: QuerySet, **kwargs
|
self, object_dn: str, mappings: QuerySet, uniq: str, **kwargs
|
||||||
) -> dict[str, dict[Any, Any]]:
|
) -> dict[str, dict[Any, Any]]:
|
||||||
properties = {"attributes": {}}
|
properties = {"attributes": {}}
|
||||||
for mapping in mappings.all().select_subclasses():
|
for mapping in mappings.all().select_subclasses():
|
||||||
@ -180,10 +191,7 @@ class BaseLDAPSynchronizer:
|
|||||||
).save()
|
).save()
|
||||||
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
||||||
continue
|
continue
|
||||||
if self._source.object_uniqueness_field in kwargs:
|
properties["attributes"][LDAP_UNIQUENESS] = uniq
|
||||||
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
|
||||||
kwargs.get(self._source.object_uniqueness_field)
|
|
||||||
)
|
|
||||||
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
|
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
@ -41,16 +41,16 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
continue
|
continue
|
||||||
attributes = group.get("attributes", {})
|
attributes = group.get("attributes", {})
|
||||||
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
|
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
|
||||||
if self._source.object_uniqueness_field not in attributes:
|
uniq = self.get_unique_identifier(group)
|
||||||
|
if not uniq:
|
||||||
self.message(
|
self.message(
|
||||||
f"Cannot find uniqueness field in attributes: '{group_dn}'",
|
f"Cannot find uniqueness field in attributes: '{group_dn}'",
|
||||||
attributes=attributes.keys(),
|
attributes=attributes.keys(),
|
||||||
dn=group_dn,
|
dn=group_dn,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
|
||||||
try:
|
try:
|
||||||
defaults = self.build_group_properties(group_dn, **attributes)
|
defaults = self.build_group_properties(group_dn, uniq, **attributes)
|
||||||
defaults["parent"] = self._source.sync_parent_group
|
defaults["parent"] = self._source.sync_parent_group
|
||||||
if "name" not in defaults:
|
if "name" not in defaults:
|
||||||
raise IntegrityError("Name was not set by propertymappings")
|
raise IntegrityError("Name was not set by propertymappings")
|
||||||
|
@ -4,7 +4,7 @@ from collections.abc import Generator
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from ldap3 import SUBTREE
|
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, 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
|
||||||
@ -33,11 +33,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
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=SUBTREE,
|
search_scope=SUBTREE,
|
||||||
attributes=[
|
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
||||||
self._source.group_membership_field,
|
|
||||||
self._source.object_uniqueness_field,
|
|
||||||
LDAP_DISTINGUISHED_NAME,
|
|
||||||
],
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -80,7 +76,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
def get_group(self, group_dict: dict[str, Any]) -> Group | None:
|
def get_group(self, group_dict: dict[str, Any]) -> Group | None:
|
||||||
"""Check if we fetched the group already, and if not cache it for later"""
|
"""Check if we fetched the group already, and if not cache it for later"""
|
||||||
group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
|
group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
|
||||||
group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
|
group_uniq = self.get_unique_identifier(group_dict)
|
||||||
# group_uniq might be a single string or an array with (hopefully) a single string
|
# group_uniq might be a single string or an array with (hopefully) a single string
|
||||||
if isinstance(group_uniq, list):
|
if isinstance(group_uniq, list):
|
||||||
if len(group_uniq) < 1:
|
if len(group_uniq) < 1:
|
||||||
|
@ -43,16 +43,16 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
continue
|
continue
|
||||||
attributes = user.get("attributes", {})
|
attributes = user.get("attributes", {})
|
||||||
user_dn = flatten(user.get("entryDN", user.get("dn")))
|
user_dn = flatten(user.get("entryDN", user.get("dn")))
|
||||||
if self._source.object_uniqueness_field not in attributes:
|
uniq = self.get_unique_identifier(user)
|
||||||
|
if not uniq:
|
||||||
self.message(
|
self.message(
|
||||||
f"Cannot find uniqueness field in attributes: '{user_dn}'",
|
f"Cannot find uniqueness field in attributes: '{user_dn}'",
|
||||||
attributes=attributes.keys(),
|
attributes=attributes.keys(),
|
||||||
dn=user_dn,
|
dn=user_dn,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
|
||||||
try:
|
try:
|
||||||
defaults = self.build_user_properties(user_dn, **attributes)
|
defaults = self.build_user_properties(user_dn, uniq, **attributes)
|
||||||
self._logger.debug("Writing user with attributes", **defaults)
|
self._logger.debug("Writing user with attributes", **defaults)
|
||||||
if "username" not in defaults:
|
if "username" not in defaults:
|
||||||
raise IntegrityError("Username was not set by propertymappings")
|
raise IntegrityError("Username was not set by propertymappings")
|
||||||
|
@ -41,7 +41,7 @@ def mock_ad_connection(password: str) -> Connection:
|
|||||||
connection.strategy.add_entry(
|
connection.strategy.add_entry(
|
||||||
"cn=group2,ou=groups,dc=goauthentik,dc=io",
|
"cn=group2,ou=groups,dc=goauthentik,dc=io",
|
||||||
{
|
{
|
||||||
"name": "test-group",
|
"name": "test-group2",
|
||||||
"objectClass": "group",
|
"objectClass": "group",
|
||||||
"distinguishedName": "cn=group2,ou=groups,dc=goauthentik,dc=io",
|
"distinguishedName": "cn=group2,ou=groups,dc=goauthentik,dc=io",
|
||||||
},
|
},
|
||||||
@ -61,18 +61,6 @@ def mock_ad_connection(password: str) -> Connection:
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# User without SID
|
|
||||||
connection.strategy.add_entry(
|
|
||||||
"cn=user1,ou=users,dc=goauthentik,dc=io",
|
|
||||||
{
|
|
||||||
"userPassword": "test1111",
|
|
||||||
"sAMAccountName": "user2_sn",
|
|
||||||
"name": "user1_sn",
|
|
||||||
"revision": 0,
|
|
||||||
"objectClass": "person",
|
|
||||||
"distinguishedName": "cn=user1,ou=users,dc=goauthentik,dc=io",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# Duplicate users
|
# Duplicate users
|
||||||
connection.strategy.add_entry(
|
connection.strategy.add_entry(
|
||||||
"cn=user2,ou=users,dc=goauthentik,dc=io",
|
"cn=user2,ou=users,dc=goauthentik,dc=io",
|
||||||
@ -87,7 +75,7 @@ def mock_ad_connection(password: str) -> Connection:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
connection.strategy.add_entry(
|
connection.strategy.add_entry(
|
||||||
"cn=user3,ou=users,dc=goauthentik,dc=io",
|
"cn=user2,ou=users,dc=goauthentik,dc=io",
|
||||||
{
|
{
|
||||||
"userPassword": "test2222",
|
"userPassword": "test2222",
|
||||||
"sAMAccountName": "user2_sn",
|
"sAMAccountName": "user2_sn",
|
||||||
@ -95,7 +83,7 @@ def mock_ad_connection(password: str) -> Connection:
|
|||||||
"revision": 0,
|
"revision": 0,
|
||||||
"objectSid": "unique-test2222",
|
"objectSid": "unique-test2222",
|
||||||
"objectClass": "person",
|
"objectClass": "person",
|
||||||
"distinguishedName": "cn=user3,ou=users,dc=goauthentik,dc=io",
|
"distinguishedName": "cn=user2,ou=users,dc=goauthentik,dc=io",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
connection.bind()
|
connection.bind()
|
||||||
|
@ -108,12 +108,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
username="user0_sn",
|
username="user0_sn",
|
||||||
attributes={
|
attributes={
|
||||||
"ldap_uniq": (
|
"ldap_uniq": "cn=user0,ou=foo,ou=users,dc=goauthentik,dc=io",
|
||||||
"S-117-6648368-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
|
|
||||||
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
|
|
||||||
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
|
|
||||||
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0"
|
|
||||||
),
|
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -470,7 +470,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value="${this.instance?.objectUniquenessField || "objectSid"}"
|
value="${this.instance?.objectUniquenessField || "entryDN"}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user