klonfish
6 years ago
4 changed files with 222 additions and 5 deletions
@ -0,0 +1,6 @@ |
|||||||
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher |
||||||
|
from django.utils.crypto import get_random_string |
||||||
|
|
||||||
|
class LDAPPBKDF2PasswordHasher(PBKDF2PasswordHasher): |
||||||
|
def salt(self): |
||||||
|
return get_random_string(16) |
@ -0,0 +1,152 @@ |
|||||||
|
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 |
@ -0,0 +1,37 @@ |
|||||||
|
from django.core.management.base import BaseCommand, CommandError |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
from django.conf import settings |
||||||
|
from ...ldap_sync import make_ldap_conn, LDAPAction, LDAPUserSyncer |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
class Command(BaseCommand): |
||||||
|
def handle(self, *args, **kwargs): |
||||||
|
uri = settings.BAM_LDAP_URI |
||||||
|
bind_dn = settings.BAM_LDAP_BIND_DN |
||||||
|
secret = settings.BAM_LDAP_SECRET |
||||||
|
base_dn_map = settings.BAM_LDAP_BASE_DN_MAP |
||||||
|
ldap_conn = make_ldap_conn(uri, bind_dn, secret) |
||||||
|
|
||||||
|
syncer = LDAPUserSyncer(ldap_conn, base_dn_map) |
||||||
|
for user in User.objects.all(): |
||||||
|
self.stdout.write('Syncing user "%s"...' % user.username) |
||||||
|
actions = syncer.sync_user(user) |
||||||
|
something_done = False |
||||||
|
for base_dn, act in zip(base_dn_map.keys(), actions): |
||||||
|
if act == LDAPAction.USER_ADDED: |
||||||
|
self.stdout.write(self.style.SUCCESS( |
||||||
|
'... added them to "%s"' % base_dn)) |
||||||
|
something_done = True |
||||||
|
elif act == LDAPAction.USER_UPDATED: |
||||||
|
self.stdout.write(self.style.WARNING( |
||||||
|
'... updated their entry in "%s"' % base_dn |
||||||
|
)) |
||||||
|
something_done = True |
||||||
|
elif act == LDAPAction.USER_DELETED: |
||||||
|
self.stdout.write(self.style.ERROR( |
||||||
|
'... deleted their entry from "%s"' % base_dn |
||||||
|
)) |
||||||
|
something_done = True |
||||||
|
if not something_done: |
||||||
|
self.stdout.write('... nothing to do') |
Loading…
Reference in new issue