Blinkenbunt Account Manager
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.
 
 

169 lines
4.8 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'
@classmethod
def get_mapped_fields(cls):
fields = set()
fields.update(cls.attr_map.values())
fields.update(cls.mandatory_attr_fallbacks.values())
fields.add(cls.passw_attr)
return fields
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