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

Working account creation form, no backend implemented

parent ad20e053
......@@ -17,26 +17,27 @@ ALLOWED_HOSTS = ['127.0.0.1', 'localhost', "130.95.13.36"]
# Application definition
INSTALLED_APPS = (
'sslserver',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'memberdb',
'import_members',
'squarepay',
'sslserver',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'memberdb',
'import_members',
'squarepay',
'formtools'
)
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'memberdb.views.MemberMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'memberdb.views.MemberMiddleware',
]
ROOT_URLCONF = 'gms.urls'
......@@ -66,33 +67,33 @@ DATABASE_ROUTERS = ['import_members.db.MemberDbRouter']
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'static'),
]
STATIC_URL = '/media/'
STATIC_ROOT = os.path.join(ROOT_DIR, 'media')
AUTHENTICATION_BACKENDS = [
# see https://django-auth-ldap.readthedocs.io/en/latest for configuration info
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
# see https://django-auth-ldap.readthedocs.io/en/latest for configuration info
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
]
# see settings_local.py for LDAP settings
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
TEMPLATE_DEBUG = DEBUG
......@@ -103,51 +104,51 @@ MESSAGE_LEVEL = message_constants.DEBUG
### Logging configuration ###
import logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt' : "%d/%b/%Y %H:%M:%S"
},
},
'handlers': {
'logfile': {
'level': LOG_LEVEL,
'class':'logging.handlers.RotatingFileHandler',
'filename': LOG_FILENAME,
'maxBytes': 500000,
'backupCount': 2,
'formatter': 'standard',
},
'console':{
'level': LOG_LEVEL,
'class':'logging.StreamHandler',
'formatter': 'standard'
},
},
'loggers': {
'django': {
'handlers':['logfile', 'console'],
'propagate': True,
'level': LOG_LEVEL,
},
'django.db.backends': {
'handlers': ['logfile', 'console'],
'level': LOG_LEVEL,
'propagate': False,
},
'django.contrib.auth': {
'handlers': ['logfile', 'console'],
'level': LOG_LEVEL,
},
'django_auth_ldap': {
'level': LOG_LEVEL,
'handlers': ['logfile', 'console'],
},
'squarepay': {
'level': LOG_LEVEL,
'handlers': ['logfile', 'console'],
}
},
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt' : "%d/%b/%Y %H:%M:%S"
},
},
'handlers': {
'logfile': {
'level': LOG_LEVEL,
'class':'logging.handlers.RotatingFileHandler',
'filename': LOG_FILENAME,
'maxBytes': 500000,
'backupCount': 2,
'formatter': 'standard',
},
'console':{
'level': LOG_LEVEL,
'class':'logging.StreamHandler',
'formatter': 'standard'
},
},
'loggers': {
'django': {
'handlers':['logfile', 'console'],
'propagate': True,
'level': LOG_LEVEL,
},
'django.db.backends': {
'handlers': ['logfile', 'console'],
'level': LOG_LEVEL,
'propagate': False,
},
'django.contrib.auth': {
'handlers': ['logfile', 'console'],
'level': LOG_LEVEL,
},
'django_auth_ldap': {
'level': LOG_LEVEL,
'handlers': ['logfile', 'console'],
},
'squarepay': {
'level': LOG_LEVEL,
'handlers': ['logfile', 'console'],
}
},
}
......@@ -2,32 +2,31 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.utils import timezone
from django.contrib import messages
from django import forms
from formtools.wizard.views import SessionWizardView
from .models import Member
from .forms import MyModelForm
from .views import MyUpdateView
from .forms import MyModelForm, MyForm
from .views import MyUpdateView, MyWizardView
from memberdb.account_backend import validate_username
class AccountForm(MyModelForm):
# form fields
user= forms.SlugField(
validators=[validate_username]
)
forward_email = forms.EmailField(
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"
username = forms.SlugField(
validators=[validate_username],
max_length=19,
)
password = forms.CharField(
min_length=10,
max_length=127,
widget=forms.PasswordInput,
strip=False,
help_text="Password must be between 10 and 127 characters long")
help_text="Password must be between 10 and 127 characters long"
)
confirm_password = forms.CharField(
min_length=10,
max_length=127,
......@@ -35,41 +34,79 @@ class AccountForm(MyModelForm):
strip=False,
)
class Meta:
model = Member
fields = ['first_name']
error_messages = {
'username': {
'unique': 'This username is already taken, please pick another one.',
'invalid': 'Please pick a username with only lowercase letters and numbers'
}
}
fields = ['username']
def clean(self):
try:
user.clean()
if (self['password'].value() != self['confirm_password'].value()):
self.add_error('confirm_password', 'Passwords must match.')
if (self['forward_email'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]):
self.add_error('forward_email', 'Forwarding address cannot be the same as your account address.')
except:
pass
super().clean();
def save(self):
return
class EmailForm(MyModelForm):
forward = forms.BooleanField(required=False)
email_address = forms.EmailField(
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"
)
class Meta:
model = Member
fields = ['forward', 'email_address']
def clean(self):
if self['forward'].value() == True:
try:
if (self['email_address'].value().split('@')[1] in ["ucc.asn.au", "ucc.gu.uwa.edu.au"]):
self.add_error('email_address', 'Forwarding address cannot be the same as your account address.')
except:
pass
super().clean();
class DispenseForm(MyForm):
pin = forms.CharField(
min_length=0,
max_length=4,
widget=forms.PasswordInput,
strip=False,
help_text="PIN must be 4 digits long")
confirm_pin = forms.CharField(
min_length=0,
max_length=4,
widget=forms.PasswordInput,
strip=False,
)
def clean(self):
try:
if len(self['pin'].value()) != 4 :
self.add_error('pin', 'PIN must be excatly 4 digits.')
if not self['pin'].value().isdigit():
self.add_error('pin', 'PIN can only contain numbers.')
if (self['pin'].value() != self['confirm_pin'].value()):
self.add_error('confirm_pin', 'PINs must match.')
except:
pass
super().clean();
class AccountView(MyUpdateView):
class AccountView(MyWizardView):
form_list = [AccountForm,EmailForm,DispenseForm]
template_name = 'admin/memberdb/account_create.html'
form_class = AccountForm
model = Member
pk_url_kwarg = 'object_id'
admin = None
def get_form_instance(self, step):
return self.object
def get_context_data(self, **kwargs):
m = self.get_object()
m = self.object
context = super().get_context_data(**kwargs)
context.update(self.admin.admin_site.each_context(self.request))
context.update({
......@@ -78,7 +115,19 @@ class AccountView(MyUpdateView):
})
return context
def form_valid(self, form):
m, ms = form.save()
messages.success(self.request, 'Your membership renewal has been submitted.')
return HttpResponseRedirect(reverse("admin:memberdb_membership_summary"))
def done(self, form_list, form_dict, **kwargs):
messages.success(self.request, 'Your membership renewal has been submitted.')
return HttpResponseRedirect(reverse("admin:memberdb_membership_changelist"))
#return accountProgressView(self.request, m)
def accountProgressView(request, member):
return
def accountFinalView():
return render(request, 'accountfinal.html', context)
......@@ -7,6 +7,11 @@ from django.utils.translation import gettext_lazy as _
import ldap
import re
import socket
import subprocess
from subprocess import CalledProcessError, TimeoutExpired
import memberdb.models
from datetime import date
from squarepay import dispense
......@@ -16,53 +21,90 @@ log = logging.getLogger('ldap')
# load config
ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI')
ldap_search_dn = getattr(settings, 'AUTH_LDAP_USER_DN_TEMPLATE')
#ldap_bind_dn = getattr()
#ldap_bind_secret = getattr()
ldap_search_dn = getattr(settings, 'LDAP_USER_SEARCH_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')
#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)
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")
return _ldap_inst
def get_user_attrs(username, attrs):
# TODO verify bind
ld = get_ldap_instance()
filter = "cn=" + username
try:
result = ld.search_s(ldap_search_dn, ldap.SCOPE_SUBTREE, filter, attrs)
if result.size > 1:
# multiple accounts matched, this is a problem
return None
if result.size == 0:
# account does not exist
return None
return result[0];
except:
return None
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];
def get_account_lock_status(username):
ld = get_ldap_instance()
try:
ld.bind(ldap_bind_dn, ldap_bind_secret)
result = get_user_attrs(username, ['userAccountControl'])
finally:
ld.unbind()
return bool(result[1]['userAccountControl'] & 0x002)
def validate_username(value):
def validate_username(value : str):
# note: slug validator ensures that username only contains [a-z0-9_-]
# usernames can't begin with a numeric
if re.match(r"^\d.*", value):
log.info("test")
if not value[0].isalpha():
raise ValidationError(
_('Username must begin with a letter')
)
# ensure username is lowercase
elif not value.islower():
raise ValidationError(
_('Username cannot begin with a number'),
params={'value': value}
_('Username cannot contain uppercase characters')
)
else:
return value
# check if the user exists, this test should catch *most* cases
if subprocess.call(["id", value]) == 0:
raise ValidationError(_('Username already taken (passwd)'))
# usernames cannot conflict with hostnames
try:
socket.gethostbyname(value)
raise ValidationError(
_('Username already taken (CNAME)')
)
except socket.gaierror:
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
# locks the specified User Account by performing the following actions:
# 1. set UAC ACCOUNTDISABLE flag (0x002) via ldap
......@@ -73,7 +115,6 @@ def lock_account(username):
ld = get_ldap_instance()
today = date.today()
try:
ld.bind(ldap_bind_dn, ldap_bind_secret)
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
......@@ -97,7 +138,6 @@ def unlock_account(username):
ld = get_ldap_instance()
today = date.today()
try:
ld.bind(ldap_bind_dn, ldap_bind_secret)
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
......@@ -118,24 +158,21 @@ def unlock_account(username):
# Account creation steps:
#
def create_account(member):
def create_account(member, passwd):
username = "changeme";
log.info("I: creating new account for %s (%s %s)")
# prepend student numbers with 'sn'
if re.fullmatch(r"^2\d{7}$", username):
log.info("I: username is a student number, adding sn prefix")
username = sn + username
# usernames can't begin with a numeric
if re.match(r"^\d", username):
log.error("E: The username %s cannot start with a digit." % username)
return;
return None;
def create_homes(member):
return
def set_email_forwarding(member, addr):
return
def subscribe_to_list(member):
return
def set_pin(member, pin):
return
......@@ -73,8 +73,14 @@ class MemberAdmin(admin.ModelAdmin):
extra_context['incassocmember_url'] = get_model_url(object_id, 'incassocmember')
return super().change_view(request, object_id, form_url, extra_context=extra_context)
def process_account(self, request, *args, **kwargs):
return AccountView.as_view(admin=self)(request, *args, **kwargs)
inst = Member.objects.get(pk=kwargs['object_id'])
model_dict = {
'0': inst,
'1': inst
}
return AccountView.as_view(object=inst,admin=self)(request, *args, **kwargs)
......
......@@ -9,6 +9,13 @@ class MyModelForm(forms.ModelForm):
# this must be passed by kwargs upon instantiating the form
request = None
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
class MyForm(forms.Form):
# this must be passed by kwargs upon instantiating the form
request = None
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
......
......@@ -9,6 +9,7 @@ from django.views.generic.base import View
from django.views.generic.edit import UpdateView
from django.contrib.auth.mixins import AccessMixin
from django.utils import timezone
from formtools.wizard.views import SessionWizardView
from .models import Member, IncAssocMember, Membership, MEMBERSHIP_TYPES, TokenConfirmation
from .forms import MemberHomeForm
......@@ -80,6 +81,25 @@ class MyUpdateView(UpdateView):
kwargs.update({'request': self.request})
return kwargs
class MyWizardView(SessionWizardView):
object = None
def get_object(self):
if (not self.object is None):
return self.object
try:
sobj = super().get_object()
if (not sobj is None):
return sobj
except:
pass
return None
def get_form_kwargs(self, step, **kwargs):
kwargs.update(super().get_form_kwargs())
kwargs.update({'request': self.request})
return kwargs
class MemberHomeView(MemberAccessMixin, MyUpdateView):
model = Member
template_name = 'home.html'
......
var conditional_fields = $("div");
\ No newline at end of file
var conditional_fields = $("#1-email_address");
if (!$("#id_1-forward").prop('checked') === true) {
conditional_fields.hide();
}
$("#id_1-forward").change(function() {
if ($(this).prop('checked') === true) {
conditional_fields.show();
} else {
conditional_fields.hide();
}
});
\ No newline at end of file
......@@ -115,7 +115,7 @@ ul.messagelist li.error {
/* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input, a.button {
.button, input[type=submit], input[type=button], .submit-row input,.submit-row button, a.button {
background: #79aec8;