From 70f2c6ddf3fcb544152c7a0f3f4c166e73246476 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Fri, 13 Oct 2017 13:59:31 +0200 Subject: [PATCH] Manage LDAP extra attributes (#19365) This extra attributes are retreived by making other LDAP queries with parameters composed by looping on an user object's attribute values. A mapping will be apply on corresponding objects's attributes and resulting informations are compiled in a list and serialize in JSON (configurable, but JSON is the only format available for now). --- src/authentic2/backends/ldap_backend.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index 80f1baa..ff24c7a 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -13,6 +13,7 @@ import base64 import urllib import six import os +import json # code originaly copied from by now merely inspired by # http://www.amherst.k12.oh.us/django-ldap.html @@ -250,6 +251,8 @@ class LDAPBackend(object): 'mandatory_attributes_values': {}, # mapping from LDAP attributes name to other names 'attribute_mappings': [], + # extra attributes retrieve by making other LDAP search using user object informations + 'extra_attributes': {}, # realm for selecting an ldap configuration or formatting usernames 'realm': 'ldap', # template for building username @@ -663,6 +666,13 @@ class LDAPBackend(object): external_id_tuple)) for from_at, to_at in block['attribute_mappings']: attributes.add(to_at) + for extra_at in block.get('extra_attributes', {}): + if 'loop_over_attribute' in block['extra_attributes'][extra_at]: + attributes.add(block['extra_attributes'][extra_at]['loop_over_attribute']) + at_mapping = block['extra_attributes'][extra_at].get('mapping',{}) + for key in at_mapping: + if at_mapping[key] != 'dn': + attributes.add(at_mapping[key]) return list(set(map(str.lower, map(str, attributes)))) @classmethod @@ -693,7 +703,54 @@ class LDAPBackend(object): old = attribute_map.setdefault(to_attribute, []) new = set(old) | set(attribute_map[from_attribute]) attribute_map[to_attribute] = list(new) + attribute_map['dn'] = dn + + # extra attributes + ldap_scopes = { + 'base': ldap.SCOPE_BASE, + 'one': ldap.SCOPE_ONELEVEL, + 'sub': ldap.SCOPE_SUBTREE, + } + for extra_attribute_name in block.get('extra_attributes', {}): + extra_attribute_config = block['extra_attributes'][extra_attribute_name] + extra_attribute_values = [] + if 'loop_over_attribute' in extra_attribute_config: + extra_attribute_config['loop_over_attribute']=str.lower(extra_attribute_config['loop_over_attribute']) + if extra_attribute_config['loop_over_attribute'] not in attribute_map: + raise ImproperlyConfigured('loop_over_attribute %s not present in user object attributes retreived' % extra_attribute_config['loop_over_attribute']) + if 'filter' not in extra_attribute_config and 'basedn' not in extra_attribute_config: + raise ImproperlyConfigured('Extra attribute %s not correctly configured : you need to defined at least one of filter or basedn parameters' % extra_attribute_name) + for item in attribute_map[extra_attribute_config['loop_over_attribute']]: + ldap_filter = unicode(extra_attribute_config.get('filter','objectClass=*')).format(item=item, **attribute_map) + ldap_basedn = unicode(extra_attribute_config.get('basedn',block.get('basedn'))).format(item=item, **attribute_map) + ldap_scope = ldap_scopes.get(extra_attribute_config.get('scope','sub'), ldap.SCOPE_SUBTREE) + ldap_attributes_mapping = extra_attribute_config.get('mapping', {}) + ldap_attributes_names = filter(lambda a: a != 'dn', ldap_attributes_mapping.values()) + try: + results = conn.search_s(ldap_basedn, ldap_scope, ldap_filter, ldap_attributes_names) + except ldap.LDAPError: + log.exception('unable to retrieve extra attribute %s for item %s' % (extra_attribute_name, item)) + continue + item_value={} + for obj in results: + obj_attributes=cls.normalize_ldap_results(obj[1]) + obj_attributes[dn]=obj[0] + for key in ldap_attributes_mapping: + item_value[key]=obj_attributes.get(ldap_attributes_mapping[key]) + if not item_value[key]: + del item_value[key] + elif len(item_value[key])==1: + item_value[key]=item_value[key][0] + extra_attribute_values.append(item_value) + else: + raise ImproperlyConfigured('loop_over_attribute not defined for extra attribute %s' % extra_attribute_name) + extra_attribute_serialization = extra_attribute_config.get('serialization','json') + if extra_attribute_serialization == 'json': + attribute_map[extra_attribute_name] = json.dumps(extra_attribute_values) + else: + raise ImproperlyConfigured('Invalid serialization type "%s" for extra attribute %s' % (extra_attribute_serialization, extra_attribute_name)) + return attribute_map @classmethod @@ -830,6 +887,7 @@ class LDAPBackend(object): for block in cls.get_config(): names.update(cls.get_ldap_attributes_names(block)) names.update(block['mandatory_attributes_values'].keys()) + names.update(block.get('extra_attributes',{}).keys()) return [(a, '%s (LDAP)' % a) for a in sorted(names)] @classmethod -- 2.1.4