You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.6 KiB
161 lines
4.6 KiB
import enum |
|
import base64 |
|
import ldap |
|
import ldap.modlist |
|
import ldap.filter |
|
|
|
class LDAPAction(enum.Enum): |
|
USER_UNCHANGED = 0 |
|
USER_ADDED = 1 |
|
USER_UPDATED = 2 |
|
USER_DELETED = 3 |
|
|
|
def b64_to_ab64(b64): |
|
return b64.replace('+', '.').replace('=', '') |
|
|
|
def transform_password(pw): |
|
parts = pw.split('$') |
|
raw_scheme = parts[0] |
|
|
|
if raw_scheme == 'pbkdf2': |
|
scheme = '{PBKDF2}' |
|
elif raw_scheme.startswith('pbkdf2_sha'): |
|
sha_version = raw_scheme[10:] |
|
scheme = '{PBKDF2-SHA' + sha_version + '}' |
|
else: |
|
raise ValueError('Unsupported hashing scheme "%s"' % raw_scheme) |
|
|
|
iters = parts[1] |
|
salt = b64_to_ab64(base64.b64encode( |
|
parts[2].encode('ascii'), b'./').decode('ascii')) |
|
pw_hash = b64_to_ab64(parts[3]) |
|
return '%s%s$%s$%s' % (scheme, iters, salt, pw_hash) |
|
|
|
def make_ldap_conn(uri, bind_dn, secret): |
|
conn = ldap.initialize(uri) |
|
conn.simple_bind_s(bind_dn, secret) |
|
return conn |
|
|
|
class LDAPUserEntry(): |
|
user_object_class = 'inetOrgPerson' |
|
id_attr = 'uid' |
|
attr_map = { |
|
'uid': 'username', |
|
'cn': 'username', |
|
'sn': 'last_name', |
|
'givenName': 'first_name', |
|
'mail': 'email', |
|
} |
|
mandatory_attr_fallbacks = { |
|
'sn': 'username', |
|
} |
|
passw_attr = 'userPassword' |
|
|
|
def __init__(self, user, ldap_conn, base_dn): |
|
self.user = user |
|
self.base_dn = base_dn |
|
self.dn = self.create_dn() |
|
self.conn = ldap_conn |
|
|
|
def create_dn(self): |
|
id_value = getattr(self.user, self.attr_map[self.id_attr]) |
|
id_value = ldap.dn.escape_dn_chars(id_value) |
|
return '%s=%s,%s' % (self.id_attr, id_value, self.base_dn) |
|
|
|
def create_attrs(self): |
|
cls = type(self) |
|
attrs = {} |
|
attrs['objectClass'] = cls.user_object_class |
|
for ldap_attr, model_attr in cls.attr_map.items(): |
|
value = getattr(self.user, model_attr, None) |
|
if ldap_attr in cls.mandatory_attr_fallbacks and not value: |
|
value = getattr(self.user, |
|
cls.mandatory_attr_fallbacks[ldap_attr], |
|
'undefined') |
|
if value != '': |
|
attrs[ldap_attr] = value |
|
|
|
pw_hash = transform_password(self.user.password) |
|
attrs[cls.passw_attr] = pw_hash |
|
|
|
return {k: [v.encode('utf8')] for k, v in attrs.items()} |
|
|
|
def get(self): |
|
try: |
|
res = self.conn.search_s(self.dn, ldap.SCOPE_BASE) |
|
|
|
if len(res) == 1: |
|
return res[0][1] |
|
except ldap.NO_SUCH_OBJECT: |
|
pass |
|
|
|
def exists(self): |
|
return self.get() is not None |
|
|
|
def add(self): |
|
attrs = self.create_attrs() |
|
user_modlist = ldap.modlist.addModlist(attrs) |
|
|
|
self.conn.add_s(self.dn, user_modlist) |
|
|
|
def update(self): |
|
attrs_ldap = self.get() |
|
attrs_model = self.create_attrs() |
|
|
|
if not self.user.has_usable_password(): |
|
# Avoid overwriting the LDAP password with an unusable one |
|
pw_attr = self(type).passw_attr |
|
attrs_model[pw_attr] = attrs_ldap[pw_attr] |
|
|
|
modlist = ldap.modlist.modifyModlist(attrs_ldap, attrs_model) |
|
if len(modlist) > 0: |
|
self.conn.modify_s(self.dn, modlist) |
|
return LDAPAction.USER_UPDATED |
|
|
|
return LDAPAction.USER_UNCHANGED |
|
|
|
def add_or_update(self): |
|
if self.exists(): |
|
return self.update() |
|
else: |
|
self.add() |
|
return LDAPAction.USER_ADDED |
|
|
|
def delete(self): |
|
self.conn.delete_s(self.dn) |
|
|
|
def delete_if_present(self): |
|
if self.exists(): |
|
self.delete() |
|
return LDAPAction.USER_DELETED |
|
|
|
return LDAPAction.USER_UNCHANGED |
|
|
|
class LDAPUserSyncer(): |
|
def __init__(self, ldap_conn, base_dn_map): |
|
self.base_dn_map = base_dn_map |
|
self.conn = ldap_conn |
|
|
|
def sync_user(self, user): |
|
actions = [] |
|
for base_dn, grp_names in self.base_dn_map.items(): |
|
user_entry = LDAPUserEntry(user, self.conn, base_dn) |
|
action = LDAPAction.USER_UNCHANGED |
|
if (user.is_active and |
|
user.groups.filter(name__in=grp_names).exists()): |
|
action = user_entry.add_or_update() |
|
else: |
|
action = user_entry.delete_if_present() |
|
|
|
actions.append(action) |
|
|
|
return actions |
|
|
|
def remove_user(self, user): |
|
actions = [] |
|
for base_dn in self.base_dn_map: |
|
user_entry = LDAPUserEntry(user, self.conn, base_dn) |
|
action = user_entry.delete_if_present() |
|
actions.append(action) |
|
|
|
return actions
|
|
|