Skip to content
Snippets Groups Projects
Commit ba0d4e11 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '3-sync-additional-ldap-fields-after-login' into 'master'

Resolve "Sync additional LDAP fields after login"

Closes #3

See merge request !2
parents 02cc4ce5 fb5f4a9a
No related branches found
No related tags found
1 merge request!2Resolve "Sync additional LDAP fields after login"
......@@ -17,6 +17,7 @@ Licence
::
Copyright © 2020 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2020 Dominik George <dominik.george@teckids.org>
Licenced under the EUPL, version 1.2 or later
......
......@@ -3,7 +3,7 @@ from django.db.models.signals import post_save
from aleksis.core.util.apps import AppConfig
from .util.ldap_sync import ldap_sync_from_user
from .util.ldap_sync import ldap_sync_from_user, update_constance_config_fields
class LDAPConfig(AppConfig):
name = "aleksis.apps.ldap"
......@@ -11,5 +11,8 @@ class LDAPConfig(AppConfig):
def ready(self) -> None:
super().ready()
update_constance_config_fields()
User = get_user_model()
post_save.connect(ldap_sync_from_user, sender=User)
from django.apps import apps
from django.utils.translation import gettext_lazy as _
CONSTANCE_ADDITIONAL_FIELDS = {
......@@ -24,7 +25,6 @@ CONSTANCE_CONFIG = {
"matching-fields-select",
),
"ENABLE_LDAP_GROUP_SYNC": (True, _("Enable ldap group sync"), bool),
"LDAP_SYNC_CREATE_GROUPS": (True, _("Create non-existing groups"), bool),
"LDAP_GROUP_SYNC_FIELD_SHORT_NAME": ("cn", _("Field for short name of group"), str),
"LDAP_GROUP_SYNC_FIELD_NAME": ("cn", _("Field for name of group"), str),
}
......@@ -34,7 +34,6 @@ CONSTANCE_CONFIG_FIELDSETS = {
"LDAP_SYNC_ON_UPDATE",
"LDAP_MATCHING_FIELDS",
"ENABLE_LDAP_GROUP_SYNC",
"LDAP_SYNC_CREATE_GROUPS",
"LDAP_GROUP_SYNC_FIELD_SHORT_NAME",
"LDAP_GROUP_SYNC_FIELD_NAME",
),
......
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import fields
from constance import config
def setting_name_from_field(model, field):
""" Generate a constance setting name from a model field """
return "LDAP_ADDITIONAL_FIELD_%s_%s" % (model._meta.label, field.name)
def syncable_fields(model):
""" Collect all fields that can be synced on a model """
return [field for field in model._meta.fields if field.editable and not field.auto_created]
def from_ldap(value, field):
""" Convert an LDAP value to the Python type of the target field
This conversion is prone to error because LDAP deliberately breaks
standards to cope with ASN.1 limitations.
"""
from ldapdb.models.fields import datetime_from_ldap # noqa
# Pre-convert DateTimeField and DateField due to ISO 8601 limitations in RFC 4517
if type(field) in (fields.DateField, fields.DateTimeField):
# Be opportunistic, but keep old value if conversion fails
value = datetime_from_ldap(value) or value
# Finally, use field's conversion method as default
return field.to_python(value)
def update_constance_config_fields():
""" Auto-generate sync field settings from models """
Person = apps.get_model("core", "Person")
for model in (Person,):
# Collect fields that are matchable
setting_names = []
for field in syncable_fields(model):
setting_name = setting_name_from_field(model, field)
setting_desc = field.verbose_name
settings.CONSTANCE_CONFIG[setting_name] = ("", setting_desc, str)
setting_names.append(setting_name)
# Add separate constance section if settings were generated
if setting_names:
fieldset_name = "LDAP-Sync: Additional fields for %s" % model._meta.verbose_name
settings.CONSTANCE_CONFIG_FIELDSETS[fieldset_name] = setting_names
def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, **kwargs):
""" Synchronise Person meta-data and groups from ldap_user on User update. """
# Semaphore to guard recursive saves within this signal
if getattr(instance, "_skip_signal", False):
return
instance._skip_signal = True
Person = apps.get_model("core", "Person")
Group = apps.get_model("core", "Group")
if config.ENABLE_LDAP_SYNC and (created or config.LDAP_SYNC_ON_UPDATE) and hasattr(instance, "ldap_user"):
# Check if there is an existing person connected to the user.
if not Person.objects.filter(user=instance).exists():
if Person.objects.filter(user=instance).exists():
person = instance.person
else:
# Build filter criteria depending on config
matches = {}
if "-email" in config.LDAP_MATCHING_FIELDS:
......@@ -28,7 +87,16 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, **
return
person.user = instance
person.save()
# Synchronise additional fields if enabled
for field in syncable_fields(Person):
setting_name = setting_name_from_field(Person, field)
# Try sync if constance setting for this field is non-empty
ldap_field = getattr(config, setting_name, "").lower()
if ldap_field and ldap_field in instance.ldap_user.attrs.data:
setattr(person, field.name,
from_ldap(instance.ldap_user.attrs.data[ldap_field][0], field))
if config.ENABLE_LDAP_GROUP_SYNC:
# Resolve Group objects from LDAP group objects
......@@ -47,4 +115,14 @@ def ldap_sync_from_user(sender, instance, created, raw, using, update_fields, **
group_objects.append(group)
# Replace linked groups of logged-in user completely
instance.person.member_of.set(group_objects)
person.member_of.set(group_objects)
try:
person.save()
except Exception:
# Exceptions here are silenced because the synchronisation is optional
# FIXME throw warning to user instead
pass
# Remove semaphore
del instance._skip_signal
......@@ -20,6 +20,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.7"
django-ldapdb = "^1.4.0"
AlekSIS = { path = "../../.." }
[build-system]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment