From 5570f172e6696b77bd13bbd1cff342d7356f1f36 Mon Sep 17 00:00:00 2001
From: Dominik George <dominik.george@teckids.org>
Date: Sun, 29 Mar 2020 23:42:00 +0100
Subject: [PATCH] Refactor exception handling out of library code and use LDAP
 DN as import ref

---
 aleksis/apps/ldap/model_extensions.py |  9 +++
 aleksis/apps/ldap/util/ldap_sync.py   | 84 ++++++++++++++++++---------
 2 files changed, 64 insertions(+), 29 deletions(-)
 create mode 100644 aleksis/apps/ldap/model_extensions.py

diff --git a/aleksis/apps/ldap/model_extensions.py b/aleksis/apps/ldap/model_extensions.py
new file mode 100644
index 0000000..15dfd02
--- /dev/null
+++ b/aleksis/apps/ldap/model_extensions.py
@@ -0,0 +1,9 @@
+from django.utils.translation import gettext_lazy as _
+
+from jsonstore import CharField
+
+from aleksis.core.models import Group, Person
+
+# Fields as import refs for LDAP objects
+Group.field(ldap_dn=CharField(verbose_name=_("LDAP Distinguished Name")))
+Person.field(ldap_dn=CharField(verbose_name=_("LDAP Distinguished Name")))
diff --git a/aleksis/apps/ldap/util/ldap_sync.py b/aleksis/apps/ldap/util/ldap_sync.py
index 2cf6263..be6cfba 100644
--- a/aleksis/apps/ldap/util/ldap_sync.py
+++ b/aleksis/apps/ldap/util/ldap_sync.py
@@ -4,7 +4,7 @@ import re
 from django.apps import apps
 from django.conf import settings
 from django.contrib.auth import get_user_model
-from django.db import IntegrityError, transaction
+from django.db import DataError, IntegrityError, transaction
 from django.db.models import fields
 from django.utils.translation import gettext as _
 
@@ -135,7 +135,7 @@ def ldap_sync_user_on_login(sender, instance, created, **kwargs):
 
 
 @transaction.atomic
-def ldap_sync_from_user(user, attrs):
+def ldap_sync_from_user(user, dn, attrs):
     """ Synchronise person information from a User object (with ldap_user) to Django """
 
     Person = apps.get_model("core", "Person")
@@ -143,7 +143,9 @@ def ldap_sync_from_user(user, attrs):
     # Check if there is an existing person connected to the user.
     if Person.objects.filter(user=user).exists():
         person = user.person
+        created = False
         logger.info("Existing person %s already linked to user %s" % (str(person), user.username))
+    # FIXME ALso account for existing person with DN here
     else:
         # Build filter criteria depending on config
         matches = {}
@@ -157,28 +159,20 @@ def ldap_sync_from_user(user, attrs):
             matches["last_name"] = user.last_name
             defaults["email"] = user.email
 
-        try:
-            with transaction.atomic():
-                if config.LDAP_SYNC_CREATE_MISSING_PERSONS:
-                    person, created = Person.objects.get_or_create(**matches, defaults=defaults)
-                else:
-                    person = Person.objects.get(**matches)
-                    created = False
-        except Person.DoesNotExist:
-            # Bail out of further processing
-            logger.warn("No matching person for user %s" % user.username)
-            return
-        except Person.MultipleObjectsReturned:
-            # Bail out of further processing
-            logger.error("More than one matching person for user %s" % user.username)
-            return
+        if config.LDAP_SYNC_CREATE_MISSING_PERSONS:
+            person, created = Person.objects.get_or_create(**matches, defaults=defaults)
         else:
-            person.user = user
-            if not created:
-                person.first_name = user.first_name
-                person.last_name = user.last_name
-                person.email = user.email
-            logger.info("Matching person %s linked to user %s" % (str(person), user.username))
+            person = Person.objects.get(**matches)
+            created = False
+
+        person.user = user
+        logger.info("%s person %s linked to user %s" % ("New" if created else "Existing", str(person), user.username))
+
+    person.ldap_dn = dn
+    if not created:
+        person.first_name = user.first_name
+        person.last_name = user.last_name
+        person.email = user.email
 
     # Synchronise additional fields if enabled
     for field in syncable_fields(Person):
@@ -200,6 +194,7 @@ def ldap_sync_from_user(user, attrs):
             setattr(person, field.name, value)
             logger.debug("Field %s set to %s for %s" % (field.name, str(value), str(person)))
 
+    person.save()
     return person
 
 
@@ -213,6 +208,7 @@ def ldap_sync_from_groups(group_infos):
     group_objects = []
     for ldap_group in tqdm(group_infos):
         # Skip group if one of the name fields is missing
+        # FIXME Throw exceptions and catch outside
         if config.LDAP_GROUP_SYNC_FIELD_SHORT_NAME not in ldap_group[1]:
             logger.error("LDAP group with DN %s does not have field %s" % (
                 ldap_group[0],
@@ -242,17 +238,18 @@ def ldap_sync_from_groups(group_infos):
         short_name = short_name[:Group._meta.get_field("short_name").max_length]
         name = name[:Group._meta.get_field("name").max_length]
 
+        # FIXME FInd a way to throw exceptions correctly but still continue import
         try:
             with transaction.atomic():
                 group, created = Group.objects.update_or_create(
-                    import_ref = ldap_group[0],
+                    ldap_dn = ldap_group[0],
                     defaults = {
                         "short_name": short_name,
                         "name": name
                     }
                 )
-        except IntegrityError:
-            logger.error("Integrity error while trying to import LDAP group %s" % ldap_group[0])
+        except IntegrityError as e:
+            logger.error("Integrity error while trying to import LDAP group %s:\n%s" % (ldap_group[0], str(e)))
             continue
         else:
             logger.info("%s LDAP group %s for Django group %s" % (
@@ -272,6 +269,8 @@ def mass_ldap_import():
 
     from django_auth_ldap.backend import LDAPBackend, _LDAPUser  # noqa
 
+    Person = apps.get_model("core", "Person")
+
     # Abuse pre-configured search object as general LDAP interface
     backend = LDAPBackend()
     connection = _LDAPUser(backend, "").connection
@@ -296,7 +295,34 @@ def mass_ldap_import():
         user, created = backend.get_or_build_user(uid, ldap_user)
 
         if created or config.LDAP_SYNC_ON_UPDATE:
-            logger.info("Will %s user %s in Django" % ("create" if created else "update", uid))
-            person = ldap_sync_from_user(user, attrs)
+            try:
+                with transaction.atomic():
+                    person = ldap_sync_from_user(user, dn, attrs)
+            except Person.DoesNotExist:
+                logger.warn("No matching person for user %s" % user.username)
+                continue
+            except Person.MultipleObjectsReturned:
+                logger.error("More than one matching person for user %s" % user.username)
+                continue
+            except (DataError, IntegrityError, ValueError) as e:
+                logger.error("Data error while synchronising user %s:\n%s" % (user.username, str(e)))
+                continue
+            else:
+                logger.info("Successfully imported user %s" % uid)
+
+    # Synchronise group memberships now
+    if config.ENABLE_LDAP_GROUP_SYNC:
+        member_attr = getattr(backend.settings.GROUP_TYPE, "member_attr", "memberUid")
+
+        for group, ldap_group in tqdm(zip(group_objects, ldap_groups)):
+            dn, attrs = ldap_group
+            ldap_members = attrs[member_attr]
+
+            if member_attr.lower() == "memberUid":
+                members = Person.objects.filter(user__username__in=ldap_members)
+            else:
+                members = Person.objects.filter(ldap_dn__in=ldap_members)
 
-        logger.info("Successfully imported user %s" % uid)
+            group.members.set(members)
+            group.save()
+            logger.info("Set group members of group %s" % str(group))
-- 
GitLab