Projet

Général

Profil

0001-Manage-LDAP-extra-attributes-19365.patch

Benjamin Dauvergne, 17 mai 2018 15:54

Télécharger (11,4 ko)

Voir les différences:

Subject: [PATCH 1/2] 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 | 72 +++++++++++++++++++
 tests/test_ldap.py                      | 91 ++++++++++++++++++++++++-
 2 files changed, 162 insertions(+), 1 deletion(-)
src/authentic2/backends/ldap_backend.py
14 14
import urllib
15 15
import six
16 16
import os
17
import json
17 18

  
18 19
# code originaly copied from by now merely inspired by
19 20
# http://www.amherst.k12.oh.us/django-ldap.html
......
256 257
        'mandatory_attributes_values': {},
257 258
        # mapping from LDAP attributes name to other names
258 259
        'attribute_mappings': [],
260
        # extra attributes retrieve by making other LDAP search using user object informations
261
        'extra_attributes': {},
259 262
        # realm for selecting an ldap configuration or formatting usernames
260 263
        'realm': 'ldap',
261 264
        # template for building username
......
733 736
                external_id_tuple))
734 737
        for from_at, to_at in block['attribute_mappings']:
735 738
            attributes.add(to_at)
739
        for extra_at in block.get('extra_attributes', {}):
740
            if 'loop_over_attribute' in block['extra_attributes'][extra_at]:
741
                attributes.add(block['extra_attributes'][extra_at]['loop_over_attribute'])
742
            at_mapping = block['extra_attributes'][extra_at].get('mapping', {})
743
            for key in at_mapping:
744
                if at_mapping[key] != 'dn':
745
                    attributes.add(at_mapping[key])
736 746
        return list(set(map(str.lower, map(str, attributes))))
737 747

  
738 748
    @classmethod
......
740 750
        '''Retrieve some attributes from LDAP, add mandatory values then apply
741 751
           defined mappings between atrribute names'''
742 752
        attributes = cls.get_ldap_attributes_names(block)
753
        log.debug(u'Attrs names : %s' % attributes)
743 754
        attribute_mappings = block['attribute_mappings']
744 755
        mandatory_attributes_values = block['mandatory_attributes_values']
745 756
        try:
......
764 775
            new = set(old) | set(attribute_map[from_attribute])
765 776
            attribute_map[to_attribute] = list(new)
766 777
        attribute_map['dn'] = force_text(dn)
778

  
779
        # extra attributes
780
        attribute_map = cls.get_ldap_extra_attributes(block, conn, dn, attribute_map)
781

  
782
        return attribute_map
783

  
784
    @classmethod
785
    def get_ldap_extra_attributes(cls, block, conn, dn, attribute_map):
786
        '''Retrieve extra attributes from LDAP'''
787

  
788
        ldap_scopes = {
789
            'base': ldap.SCOPE_BASE,
790
            'one': ldap.SCOPE_ONELEVEL,
791
            'sub': ldap.SCOPE_SUBTREE,
792
        }
793
        log.debug(u'Attrs before extra attributes : %s' % attribute_map)
794
        for extra_attribute_name in block.get('extra_attributes', {}):
795
            extra_attribute_config = block['extra_attributes'][extra_attribute_name]
796
            extra_attribute_values = []
797
            if 'loop_over_attribute' in extra_attribute_config:
798
                extra_attribute_config['loop_over_attribute'] = extra_attribute_config['loop_over_attribute'].lower()
799
                if extra_attribute_config['loop_over_attribute'] not in attribute_map:
800
                    log.debug('loop_over_attribute %s not present (or empty) in user object attributes retreived. Pass.' % extra_attribute_config['loop_over_attribute'])
801
                    continue
802
                if 'filter' not in extra_attribute_config and 'basedn' not in extra_attribute_config:
803
                    log.warning('Extra attribute %s not correctly configured : you need to defined at least one of filter or basedn parameters' % extra_attribute_name)
804
                for item in attribute_map[extra_attribute_config['loop_over_attribute']]:
805
                    ldap_filter = unicode(extra_attribute_config.get('filter', 'objectClass=*')).format(item=item, **attribute_map)
806
                    ldap_basedn = unicode(extra_attribute_config.get('basedn', block.get('basedn'))).format(item=item, **attribute_map)
807
                    ldap_scope = ldap_scopes.get(extra_attribute_config.get('scope', 'sub'), ldap.SCOPE_SUBTREE)
808
                    ldap_attributes_mapping = extra_attribute_config.get('mapping', {})
809
                    ldap_attributes_names = filter(lambda a: a != 'dn', ldap_attributes_mapping.values())
810
                    try:
811
                        results = conn.search_s(ldap_basedn, ldap_scope, ldap_filter, ldap_attributes_names)
812
                    except ldap.LDAPError:
813
                        log.exception('unable to retrieve extra attribute %s for item %s' % (extra_attribute_name, item))
814
                        continue
815
                    item_value = {}
816
                    for obj in results:
817
                        log.debug(u'Object retrieved for extra attr %s with item %s : %s' % (extra_attribute_name, item, obj))
818
                        obj_attributes = cls.normalize_ldap_results(obj[1])
819
                        obj_attributes[dn] = obj[0]
820
                        log.debug(u'Object attributes normalized for extra attr %s with item %s : %s' % (extra_attribute_name, item, obj_attributes))
821
                        for key in ldap_attributes_mapping:
822
                            item_value[key] = obj_attributes.get(ldap_attributes_mapping[key].lower())
823
                            log.debug(u'Object attribute %s value retrieved for extra attr %s with item %s : %s' % (ldap_attributes_mapping[key], extra_attribute_name, item, item_value[key]))
824
                            if not item_value[key]:
825
                                del item_value[key]
826
                            elif len(item_value[key]) == 1:
827
                                item_value[key] = item_value[key][0]
828
                    extra_attribute_values.append(item_value)
829
            else:
830
                log.warning('loop_over_attribute not defined for extra attribute %s' % extra_attribute_name)
831
            extra_attribute_serialization = extra_attribute_config.get('serialization', None)
832
            if extra_attribute_serialization is None:
833
                attribute_map[extra_attribute_name] = extra_attribute_values
834
            elif extra_attribute_serialization == 'json':
835
                attribute_map[extra_attribute_name] = json.dumps(extra_attribute_values)
836
            else:
837
                log.warning('Invalid serialization type "%s" for extra attribute %s' % (extra_attribute_serialization, extra_attribute_name))
767 838
        return attribute_map
768 839

  
769 840
    @classmethod
......
906 977
        for block in cls.get_config():
907 978
            names.update(cls.get_ldap_attributes_names(block))
908 979
            names.update(block['mandatory_attributes_values'].keys())
980
            names.update(block.get('extra_attributes', {}).keys())
909 981
        return [(a, '%s (LDAP)' % a) for a in sorted(names)]
910 982

  
911 983
    @classmethod
tests/test_ldap.py
17 17
from authentic2 import crypto
18 18

  
19 19
import utils
20
import json
20 21

  
21 22
pytestmark = pytest.mark.skipunless(has_slapd(), reason='slapd is not installed')
22 23

  
......
26 27
DN = 'cn=%s,o=orga' % escape_dn_chars(CN)
27 28
PASS = 'passé'
28 29
EMAIL = 'etienne.michu@example.net'
30
CARLICENSE = '123445ABC'
31

  
32
EO_O = "EO"
33
EO_STREET = "169 rue du Chateau"
34
EO_POSTALCODE = "75014"
35
EO_CITY = "PARIS"
36

  
37
EE_O = "EE"
38
EE_STREET = "44 rue de l'Ouest"
39
EE_POSTALCODE = "75014"
40
EE_CITY = "PARIS"
29 41

  
30 42

  
31 43
@pytest.fixture
......
39 51
sn: Michu
40 52
gn: Étienne
41 53
mail: etienne.michu@example.net
54
carLicense: {cl}
55
o: EO
56
o: EE
42 57

  
43 58
dn: cn=group1,o=orga
44 59
objectClass: groupOfNames
45 60
member: {dn}
46 61

  
47
'''.format(dn=DN, uid=UID, password=PASS))
62
dn: o={eo_o},o=orga
63
objectClass: organization
64
o: {eo_o}
65
postalAddress: {eo_street}
66
postalCode: {eo_postalcode}
67
l: {eo_city}
68

  
69
dn: o={ee_o},o=orga
70
objectClass: organization
71
o: {ee_o}
72
postalAddress: {ee_street}
73
postalCode: {ee_postalcode}
74
l: {ee_city}
75

  
76
'''.format(dn=DN, uid=UID, password=PASS, cl=CARLICENSE,
77
           eo_o=EO_O, eo_street=EO_STREET, eo_postalcode=EO_POSTALCODE, eo_city=EO_CITY,
78
           ee_o=EE_O, ee_street=EE_STREET, ee_postalcode=EE_POSTALCODE, ee_city=EE_CITY
79
          )
80
    )
48 81
    for i in range(100):
49 82
        slapd.add_ldif('''dn: uid=michu{i},o=orga
50 83
objectClass: inetOrgPerson
......
350 383
    assert not response.context['user'].is_superuser
351 384

  
352 385

  
386
@pytest.mark.django_db
387
def test_get_attributes(slapd, settings, client):
388
    settings.LDAP_AUTH_SETTINGS = [{
389
        'url': [slapd.ldap_url],
390
        'basedn': 'o=orga',
391
        'use_tls': False,
392
        'groupstaff': ['cn=group1,o=orga'],
393
        'attributes': ['uid', 'carLicense'],
394
    }]
395
    response = client.post('/login/', {'login-password-submit': '1',
396
                                       'username': 'etienne.michu',
397
                                       'password': PASS}, follow=True)
398
    user = response.context['user']
399
    fetched_attrs = user.get_attributes()
400
    assert UID in fetched_attrs.get('uid')
401
    assert CARLICENSE in fetched_attrs.get('carlicense')
402

  
403

  
404
@pytest.mark.django_db
405
def test_get_extra_attributes(slapd, settings, client):
406
    settings.LDAP_AUTH_SETTINGS = [{
407
        'url': [slapd.ldap_url],
408
        'basedn': 'o=orga',
409
        'use_tls': False,
410
        'groupstaff': ['cn=group1,o=orga'],
411
        'attributes': ['uid'],
412
        'extra_attributes': {
413
            'orga': {
414
                'loop_over_attribute': 'o',
415
                'filter': '(&(objectclass=organization)(o={item}))',
416
                'basedn': 'o=orga',
417
                'scope': 'sub',
418
                'mapping': {
419
                    'id': 'o',
420
                    'street': 'postalAddress',
421
                    'city': 'l',
422
                    'postal_code': 'postalCode',
423
                },
424
                'serialization': 'json'
425
            }
426
        },
427
    }]
428
    response = client.post('/login/', {'login-password-submit': '1',
429
                                       'username': 'etienne.michu',
430
                                       'password': PASS}, follow=True)
431
    user = response.context['user']
432
    fetched_attrs = user.get_attributes()
433
    assert UID in fetched_attrs.get('uid')
434
    assert 'orga' in fetched_attrs
435
    orgas = json.loads(fetched_attrs.get('orga'))
436
    assert isinstance(orgas, list)
437
    assert len(orgas) == 2
438
    assert {'id': EO_O, 'street': EO_STREET, 'city': EO_CITY, 'postal_code': EO_POSTALCODE} in orgas
439
    assert {'id': EE_O, 'street': EE_STREET, 'city': EE_CITY, 'postal_code': EE_POSTALCODE} in orgas
440

  
441

  
353 442
@pytest.mark.django_db
354 443
def test_get_users(slapd, settings):
355 444
    import django.db.models.base
356
-