Commit d30b0c3d authored by Zack Wong's avatar Zack Wong
Browse files

implemented AD account creation via LDAP

parent 9980f39a
......@@ -9,27 +9,26 @@ from formtools.wizard.views import SessionWizardView
from .models import Member
from .forms import MyModelForm, MyForm
from .views import MyUpdateView, MyWizardView
from memberdb.account_backend import validate_username
from memberdb.account_backend import validate_username, create_ad_user
class AccountForm(MyModelForm):
# form fields
username = forms.SlugField(
validators=[validate_username],
max_length=19,
)
password = forms.CharField(
min_length=10,
max_length=127,
widget=forms.PasswordInput,
min_length=10,
max_length=127,
widget=forms.PasswordInput,
strip=False,
help_text="Password must be between 10 and 127 characters long"
)
)
confirm_password = forms.CharField(
min_length=10,
max_length=127,
min_length=10,
max_length=127,
widget=forms.PasswordInput,
strip=False,
)
......@@ -48,11 +47,11 @@ class AccountForm(MyModelForm):
def save(self):
return
class EmailForm(MyModelForm):
forward = forms.BooleanField(required=False)
email_address = forms.EmailField(
label='Forwarding address (optional)',
label='Forwarding address (optional)',
required=False,
help_text="Your club email will be forwarded to this address. Leave blank if email forwarding is not required"
)
......@@ -60,7 +59,7 @@ class EmailForm(MyModelForm):
class Meta:
model = Member
fields = ['forward', 'email_address']
def clean(self):
if self['forward'].value() == True:
try:
......@@ -74,16 +73,16 @@ class EmailForm(MyModelForm):
class DispenseForm(MyForm):
pin = forms.CharField(
min_length=0,
max_length=4,
widget=forms.PasswordInput,
min_length=0,
max_length=4,
widget=forms.PasswordInput,
strip=False,
required=False,
help_text="PIN must be 4 digits long")
help_text="PIN must be 4 digits long")
confirm_pin = forms.CharField(
min_length=0,
max_length=4,
min_length=0,
max_length=4,
widget=forms.PasswordInput,
required=False,
strip=False,
......@@ -99,7 +98,7 @@ class DispenseForm(MyForm):
except:
pass
super().clean();
class AccountView(MyWizardView):
form_list = [AccountForm,EmailForm,DispenseForm]
......@@ -110,7 +109,7 @@ class AccountView(MyWizardView):
return self.object
def get_context_data(self, **kwargs):
m = self.object
m = self.object
context = super().get_context_data(**kwargs)
context.update(self.admin.admin_site.each_context(self.request))
context.update({
......@@ -121,6 +120,12 @@ class AccountView(MyWizardView):
def done(self, form_list, form_dict, **kwargs):
# create the user and save their username if successfull
if create_ad_user(self.get_cleaned_data_for_step('0'), self.object):
form_list[0].save()
messages.success(self.request, 'Your membership renewal has been submitted.')
return HttpResponseRedirect(reverse("admin:memberdb_membership_changelist"))
......@@ -130,8 +135,8 @@ class AccountView(MyWizardView):
def accountProgressView(request, member):
return
def accountFinalView():
return render(request, 'accountfinal.html', context)
return render(request, 'accountfinal.html', context)
......@@ -5,9 +5,12 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.translation import gettext_lazy as _
import ldap
import re
import socket
from ldap3 import Server, Connection, MODIFY_REPLACE,MODIFY_ADD
from ldap3.core.results import RESULT_SUCCESS
from ldap3.core.exceptions import *
import subprocess
from subprocess import CalledProcessError, TimeoutExpired
......@@ -18,69 +21,94 @@ from squarepay import dispense
log = logging.getLogger('ldap')
# load config
# load config
ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI')
ldap_search_dn = getattr(settings, 'LDAP_USER_SEARCH_DN')
ldap_user_dn = getattr(settings, 'LDAP_USER_SEARCH_DN')
ldap_base_dn = getattr(settings, 'LDAP_BASE_DN')
ldap_bind_dn = getattr(settings, 'LDAP_BIND_DN')
ldap_bind_secret = getattr(settings, 'LDAP_BIND_SECRET')
ldap_opts = getattr(settings, 'AUTH_LDAP_GLOBAL_OPTIONS')
maxuid_dn = "CN=uccdomayne,CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System,"+ldap_base_dn
#initalise ldap instace
_ldap_inst = ldap.initialize(ldap_uri)
for option,value in ldap_opts.items():
_ldap_inst.set_option(option,value)
_ldap_inst.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
_ldap_inst = Connection(
Server(ldap_uri),
client_strategy='SYNC',
user=ldap_bind_dn,
password=ldap_bind_secret,
raise_exceptions=True,
)
# get the ldap instance and bind if required
def get_ldap_instance():
try:
_ldap_inst.bind(ldap_bind_dn, ldap_bind_secret)
except ldap.INVALID_CREDENTIALS:
log.error("LDAP: Invalid bind credentials")
except ldap.SERVER_DOWN:
log.error("LDAP: Cannot Contact LDAP server")
if not _ldap_inst.bound:
try:
_ldap_inst.bind()
except LDAPInvalidCredentialsResult:
log.error("LDAP: Invalid bind credentials")
raise
return _ldap_inst
def get_user_attrs(username, attrs):
# TODO verify bind
def get_ldap_attrs(dn, filter, limit, attrs):
ld = get_ldap_instance()
filter = "cn=" + username
result = ld.search_s(ldap_search_dn, ldap.SCOPE_SUBTREE, filter, attrs)
if len(result) > 1:
# multiple accounts matched, this is a problem
return ldap.NO_UNIQUE_ENTRY
if len(result) == 0:
return ldap.NO_SUCH_OBJECT
return result[0];
ld.search(dn, filter, size_limit=limit, attributes=attrs)
result = ld.result
# fetch matched objects on success
if (result['result'] == RESULT_SUCCESS):
entries = ld.entries
else:
# otherwise raise an exception
raise LDAPOperationResult(
result=result['result'],
description=result['description'],
dn=result['dn'],
message=result['message'],
response_type=result['type'])
if len(entries) == 0:
raise LDAPNoSuchObjectResult()
return entries;
def get_user_attrs(username, attrs):
# find the user
filter = "(cn=" + username + ')'
result = get_ldap_attrs(ldap_user_dn, filter, 1, attrs)
return result[0];
def get_maxuid():
ld = get_ldap_instance()
filter = "(cn=*)"
attrs = ['msSFU30MaxUidNumber']
result = get_ldap_attrs(maxuid_dn, filter, 1, attrs)
return result[0]
def get_account_lock_status(username):
ld = get_ldap_instance()
try:
result = get_user_attrs(username, ['userAccountControl'])
finally:
ld.unbind()
# user does not exist
except LDAPNoSuchObjectResult:
return None
# return UAC flag 0x002 ('ACCOUNT_DISABLE')
return bool(result[1]['userAccountControl'] & 0x002)
def validate_username(value : str):
# note: slug validator ensures that username only contains [a-z0-9_-]
# usernames can't begin with a numeric
if not value[0].isalpha():
raise ValidationError(
_('Username must begin with a letter')
)
raise ValidationError(_('Username must begin with a letter'))
# ensure username is lowercase
elif not value.islower():
raise ValidationError(
_('Username cannot contain uppercase characters')
)
if not value.islower():
raise ValidationError(_('Username cannot contain uppercase characters'))
# check if the user exists, this test should catch *most* cases
if subprocess.call(["id", value]) == 0:
if subprocess.call(["id", value], stderr=subprocess.DEVNULL) == 0:
raise ValidationError(_('Username already taken (passwd)'))
# usernames cannot conflict with hostnames
......@@ -90,20 +118,14 @@ def validate_username(value : str):
_('Username already taken (CNAME)')
)
except socket.gaierror:
pass
pass
# lookup user in ldap, required because not all users are mapped to *nix users
try:
if get_user_attrs(value, ['cn']) != ldap.NO_SUCH_OBJECT:
raise ValidationError(
_('Username already taken (AD)')
)
except ldap.LDAPError:
log.error("Network error, cannot verify username")
raise ldap.OPERATIONS_ERROR
get_user_attrs(value, None)
except LDAPNoSuchObjectResult:
pass
else:
raise ValidationError(_('Username already taken (AD)'))
# locks the specified User Account by performing the following actions:
......@@ -117,19 +139,20 @@ def lock_account(username):
try:
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
dn = result[0]
uac = result[1]['userAccountControl'] | 0x002 # set ACCOUNTDISABLE
actions = [
(ldap.MOD_REPLACE, "userAccountControl", uac),
(ldap.MOD_REPLACE, "userShell", "/etc/locked" + str(today.year))
]
dn = result.entry_dn
uac = result['userAccountControl'] | 0x002 # set ACCOUNTDISABLE
actions = {
"userAccountControl": [(MODIFY_REPLACE,[uac])],
"userShell": [(MODIFY_REPLACE,["/etc/locked"+str(today.year)])]
}
# write record
ld.modify(dn, actions)
except LDAPOperationResult:
raise
finally:
ld.unbind()
reason = "account locked by uccportal on %s" % str(today)
dispense.set_dispense_flag(username, 'disabled', reason)
......@@ -140,31 +163,108 @@ def unlock_account(username):
try:
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
dn = result[0]
uac = result[1]['userAccountControl'] & ~0x002 # clear ACCOUNTDISABLE
actions = [
(ldap.MOD_REPLACE, "userAccountControl",uac),
(ldap.MOD_REPLACE, "userShell", "/bin/zsh")
]
actions = {
"userAccountControl": [(MODIFY_REPLACE,[uac])],
"userShell": [(MODIFY_REPLACE,["/bin/zsh"])]
}
# write record
ld.modify(dn, actions)
except LDAPOperationResult:
raise
finally:
ld.unbind()
reason = "account unlocked by uccportal on %s" % str(today)
dispense.set_dispense_flag(username, '!disabled', reason)
# Account creation steps:
#
def create_account(member, passwd):
username = "changeme";
log.info("I: creating new account for %s (%s %s)")
#
def create_ad_user(form_data, member):
log.info("I: creating new account for %s (%s)")
# store user details
# TODO add overides
username=form_data['username']
displayName = member.first_name + ' ' + member.last_name
dn = 'CN=' + username +','+ ldap_user_dn
# enclose password in quotes and convert to utf16 as required:
# https://msdn.microsoft.com/en-us/library/cc223248.aspx
quotedpass = '"'+ form_data['password']+'"'
utf16pass = quotedpass.encode('utf-16-le')
# generate uid
try:
result = get_maxuid()
except:
log.error("LDAP: cannot find base uid")
return False
maxuid = int(result.msSFU30MaxUidNumber.value)
# gets all uids >= maxuid
# this is done so that we don't encounter the 1000 item limit to ad queries
entries = get_ldap_attrs(ldap_user_dn,"(uidNumber>=%s)" % maxuid, 100, ['uidNumber'])
# generate a new uid
uids = []
for user in entries:
uids.append(int(user.uidNumber.value))
uids.sort()
# use max uid if it is free
if uids[0] != maxuid:
newuid = str(maxuid)
else:
prev = uids[0]
for uid in uids:
if uid - prev > 1:
newuid = uid + 1
break;
prev = uid
#increment uid
newuid = str(prev + 1)
# sanity check: make sure the uid is free
if subprocess.call(["id", newuid], stderr=subprocess.DEVNULL) == 0:
log.error("LDAP: uid already taken")
return False
# create the new user struct
objclass = ['top','posixAccount','person','organizationalPerson','user']
attrs = {
'cn' : username,
'sAMAccountName' : username,
'givenName' : member.first_name,
'sn': member.last_name,
'displayName': displayName,
'userAccountControl' : '512',
'unixHomeDirectory' : "/home/ucc/" + username,
'loginShell' : '/bin/zsh',
'gidNumber' : '20021',
'uidNumber' : newuid,
'gecos' : displayName,
'mail' : username + '@ucc.gu.uwa.edu.au',
'unicodePwd': utf16pass
}
# commit the new user to AD
ld = get_ldap_instance()
result = ld.add(dn, objclass, attrs)
if not result:
log.error("LDAP: user add failed")
return False
# set maxuid
result = ld.modify(maxuid_dn, {'msSFU30MaxUidNumber': [(MODIFY_REPLACE, newuid)]})
if not result:
log.warning("LDAP: user created but msSFU30MaxUidNumber not update")
ld.unbind();
return True;
return None;
def create_homes(member):
return
def set_email_forwarding(member, addr):
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment