klonfish
6 years ago
4 changed files with 222 additions and 5 deletions
@ -0,0 +1,6 @@
@@ -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 @@
@@ -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 @@
@@ -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