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