From 42232a5c412eac0b564704cb83207cf954c314e7 Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Sun, 29 Mar 2020 13:16:44 +0200 Subject: [PATCH] Implement LDAP mass import with Celery job and management command Also introduces some loggin for LDAP synchronisation --- .../apps/ldap/management/commands/__init__.py | 0 .../management/commands/mass_ldap_import.py | 8 +++ aleksis/apps/ldap/tasks.py | 7 +++ aleksis/apps/ldap/util/ldap_sync.py | 54 +++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 aleksis/apps/ldap/management/commands/__init__.py create mode 100644 aleksis/apps/ldap/management/commands/mass_ldap_import.py create mode 100644 aleksis/apps/ldap/tasks.py diff --git a/aleksis/apps/ldap/management/commands/__init__.py b/aleksis/apps/ldap/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aleksis/apps/ldap/management/commands/mass_ldap_import.py b/aleksis/apps/ldap/management/commands/mass_ldap_import.py new file mode 100644 index 0000000..1547358 --- /dev/null +++ b/aleksis/apps/ldap/management/commands/mass_ldap_import.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand + +from ...tasks import ldap_import + + +class Command(BaseCommand): + def handle(self, *args, **options): + ldap_import() diff --git a/aleksis/apps/ldap/tasks.py b/aleksis/apps/ldap/tasks.py new file mode 100644 index 0000000..840c19e --- /dev/null +++ b/aleksis/apps/ldap/tasks.py @@ -0,0 +1,7 @@ +from aleksis.core.util.core_helpers import celery_optional + +from .util.ldap_sync import mass_ldap_import + +@celery_optional +def ldap_import(): + mass_ldap_import() diff --git a/aleksis/apps/ldap/util/ldap_sync.py b/aleksis/apps/ldap/util/ldap_sync.py index 6d484db..645927b 100644 --- a/aleksis/apps/ldap/util/ldap_sync.py +++ b/aleksis/apps/ldap/util/ldap_sync.py @@ -1,3 +1,4 @@ +import logging import re from django.apps import apps @@ -9,6 +10,9 @@ from django.utils.translation import gettext as _ from constance import config +logger = logging.getLogger(__name__) + + def setting_name_from_field(model, field): """ Generate a constance setting name from a model field """ @@ -82,7 +86,7 @@ def apply_templates(value, patterns, templates, separator="|"): return value -def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, **kwargs): +def ldap_sync_from_user(sender, instance, created, **kwargs): """ Synchronise Person meta-data and groups from ldap_user on User update. """ # Semaphore to guard recursive saves within this signal @@ -97,6 +101,7 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, ** # Check if there is an existing person connected to the user. if Person.objects.filter(user=instance).exists(): person = instance.person + logger.info("Existing person %s already linked to user %s" % (str(person), instance.username)) else: # Build filter criteria depending on config matches = {} @@ -110,9 +115,11 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, ** person = Person.objects.get(**matches) except Person.DoesNotExist: # Bail out of further processing + logger.info("No matching person for user %s" % instance.username) return person.user = instance + logger.info("Matching person %s linked to user %s" % (str(person), instance.username)) # Synchronise additional fields if enabled for field in syncable_fields(Person): @@ -132,6 +139,7 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, ** value = from_ldap(value, field) setattr(person, field.name, value) + logger.debug("Field %s set to %s for %s" % (field.name, str(value), str(person))) if config.ENABLE_LDAP_GROUP_SYNC: # Resolve Group objects from LDAP group objects @@ -162,18 +170,56 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, ** "name": name } ) + logger.info("%s LDAP group %s for Django group %s" % ( + ("Created" if created else "Updated"), + ldap_group[1][config.LDAP_GROUP_SYNC_FIELD_NAME][0], + name + )) group_objects.append(group) # Replace linked groups of logged-in user completely person.member_of.set(group_objects) + logger.info("Replaced group memberships of %s" % str(person)) try: person.save() - except Exception: - # Exceptions here are silenced because the synchronisation is optional + except Exception as e: + # Exceptions here are logged only because the synchronisation is optional # FIXME throw warning to user instead - pass + logger.error("Could not save person %s:\n%s" % (str(person), str(e))) # Remove semaphore del instance._skip_signal + + +def mass_ldap_import(): + """ Utility code for mass import from ldap """ + + from django_auth_ldap.backend import LDAPBackend, _LDAPUser # noqa + + # Abuse pre-configured search object to find all users by letting * pass + backend = LDAPBackend() + res = backend.settings.USER_SEARCH.execute(_LDAPUser(backend, "").connection, {"user": "*"}, escape=False) + + # Guess LDAP username field from user filter + uid_field = re.search(r"([a-zA-Z]+)=%\(user\)s", backend.settings.USER_SEARCH.filterstr).group(1) + + uids = [entry[1][uid_field][0] for entry in res] + + for uid in uids: + # Prepare an empty LDAPUser object with the target username + ldap_user = _LDAPUser(backend, username=uid) + + # Find out whether the User object would be created, but do not save + _, created = backend.get_or_build_user(uid, backend.ldap_to_django_username(ldap_user)) + logger.info("Will %s user %s in Django" % ("create" if created else "update", uid)) + + # Run creation and/or population, like the auth backend would + ldap_user._get_or_create_user() + user = ldap_user._user + + # Trigger sync run like on login signal + ldap_sync_from_user(user.__class__, user, created) + + logger.info("Successfully imported user %s" % uid) -- GitLab