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

Merge branch 'coffee-testing' into 'master'

Coffee testing

See merge request frekk/uccportal!1
parents 9fc650e8 528c2d2a
certifi==2018.11.29
Django==2.1.5
django-auth-ldap==1.7.0
django-formtools==2.1
django-sslserver==0.20
psycopg2-binary==2.7.6.1
pyasn1==0.4.4
......
default_app_config = 'gms.apps.GmsConfig'
......@@ -11,29 +11,33 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
# import local settings
from gms.settings_local import *
# DEBUG ALLOW
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'
......@@ -63,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
......@@ -100,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'],
}
},
}
......@@ -44,6 +44,11 @@ LOG_FILENAME = os.path.join(ROOT_DIR, "django.log")
import ldap
from django_auth_ldap.config import LDAPSearch, ActiveDirectoryGroupType, LDAPGroupQuery
# LDAP admin settings
LDAP_USER_SEARCH_DN = 'CN=Users,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au'
LDAP_BIND_DN = ""
LDAP_BIND_SECRET = ""
# this could be ad.ucc.gu.uwa.edu.au but that doesn't resolve externally -
# useful for testing, but should be changed in production so failover works
AUTH_LDAP_SERVER_URI = 'ldaps://samson.ucc.gu.uwa.edu.au/'
......
wsgi.wsgi
\ No newline at end of file
"""
WSGI config for gms project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gms.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
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, MyForm
from .views import MyUpdateView, MyWizardView
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,
strip=False,
help_text="Password must be between 10 and 127 characters long"
)
confirm_password = forms.CharField(
min_length=10,
max_length=127,
widget=forms.PasswordInput,
strip=False,
)
class Meta:
model = Member
fields = ['username']
def clean(self):
try:
if (self['password'].value() != self['confirm_password'].value()):
self.add_error('confirm_password', 'Passwords must match.')
except:
pass
super().clean();
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 (len(self['email_address'].value()) == 0):
self.add_error('email_address', 'Email field cannot be left blank.')
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,
required=False,
help_text="PIN must be 4 digits long")
confirm_pin = forms.CharField(
min_length=0,
max_length=4,
widget=forms.PasswordInput,
required=False,
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(MyWizardView):
form_list = [AccountForm,EmailForm,DispenseForm]
template_name = 'admin/memberdb/account_create.html'
admin = None
def get_form_instance(self, step):
return self.object
def get_context_data(self, **kwargs):
m = self.object
context = super().get_context_data(**kwargs)
context.update(self.admin.admin_site.each_context(self.request))
context.update({
'opts': self.admin.model._meta,
'member': m,
})
return context
def done(self, form_list, form_dict, **kwargs):
# create the user and save their username if successfull
try:
if create_ad_user(self.get_cleaned_data_for_step('0'), self.object):
form_dict['0'].save()
make_home(self.get_cleaned_data_for_step('1'), self.object)
make_dispense_account(self.object.username, self.get_cleaned_data_for_step('2')['pin'])
subscribe_to_list(self.object)
except Exception as e:
messages.error(self.request,'Account creation failed for %s', self.object)
messages.error(self.request, e)
raise #DEBUG
else:
messages.success(self.request, 'An account has been successfully created for %s.' % self.object)
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)
import logging
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.translation import gettext_lazy as _
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
import memberdb.models
from datetime import date
from squarepay import dispense
import shutil
import os
log = logging.getLogger('ldap')
# load config
ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI')
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')
make_home_cmd = ["sudo", "python3", "root_actions.py"]
make_mail_cmd = 'ssh -i %s [email protected] "/usr/local/mailman/bin/add_members" -r- ucc-announce <<< %[email protected]'
make_mail_key = './mooneye.key'
maxuid_dn = "CN=uccdomayne,CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System,"+ldap_base_dn
#initalise ldap instace
_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():
if not _ldap_inst.bound:
try:
_ldap_inst.bind()
except LDAPInvalidCredentialsResult:
log.error("LDAP: Invalid bind credentials")
raise
return _ldap_inst
def get_ldap_attrs(dn, filter, limit, attrs):
ld = get_ldap_instance()
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'])
# 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'))
# ensure username is lowercase
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], stderr=subprocess.DEVNULL) == 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:
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:
# 1. set UAC ACCOUNTDISABLE flag (0x002) via ldap
# 2. set user shell to `/etc/locked20xx` via ldap
# 3. do `dispense user type disabled <username> <reason>`
def lock_account(username):
# TODO: error handling
ld = get_ldap_instance()
today = date.today()
try:
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
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)
def unlock_account(username):
# TODO: error handling
ld = get_ldap_instance()
today = date.today()
try:
# fetch current uac
result = get_user_attrs(username, ['userAccountControl'])
dn = result[0]
uac = result[1]['userAccountControl'] & ~0x002 # clear ACCOUNTDISABLE
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
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")
raise
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))