From 166a02e7cb9ea89da1cc7d16cd801a2615059553 Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Fri, 20 Feb 2015 23:34:04 +0100 Subject: [PATCH] New user: Initial LDAP work --- neuf_ldap/models.py | 48 ++++++++------- neuf_ldap/utils.py | 123 +++++++++++++++++++++++++-------------- neuf_radius/utils.py | 6 +- neuf_userinfo/forms.py | 9 ++- neuf_userinfo/views.py | 19 +++--- userinfosite/settings.py | 57 +++++++++++------- 6 files changed, 165 insertions(+), 97 deletions(-) diff --git a/neuf_ldap/models.py b/neuf_ldap/models.py index c5fa2ea..e6b0b0f 100644 --- a/neuf_ldap/models.py +++ b/neuf_ldap/models.py @@ -1,4 +1,5 @@ import datetime +from django.conf import settings import ldapdb.models from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField @@ -7,15 +8,14 @@ from neuf_ldap.utils import ldap_create, ldap_validate class LdapUser(ldapdb.models.Model): """ - Represents an LDAP posixAccount,inetOrgPerson entry. - Ref: - - http://www.zytrax.com/books/ldap/apa/types.html + Represents an LDAP posixAccount, inetOrgPerson, shadowAccount entry. + Ref: http://www.zytrax.com/books/ldap/apa/types.html """ connection_name = 'ldap' # LDAP meta-data - base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG + base_dn = settings.LDAP_USER_DN object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount'] # inetOrgPerson @@ -31,33 +31,41 @@ class LdapUser(ldapdb.models.Model): id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form group = IntegerField(db_column='gidNumber') gecos = CharField(db_column='gecos') + display_name = CharField(db_column='displayname') home_directory = CharField(db_column='homeDirectory') - login_shell = CharField(db_column='loginShell', default='/bin/bash') + login_shell = CharField(db_column='loginShell', default=settings.LDAP_LOGIN_SHELL) username = CharField(db_column='uid', primary_key=True) # shadowAccount password = CharField(db_column='userPassword') - shadowLastChange = CharField(db_column='shadowLastChange', default='10877') # Magic number? - shadowMin = CharField(db_column='shadowMin', default='8') - shadowMax = CharField(db_column='shadowMax', default='999999') - shadowWarning = CharField(db_column='shadowWarning', default='7') - shadowInactive = CharField(db_column='shadowInactive') # FIXME what should this be? - shadowExpire = CharField(db_column='shadowExpire', default='-1') - shadowFlag = CharField(db_column='shadowFlag', default='0') + shadowLastChange = IntegerField(db_column='shadowLastChange', default=settings.LDAP_SHADOW_LAST_CHANGE) + shadowMin = IntegerField(db_column='shadowMin', default=settings.LDAP_SHADOW_MIN) + shadowMax = IntegerField(db_column='shadowMax', default=settings.LDAP_SHADOW_MAX) + shadowWarning = IntegerField(db_column='shadowWarning', default=settings.LDAP_SHADOW_WARNING) + shadowInactive = IntegerField(db_column='shadowInactive') + shadowExpire = IntegerField(db_column='shadowExpire', default=settings.LDAP_SHADOW_EXPIRE) + shadowFlag = IntegerField(db_column='shadowFlag', default=settings.LDAP_SHADOW_FLAG) # core description = CharField(db_column='description') - # ugly hack to use django internals for password reset + # Hack to use django internals for password reset last_login = datetime.datetime(2001, 1, 1) - def set_password(self, raw_password): - if raw_password is None: - self.password = "!" - return + def _days_since_1970(self): + epoch = datetime.datetime.utcfromtimestamp(0) + today = datetime.datetime.today() + delta = today - epoch + return delta.days + + def set_password(self, raw_password, commit=True): self.password = ldap_create(raw_password) - self.save() + # Update last changed password date + self.shadowLastChange = self._days_since_1970() + + if commit: + self.save() def check_password(self, raw_password): return ldap_validate(raw_password, self.password) @@ -72,13 +80,13 @@ class LdapGroup(ldapdb.models.Model): connection_name = 'ldap' # LDAP meta-data - base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG + base_dn = settings.LDAP_GROUP_DN object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) - usernames = ListField(db_column='memberUid') + members = ListField(db_column='memberUid') def __unicode__(self): return self.name diff --git a/neuf_ldap/utils.py b/neuf_ldap/utils.py index d513c14..975b2a4 100644 --- a/neuf_ldap/utils.py +++ b/neuf_ldap/utils.py @@ -1,51 +1,23 @@ -import hashlib +# coding: utf-8 +from __future__ import unicode_literals +import json +from django.conf import settings + import os -from base64 import encodestring as encode -from base64 import decodestring as decode -import os.path +from passlib.hash import ldap_salted_sha1 +import logging + +logger = logging.getLogger(__name__) -""" - Ref: - - http://www.openldap.org/faq/data/cache/347.html - - http://www.openldap.org/doc/admin24/security.html -""" - -# TODO: Move this to settings file? -BASE_DN = "dc=neuf,dc=no" -KERBEROS_DN = "cn=krbcontainer,{}".format(BASE_DN) -AUTOMOUNT_DN = "ou=Automount,{}".format(BASE_DN) -USER_DN = "ou=People,{}".format(BASE_DN) -GROUP_DN = "ou=Groups,{}".format(BASE_DN) - -FILESERVER_HOST = "wii.neuf.no" -FILESERVER_HOME_PATH = "/fileserver/homes" - -UID_MIN = "10000" -UID_MAX = "100000" -GID_MIN = "9000" -GID_MAX = "9999" -USER_GID_MIN = "10000" -USER_GID_MAX = "100000" - - -def ldap_create(raw_password, hash_type='ssha'): - # Generate a salted SHA1 hash - salt = os.urandom(4) - h = hashlib.sha1(raw_password) - h.update(salt) - return "{SSHA}" + encode(h.digest() + salt)[:-1] +def ldap_create(raw_password): + return ldap_salted_sha1.encrypt(raw_password) def ldap_validate(raw_password, challenge_password): # challenge_password is hash from db # raw_password is the cleartext password. - challenge_bytes = decode(challenge_password[6:]) - digest = challenge_bytes[:20] - salt = challenge_bytes[20:] - hr = hashlib.sha1(raw_password) - hr.update(salt) - return digest == hr.digest() + return ldap_salted_sha1.validate(raw_password, challenge_password) def set_ldap_password(username, raw_password): @@ -58,10 +30,75 @@ def set_ldap_password(username, raw_password): pass -def ldap_create_user(user): +def ldap_user_group_exists(username): + from neuf_ldap.models import LdapGroup + + return len(LdapGroup.objects.filter(name=username)) != 0 + + +def ldap_username_exists(username): from neuf_ldap.models import LdapUser - u = LdapUser() - # TODO + + return len(LdapUser.objects.filter(username=username)) != 0 + + +def ldap_create_user(user): + from neuf_ldap.models import LdapUser, LdapGroup + + def _get_next_uid(): + # Get user ids between min and max, and order desc by id + users = LdapUser.objects.filter(id__gte=settings.LDAP_UID_MIN, id__lte=settings.LDAP_UID_MAX) + users = users.order_by('-id').values_list('id', flat=True) + + if len(users) == 0: + return settings.LDAP_UID_MIN + + if len(users) > 0 and users[0] >= settings.LDAP_UID_MAX: + logger.exception("UID larger than LDAP_UID_MAX") + + return users[0] + 1 + + def _get_next_user_gid(): + # Get user group ids between min and max, and order desc by id + user_groups = LdapGroup.objects.filter(gid__gte=settings.LDAP_USER_GID_MIN, gid__lte=settings.LDAP_USER_GID_MAX) + user_groups = user_groups.order_by('-gid').values_list('gid', flat=True) + + if len(user_groups) > 0 and user_groups[0] >= settings.LDAP_USER_GID_MAX: + logger.exception("UID larger than LDAP_USER_GID_MAX") + + return user_groups[0] + 1 + + groups = user.pop('groups') + # User + full_name = '{} {}'.format(user['firstname'], user['lastname']) + user_data = { + 'first_name': user['firstname'], + 'last_name': user['lastname'], + 'full_name': full_name, + 'display_name': full_name, + 'gecos': full_name, + 'email': user['email'], + 'username': user['username'], + 'id': _get_next_uid(), + 'group': _get_next_user_gid(), + 'home_directory': os.path.join(settings.LDAP_HOME_DIRECTORY_PREFIX, user['username']) + } + ldap_user = LdapUser(**user_data) + ldap_user.set_password(user['password'], commit=False) + ldap_user.save() + + # Add user group + ldap_user_group = LdapGroup(name=user['username'], gid=user_data['group'], members=[user['username']]) + ldap_user_group.save() + + # Add groups + ldap_groups = LdapGroup.objects.filter(name__in=groups) + for g in ldap_groups: + if user['username'] not in g.members: + g.members.append(user['username']) + g.save() + + # Finito! def ldap_create_automount(user): diff --git a/neuf_radius/utils.py b/neuf_radius/utils.py index bcab56b..c2c68c3 100644 --- a/neuf_radius/utils.py +++ b/neuf_radius/utils.py @@ -1,12 +1,10 @@ -import hashlib +from passlib.hash import nthash from neuf_radius.models import Radcheck def radius_create(raw_password): - # Found in the wild. - nt_password = hashlib.new('md4', raw_password.encode('utf-16le')).hexdigest() - return nt_password + return nthash.encrypt(raw_password) def set_radius_password(username, raw_password): diff --git a/neuf_userinfo/forms.py b/neuf_userinfo/forms.py index fc80dee..955f568 100644 --- a/neuf_userinfo/forms.py +++ b/neuf_userinfo/forms.py @@ -16,8 +16,9 @@ from inside.models import InsideUser from inside.utils import set_inside_password from neuf_kerberos.utils import set_kerberos_password +from neuf_ldap.models import LdapGroup, LdapUser from neuf_radius.utils import set_radius_password -from neuf_ldap.utils import set_ldap_password +from neuf_ldap.utils import set_ldap_password, ldap_username_exists, ldap_user_group_exists from neuf_userinfo.utils import decrypt_rijndael from neuf_userinfo.validators import PasswordValidator, UsernameValidator @@ -132,6 +133,12 @@ class NewUserForm(forms.Form): username = self.cleaned_data['username'] UsernameValidator(username) + if ldap_username_exists(username): + raise ValidationError("User '{}' already exists in LDAP".format(username)) + + if ldap_user_group_exists(username): + raise ValidationError("User '{}' already has a user group in LDAP".format(username)) + return username def clean_password(self): diff --git a/neuf_userinfo/views.py b/neuf_userinfo/views.py index 6740c4c..1b1efb5 100644 --- a/neuf_userinfo/views.py +++ b/neuf_userinfo/views.py @@ -150,16 +150,17 @@ class AddNewUserView(View): user = form.cleaned_data - ldap_create_user(user) - ldap_create_automount(user) - kerberos_create_principal(user) - create_homedir(user.username) # FIXME run ssh script? - radius_create_user(user.username, user.password) - # print "yey", user - # TODO you are still here + results = { + 'user': user, + 'ldap_user': ldap_create_user(user), + 'ldap_automount': ldap_create_automount(user), + 'kerberos_principal': kerberos_create_principal(user), + 'homedir': create_homedir(user['username']), # FIXME run ssh script? + 'radius': radius_create_user(user['username'], user['password']) + } + # import pprint; pprint.pprint(results) - result = {'yousir': 'are a scholar!'} - return JsonResponse(result) + return JsonResponse({'results': 'success'}) def validate_api_key(self, api_key): if api_key == '' or api_key != settings.INSIDE_USERSYNC_API_KEY: diff --git a/userinfosite/settings.py b/userinfosite/settings.py index 0aca2a2..0ea000f 100644 --- a/userinfosite/settings.py +++ b/userinfosite/settings.py @@ -62,6 +62,29 @@ TEMPLATE_DIRS = ( WSGI_APPLICATION = 'userinfosite.wsgi.application' +# LDAP (neuf.no) +LDAP_BASE_DN = "dc=neuf,dc=no" +LDAP_USER_DN = "ou=People,{}".format(LDAP_BASE_DN) +LDAP_GROUP_DN = "ou=Groups,{}".format(LDAP_BASE_DN) +LDAP_KERBEROS_DN = "cn=krbcontainer,{}".format(LDAP_BASE_DN) +LDAP_AUTOMOUNT_DN = "ou=Automount,{}".format(LDAP_BASE_DN) + +LDAP_UID_MIN = 10000 +LDAP_UID_MAX = 100000 +LDAP_GID_MIN = 9000 +LDAP_GID_MAX = 9999 +LDAP_USER_GID_MIN = 10000 +LDAP_USER_GID_MAX = 100000 + +LDAP_LOGIN_SHELL = '/bin/bash' +LDAP_HOME_DIRECTORY_PREFIX = '/home' +# Ref: http://tille.garrels.be/training/ldap/ch02s02.html +LDAP_SHADOW_LAST_CHANGE = 0 # Days since password last change +LDAP_SHADOW_MIN = 8 # +LDAP_SHADOW_MAX = 999999 +LDAP_SHADOW_WARNING = 7 # +LDAP_SHADOW_EXPIRE = -1 # +LDAP_SHADOW_FLAG = 0 # Database # https://docs.djangoproject.com/en/1.6/ref/settings/#databases @@ -73,14 +96,14 @@ DATABASES = { 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://localhost/', - 'USER': 'uid=test,ou=People,dc=neuf,dc=no', + 'USER': 'uid=test,{}'.format(LDAP_USER_DN), 'PASSWORD': 'test', }, 'radius': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'radius', - 'USER': 'radius', - 'PASSWORD': '', + 'NAME': 'dev_radius', + 'USER': 'dev', + 'PASSWORD': 'dev', 'HOST': 'localhost', }, 'inside': { @@ -138,21 +161,17 @@ import ldap from django_auth_ldap.config import LDAPSearch, PosixGroupType # Basic user auth -AUTH_LDAP_U_USER_SEARCH = LDAPSearch("ou=People,dc=neuf,dc=no", ldap.SCOPE_ONELEVEL, "(uid=%(user)s)") +AUTH_LDAP_U_USER_SEARCH = LDAPSearch(LDAP_USER_DN, ldap.SCOPE_ONELEVEL, "(uid=%(user)s)") # Basic groups -AUTH_LDAP_U_GROUP_SEARCH = LDAPSearch("ou=Groups,dc=neuf,dc=no", ldap.SCOPE_ONELEVEL, "(objectClass=posixGroup)") +AUTH_LDAP_U_GROUP_SEARCH = LDAPSearch(LDAP_GROUP_DN, ldap.SCOPE_ONELEVEL, "(objectClass=posixGroup)") AUTH_LDAP_U_GROUP_TYPE = PosixGroupType() # Mirror groups on each auth AUTH_LDAP_U_MIRROR_GROUPS = True # Group to user flag mappings AUTH_LDAP_U_USER_FLAGS_BY_GROUP = { - "is_active": "cn=dns-alle,ou=Groups,dc=neuf,dc=no", - "is_staff": "cn=edb,ou=Groups,dc=neuf,dc=no", - "is_superuser": "cn=edbadmin,ou=Groups,dc=neuf,dc=no" -} -# Group to profile flag mappings, not used. -AUTH_LDAP_U_PROFILE_FLAGS_BY_GROUP = { - # "is_edb": "cn=edb,ou=Groups,dc=neuf,dc=no" + "is_active": "cn=dns-alle,{}".format(LDAP_GROUP_DN), + "is_staff": "cn=edb,{}".format(LDAP_GROUP_DN), + "is_superuser": "cn=edbadmin,{}".format(LDAP_GROUP_DN) } # User attribute mappings AUTH_LDAP_U_USER_ATTR_MAP = { @@ -160,10 +179,6 @@ AUTH_LDAP_U_USER_ATTR_MAP = { "last_name": "sn", "email": "mail", } -# User profile attribute mappings -AUTH_LDAP_U_PROFILE_ATTR_MAP = { - "home_directory": "homeDirectory" -} # Allways update the django user object on authentication. AUTH_LDAP_U_ALWAYS_UPDATE_USER = True @@ -172,15 +187,13 @@ AUTH_LDAP_E_SERVER_URI = AUTH_LDAP_U_SERVER_URI AUTH_LDAP_E_BIND_DN = AUTH_LDAP_U_BIND_DN AUTH_LDAP_E_BIND_PASSWORD = AUTH_LDAP_U_BIND_PASSWORD -AUTH_LDAP_E_USER_SEARCH = LDAPSearch("ou=People,dc=neuf,dc=no", ldap.SCOPE_ONELEVEL, "(mail=%(user)s)") +AUTH_LDAP_E_USER_SEARCH = LDAPSearch(LDAP_USER_DN, ldap.SCOPE_ONELEVEL, "(mail=%(user)s)") AUTH_LDAP_E_GROUP_SEARCH = AUTH_LDAP_U_GROUP_SEARCH AUTH_LDAP_E_GROUP_TYPE = AUTH_LDAP_U_GROUP_TYPE AUTH_LDAP_E_MIRROR_GROUPS = AUTH_LDAP_U_MIRROR_GROUPS AUTH_LDAP_E_USER_FLAGS_BY_GROUP = AUTH_LDAP_U_USER_FLAGS_BY_GROUP -AUTH_LDAP_E_PROFILE_FLAGS_BY_GROUP = AUTH_LDAP_U_PROFILE_FLAGS_BY_GROUP AUTH_LDAP_E_USER_ATTR_MAP = AUTH_LDAP_U_USER_ATTR_MAP -AUTH_LDAP_E_PROFILE_ATTR_MAP = AUTH_LDAP_U_PROFILE_ATTR_MAP AUTH_LDAP_E_ALWAYS_UPDATE_USER = AUTH_LDAP_U_ALWAYS_UPDATE_USER # Debug logging @@ -199,6 +212,10 @@ INSIDE_GROUPS_SYNC_DELETE = True # When user logs in, groups are synced (locall INSIDE_USERSYNC_API_KEY = '' INSIDE_USERSYNC_ENC_KEY = '' +# Home dir +FILESERVER_HOST = "wii.neuf.no" +FILESERVER_HOME_PATH = "/fileserver/homes" + try: from local_settings import * except ImportError: