|
|
|
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
|