Archived
New user: Initial LDAP work
This commit is contained in:
1 parent
425f4cc3cb
commit
166a02e7cb
6 files changed
+165
-97
No files matched your search
+28
-20
@@ -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
|
||||
|
||||
+80
-43
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
+10
-9
@@ -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:
|
||||
|
||||
+37
-20
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user