From 8478b03892112d466180f282149ff457580e0881 Mon Sep 17 00:00:00 2001 From: "Langhammer, Jens" Date: Fri, 11 Oct 2019 13:41:12 +0200 Subject: [PATCH] sources/ldap(major): implement membership sync, add more settings --- passbook/sources/ldap/connector.py | 58 +++++++++++++++---- passbook/sources/ldap/forms.py | 6 +- .../migrations/0005_auto_20191011_1059.py | 33 +++++++++++ passbook/sources/ldap/models.py | 10 +++- 4 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 passbook/sources/ldap/migrations/0005_auto_20191011_1059.py diff --git a/passbook/sources/ldap/connector.py b/passbook/sources/ldap/connector.py index 02725ab37..0c09e2af2 100644 --- a/passbook/sources/ldap/connector.py +++ b/passbook/sources/ldap/connector.py @@ -49,11 +49,9 @@ class Connector: def sync_groups(self): """Iterate over all LDAP Groups and create passbook_core.Group instances""" - attributes = [ - 'objectSid', # Used as unique Identifier - 'name', - 'dn', - ] + if not self._source.sync_groups: + LOGGER.debug("Group syncing is disabled for this Source") + return groups = self._connection.extend.standard.paged_search( search_base=self.base_dn_groups, search_filter=self._source.group_object_filter, @@ -62,8 +60,16 @@ class Connector: for group in groups: attributes = group.get('attributes', {}) _, created = Group.objects.update_or_create( - attributes__objectSid=attributes.get('objectSid', ''), - defaults=self._build_object_properties(attributes), + attributes__ldap_uniq=attributes.get(self._source.object_uniqueness_field, ''), + parent=self._source.sync_parent_group, + # defaults=self._build_object_properties(attributes), + defaults={ + 'name': attributes.get('name', ''), + 'attributes': { + 'ldap_uniq': attributes.get(self._source.object_uniqueness_field, ''), + 'distinguishedName': attributes.get('distinguishedName') + } + } ) LOGGER.debug("Synced group", group=attributes.get('name', ''), created=created) @@ -77,14 +83,43 @@ class Connector: for user in users: attributes = user.get('attributes', {}) _, created = User.objects.update_or_create( - attributes__objectSid=attributes.get('objectSid', ''), + attributes__ldap_uniq=attributes.get(self._source.object_uniqueness_field, ''), defaults=self._build_object_properties(attributes), ) LOGGER.debug("Synced User", user=attributes.get('name', ''), created=created) def sync_membership(self): """Iterate over all Users and assign Groups using memberOf Field""" - pass + users = self._connection.extend.standard.paged_search( + search_base=self.base_dn_users, + search_filter=self._source.user_object_filter, + search_scope=ldap3.SUBTREE, + attributes=[ + self._source.user_group_membership_field, + self._source.object_uniqueness_field]) + group_cache: Dict[str, Group] = {} + for user in users: + member_of = user.get('attributes', {}).get(self._source.user_group_membership_field, []) + uniq = user.get('attributes', {}).get(self._source.object_uniqueness_field, []) + for group_dn in member_of: + # Check if group_dn is within our base_dn_groups, and skip if not + if not group_dn.endswith(self.base_dn_groups): + continue + # Check if we fetched the group already, and if not cache it for later + if group_dn not in group_cache: + groups = Group.objects.filter(attributes__distinguishedName=group_dn) + if not groups.exists(): + LOGGER.warning("Group does not exist in our DB yet, run sync_groups first.", + group=group_dn) + return + group_cache[group_dn] = groups.first() + group = group_cache[group_dn] + users = User.objects.filter(attributes__ldap_uniq=uniq) + group.user_set.add(*list(users)) + # Now that all users are added, lets write everything + for _, group in group_cache.items(): + group.save() + LOGGER.debug("Successfully updated group membership") def _build_object_properties(self, attributes: Dict[str, Any]) -> Dict[str, Dict[Any, Any]]: properties = { @@ -92,8 +127,9 @@ class Connector: } for mapping in self._source.property_mappings.all().select_subclasses(): properties[mapping.object_field] = attributes.get(mapping.ldap_property, '') - if 'objectSid' in attributes: - properties['attributes']['objectSid'] = attributes.get('objectSid') + if self._source.object_uniqueness_field in attributes: + properties['attributes']['ldap_uniq'] = \ + attributes.get(self._source.object_uniqueness_field) properties['attributes']['distinguishedName'] = attributes.get('distinguishedName') return properties diff --git a/passbook/sources/ldap/forms.py b/passbook/sources/ldap/forms.py index 9b3d7bd8e..180985e9c 100644 --- a/passbook/sources/ldap/forms.py +++ b/passbook/sources/ldap/forms.py @@ -24,6 +24,8 @@ class LDAPSourceForm(forms.ModelForm): 'additional_group_dn', 'user_object_filter', 'group_object_filter', + 'user_group_membership_field', + 'object_uniqueness_field', 'sync_groups', 'sync_parent_group', 'property_mappings', @@ -32,12 +34,14 @@ class LDAPSourceForm(forms.ModelForm): 'name': forms.TextInput(), 'server_uri': forms.TextInput(), 'bind_cn': forms.TextInput(), - 'bind_password': forms.PasswordInput(), + 'bind_password': forms.TextInput(), 'base_dn': forms.TextInput(), 'additional_user_dn': forms.TextInput(), 'additional_group_dn': forms.TextInput(), 'user_object_filter': forms.TextInput(), 'group_object_filter': forms.TextInput(), + 'user_group_membership_field': forms.TextInput(), + 'object_uniqueness_field': forms.TextInput(), 'policies': FilteredSelectMultiple(_('policies'), False), 'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False) } diff --git a/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py b/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py new file mode 100644 index 000000000..dce40c4dc --- /dev/null +++ b/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-10-11 10:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_sources_ldap', '0004_auto_20191011_0839'), + ] + + operations = [ + migrations.AddField( + model_name='ldapsource', + name='object_uniqueness_field', + field=models.TextField(default='objectSid', help_text='Field which contains a unique Identifier.'), + ), + migrations.AddField( + model_name='ldapsource', + name='user_group_membership_field', + field=models.TextField(default='memberOf', help_text='Field which contains Groups of user.'), + ), + migrations.AlterField( + model_name='ldapsource', + name='group_object_filter', + field=models.TextField(default='(objectCategory=Group)', help_text='Consider Objects matching this filter to be Groups.'), + ), + migrations.AlterField( + model_name='ldapsource', + name='user_object_filter', + field=models.TextField(default='(objectCategory=Person)', help_text='Consider Objects matching this filter to be Users.'), + ), + ] diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index a908cc3dd..93a83bbe0 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -19,8 +19,14 @@ class LDAPSource(Source): additional_user_dn = models.TextField(help_text=_('Prepended to Base DN for User-queries.')) additional_group_dn = models.TextField(help_text=_('Prepended to Base DN for Group-queries.')) - user_object_filter = models.TextField() - group_object_filter = models.TextField() + user_object_filter = models.TextField(default="(objectCategory=Person)", help_text=_( + 'Consider Objects matching this filter to be Users.')) + user_group_membership_field = models.TextField(default="memberOf", help_text=_( + "Field which contains Groups of user.")) + group_object_filter = models.TextField(default="(objectCategory=Group)", help_text=_( + 'Consider Objects matching this filter to be Groups.')) + object_uniqueness_field = models.TextField(default="objectSid", help_text=_( + 'Field which contains a unique Identifier.')) sync_groups = models.BooleanField(default=True) sync_parent_group = models.ForeignKey(Group, blank=True, null=True,