Skip to content
Snippets Groups Projects

Resolve "Sync additional LDAP fields after login"

Merged Tom Teichler requested to merge 3-sync-additional-ldap-fields-after-login into master
1 unresolved thread
Files
5
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
Loading