0001-Manage-LDAP-extra-attributes-19365.patch
src/authentic2/backends/ldap_backend.py | ||
---|---|---|
33 | 33 |
import random |
34 | 34 |
import base64 |
35 | 35 |
import os |
36 |
import json |
|
36 | 37 | |
37 | 38 |
# code originaly copied from by now merely inspired by |
38 | 39 |
# http://www.amherst.k12.oh.us/django-ldap.html |
... | ... | |
449 | 450 |
'mandatory_attributes_values': {}, |
450 | 451 |
# mapping from LDAP attributes name to other names |
451 | 452 |
'attribute_mappings': [], |
453 |
# extra attributes retrieve by making other LDAP search using user object informations |
|
454 |
'extra_attributes': {}, |
|
452 | 455 |
# realm for selecting an ldap configuration or formatting usernames |
453 | 456 |
'realm': 'ldap', |
454 | 457 |
# template for building username |
... | ... | |
961 | 964 |
from_ldap = mapping.get('from_ldap') |
962 | 965 |
if from_ldap: |
963 | 966 |
attributes.add(from_ldap) |
967 |
for extra_at in block.get('extra_attributes', {}): |
|
968 |
if 'loop_over_attribute' in block['extra_attributes'][extra_at]: |
|
969 |
attributes.add(block['extra_attributes'][extra_at]['loop_over_attribute']) |
|
970 |
at_mapping = block['extra_attributes'][extra_at].get('mapping', {}) |
|
971 |
for key in at_mapping: |
|
972 |
if at_mapping[key] != 'dn': |
|
973 |
attributes.add(at_mapping[key]) |
|
964 | 974 |
return list(set(attribute.lower() for attribute in attributes)) |
965 | 975 | |
966 | 976 |
@classmethod |
... | ... | |
992 | 1002 |
new = set(old) | set(attribute_map[from_attribute]) |
993 | 1003 |
attribute_map[to_attribute] = list(new) |
994 | 1004 |
attribute_map['dn'] = force_text(dn) |
1005 | ||
1006 |
# extra attributes |
|
1007 |
attribute_map = cls.get_ldap_extra_attributes(block, conn, dn, attribute_map) |
|
1008 | ||
1009 |
return attribute_map |
|
1010 | ||
1011 |
@classmethod |
|
1012 |
def get_ldap_extra_attributes(cls, block, conn, dn, attribute_map): |
|
1013 |
'''Retrieve extra attributes from LDAP''' |
|
1014 | ||
1015 |
ldap_scopes = { |
|
1016 |
'base': ldap.SCOPE_BASE, |
|
1017 |
'one': ldap.SCOPE_ONELEVEL, |
|
1018 |
'sub': ldap.SCOPE_SUBTREE, |
|
1019 |
} |
|
1020 |
log.debug('Attrs before extra attributes : %s', attribute_map) |
|
1021 |
for extra_attribute_name in block.get('extra_attributes', {}): |
|
1022 |
extra_attribute_config = block['extra_attributes'][extra_attribute_name] |
|
1023 |
extra_attribute_values = [] |
|
1024 |
if 'loop_over_attribute' in extra_attribute_config: |
|
1025 |
extra_attribute_config['loop_over_attribute'] = extra_attribute_config['loop_over_attribute'].lower() |
|
1026 |
if extra_attribute_config['loop_over_attribute'] not in attribute_map: |
|
1027 |
log.debug('loop_over_attribute %s not present (or empty) in user object attributes retreived. Pass.' % extra_attribute_config['loop_over_attribute']) |
|
1028 |
continue |
|
1029 |
if 'filter' not in extra_attribute_config and 'basedn' not in extra_attribute_config: |
|
1030 |
log.warning('Extra attribute %s not correctly configured : you need to defined at least one of filter or basedn parameters' % extra_attribute_name) |
|
1031 |
for item in attribute_map[extra_attribute_config['loop_over_attribute']]: |
|
1032 |
ldap_filter = extra_attribute_config.get('filter', 'objectClass=*').format(item=item, **attribute_map) |
|
1033 |
ldap_basedn = extra_attribute_config.get('basedn', block.get('basedn')).format(item=item, **attribute_map) |
|
1034 |
ldap_scope = ldap_scopes.get(extra_attribute_config.get('scope', 'sub'), ldap.SCOPE_SUBTREE) |
|
1035 |
ldap_attributes_mapping = extra_attribute_config.get('mapping', {}) |
|
1036 |
ldap_attributes_names = list(filter(lambda a: a != 'dn', ldap_attributes_mapping.values())) |
|
1037 |
try: |
|
1038 |
results = conn.search_s(ldap_basedn, ldap_scope, ldap_filter, ldap_attributes_names) |
|
1039 |
except ldap.LDAPError: |
|
1040 |
log.exception('unable to retrieve extra attribute %s for item %s' % (extra_attribute_name, item)) |
|
1041 |
continue |
|
1042 |
item_value = {} |
|
1043 |
for obj in results: |
|
1044 |
log.debug(u'Object retrieved for extra attr %s with item %s : %s' % (extra_attribute_name, item, obj)) |
|
1045 |
obj_attributes = cls.normalize_ldap_results(obj[1]) |
|
1046 |
obj_attributes[dn] = obj[0] |
|
1047 |
log.debug(u'Object attributes normalized for extra attr %s with item %s : %s' % (extra_attribute_name, item, obj_attributes)) |
|
1048 |
for key in ldap_attributes_mapping: |
|
1049 |
item_value[key] = obj_attributes.get(ldap_attributes_mapping[key].lower()) |
|
1050 |
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])) |
|
1051 |
if not item_value[key]: |
|
1052 |
del item_value[key] |
|
1053 |
elif len(item_value[key]) == 1: |
|
1054 |
item_value[key] = item_value[key][0] |
|
1055 |
extra_attribute_values.append(item_value) |
|
1056 |
else: |
|
1057 |
log.warning('loop_over_attribute not defined for extra attribute %s' % extra_attribute_name) |
|
1058 |
extra_attribute_serialization = extra_attribute_config.get('serialization', None) |
|
1059 |
if extra_attribute_serialization is None: |
|
1060 |
attribute_map[extra_attribute_name] = extra_attribute_values |
|
1061 |
elif extra_attribute_serialization == 'json': |
|
1062 |
attribute_map[extra_attribute_name] = json.dumps(extra_attribute_values) |
|
1063 |
else: |
|
1064 |
log.warning('Invalid serialization type "%s" for extra attribute %s' % (extra_attribute_serialization, extra_attribute_name)) |
|
995 | 1065 |
return attribute_map |
996 | 1066 | |
997 | 1067 |
@classmethod |
... | ... | |
1141 | 1211 |
for block in cls.get_config(): |
1142 | 1212 |
names.update(cls.get_ldap_attributes_names(block)) |
1143 | 1213 |
names.update(map_text(block['mandatory_attributes_values']).keys()) |
1214 |
names.update(map_text(block['extra_attributes']).keys()) |
|
1144 | 1215 |
return [(a, '%s (LDAP)' % a) for a in sorted(names)] |
1145 | 1216 | |
1146 | 1217 |
@classmethod |
tests/test_ldap.py | ||
---|---|---|
15 | 15 |
# You should have received a copy of the GNU Affero General Public License |
16 | 16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 | |
18 |
import json |
|
18 | 19 |
import os |
19 | 20 | |
20 | 21 |
import pytest |
... | ... | |
54 | 55 |
PASS = 'passé' |
55 | 56 |
UPASS = u'passé' |
56 | 57 |
EMAIL = 'etienne.michu@example.net' |
58 |
CARLICENSE = '123445ABC' |
|
59 | ||
60 |
EO_O = "EO" |
|
61 |
EO_STREET = "169 rue du Chateau" |
|
62 |
EO_POSTALCODE = "75014" |
|
63 |
EO_CITY = "PARIS" |
|
64 | ||
65 |
EE_O = "EE" |
|
66 |
EE_STREET = "44 rue de l'Ouest" |
|
67 |
EE_POSTALCODE = "75014" |
|
68 |
EE_CITY = "PARIS" |
|
57 | 69 | |
58 | 70 |
base_dir = os.path.dirname(__file__) |
59 | 71 |
key_file = os.path.join(base_dir, 'key.pem') |
... | ... | |
90 | 102 |
l: Paris |
91 | 103 |
mail: etienne.michu@example.net |
92 | 104 |
jpegPhoto:: ACOE |
105 |
carLicense: {cl} |
|
106 |
o: EO |
|
107 |
o: EE |
|
93 | 108 | |
94 | 109 |
dn: cn=group1,o=ôrga |
95 | 110 |
objectClass: groupOfNames |
96 | 111 |
member: {dn} |
97 | 112 | |
98 |
'''.format(dn=DN, uid=UID, password=PASS)) |
|
113 |
dn: o={eo_o},o=ôrga |
|
114 |
objectClass: organization |
|
115 |
o: {eo_o} |
|
116 |
postalAddress: {eo_street} |
|
117 |
postalCode: {eo_postalcode} |
|
118 |
l: {eo_city} |
|
119 | ||
120 |
dn: o={ee_o},o=ôrga |
|
121 |
objectClass: organization |
|
122 |
o: {ee_o} |
|
123 |
postalAddress: {ee_street} |
|
124 |
postalCode: {ee_postalcode} |
|
125 |
l: {ee_city} |
|
126 | ||
127 |
'''.format(dn=DN, uid=UID, password=PASS, cl=CARLICENSE, |
|
128 |
eo_o=EO_O, eo_street=EO_STREET, eo_postalcode=EO_POSTALCODE, eo_city=EO_CITY, |
|
129 |
ee_o=EE_O, ee_street=EE_STREET, ee_postalcode=EE_POSTALCODE, ee_city=EE_CITY |
|
130 |
)) |
|
99 | 131 |
for i in range(5): |
100 | 132 |
slapd.add_ldif('''dn: uid=mïchu{i},o=ôrga |
101 | 133 |
objectClass: inetOrgPerson |
... | ... | |
949 | 981 |
'url': [slapd.ldap_url], |
950 | 982 |
'basedn': u'o=ôrga', |
951 | 983 |
'use_tls': False, |
984 |
'attributes': ['uid', 'carLicense'], |
|
952 | 985 |
}] |
953 | 986 |
user = authenticate(username=USERNAME, password=UPASS) |
954 | 987 |
assert user |
... | ... | |
958 | 991 |
'mail': [u'etienne.michu@example.net'], |
959 | 992 |
'sn': [u'Michu'], |
960 | 993 |
'uid': [u'etienne.michu'], |
994 |
'carlicense': ['123445ABC'], |
|
961 | 995 |
} |
962 | 996 |
# simulate LDAP down |
963 | 997 |
slapd.stop() |
... | ... | |
967 | 1001 |
'mail': [u'etienne.michu@example.net'], |
968 | 1002 |
'sn': [u'Michu'], |
969 | 1003 |
'uid': [u'etienne.michu'], |
1004 |
'carlicense': ['123445ABC'], |
|
970 | 1005 |
} |
971 | 1006 |
assert not user.check_password(UPASS) |
972 | 1007 |
# simulate LDAP come back up |
... | ... | |
982 | 1017 |
'mail': [u'etienne.michu@example.net'], |
983 | 1018 |
'sn': [u'Micho'], |
984 | 1019 |
'uid': [u'etienne.michu'], |
1020 |
'carlicense': ['123445ABC'], |
|
985 | 1021 |
} |
1022 | ||
1023 | ||
1024 |
@pytest.mark.django_db |
|
1025 |
def test_get_extra_attributes(slapd, settings, client): |
|
1026 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
1027 |
'url': [slapd.ldap_url], |
|
1028 |
'basedn': 'o=ôrga', |
|
1029 |
'use_tls': False, |
|
1030 |
'groupstaff': ['cn=group1,o=ôrga'], |
|
1031 |
'attributes': ['uid'], |
|
1032 |
'extra_attributes': { |
|
1033 |
'orga': { |
|
1034 |
'loop_over_attribute': 'o', |
|
1035 |
'filter': '(&(objectclass=organization)(o={item}))', |
|
1036 |
'basedn': 'o=ôrga', |
|
1037 |
'scope': 'sub', |
|
1038 |
'mapping': { |
|
1039 |
'id': 'o', |
|
1040 |
'street': 'postalAddress', |
|
1041 |
'city': 'l', |
|
1042 |
'postal_code': 'postalCode', |
|
1043 |
}, |
|
1044 |
'serialization': 'json' |
|
1045 |
} |
|
1046 |
}, |
|
1047 |
}] |
|
1048 |
response = client.post('/login/', {'login-password-submit': '1', |
|
1049 |
'username': 'etienne.michu', |
|
1050 |
'password': PASS}, follow=True) |
|
1051 |
user = response.context['user'] |
|
1052 |
fetched_attrs = user.get_attributes(object(), {}) |
|
1053 |
assert UID in fetched_attrs.get('uid') |
|
1054 |
assert 'orga' in fetched_attrs |
|
1055 |
orgas = json.loads(fetched_attrs.get('orga')) |
|
1056 |
assert isinstance(orgas, list) |
|
1057 |
assert len(orgas) == 2 |
|
1058 |
assert {'id': EO_O, 'street': EO_STREET, 'city': EO_CITY, 'postal_code': EO_POSTALCODE} in orgas |
|
1059 |
assert {'id': EE_O, 'street': EE_STREET, 'city': EE_CITY, 'postal_code': EE_POSTALCODE} in orgas |
|
986 |
- |