Compare commits

...

5 Commits

Author SHA1 Message Date
57a38c93fc cleanup and fix tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 22:57:41 +02:00
93b9dae178 fix logic
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:32:58 +02:00
589c123dc1 update in models too
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:19:31 +02:00
a4d9f08095 also default to entryDN for new sources
(this will also help us with a future migration to better save association with ldap sources)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:01:27 +02:00
d856e403f8 sources/ldap: allow using entryDN as uniqueness field 2024-04-25 19:59:11 +02:00
9 changed files with 33 additions and 46 deletions

View File

@ -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)),

View File

@ -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(

View File

@ -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

View File

@ -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")

View File

@ -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:

View File

@ -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")

View File

@ -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()

View File

@ -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",
}, },
) )

View File

@ -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
/> />