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

added account (un)locking actions

parent a2dfa2b1
import logging
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import ldap
from datetime import date
from squarepay import dispense
log = logging.getLogger('ldap')
# load config
ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI')
ldap_search_dn = getattr(settings, 'REPLACE_ME')
ldap_bind_dn = getattr()
ldap_bind_secret = getattr()
#initalise ldap instace
_ldap_inst = ldap.initialize(ldap_uri)
def get_ldap_instance():
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];
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)
# 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:
ld.bind(ldap_bind_dn, ldap_bind_secret)
# 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))
]
# write record
ld.modify(dn, actions)
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:
ld.bind(ldap_bind_dn, ldap_bind_secret)
# 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")
]
# write record
ld.modify(dn, actions)
finally:
ld.unbind()
reason = "account unlocked by uccportal on %s" % str(today)
dispense.set_dispense_flag(username, '!disabled', reason)
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()
......@@ -6,200 +6,211 @@ from django.urls import reverse
from squarepay.dispense import get_item_price
import ldap
"""
dictionary of membership types & descriptions, should be updated if these are changed in dispense.
"""
MEMBERSHIP_TYPES = {
'oday': {
'dispense':'pseudo:11',
'desc':'O\' Day Special - first time members only',
'is_guild':True,
'is_student':True,
'must_be_fresh':True,
},
'student_and_guild': {
'dispense':'pseudo:10',
'desc':'Student and UWA Guild member',
'is_guild':True,
'is_student':True,
'must_be_fresh':False,
},
'student_only': {
'dispense':'pseudo:9',
'desc':'Student and not UWA Guild member',
'is_guild':False,
'is_student':True,
'must_be_fresh':False,
},
'guild_only': {
'dispense':'pseudo:8',
'desc':'Non-Student and UWA Guild member',
'is_guild':True,
'is_student':False,
'must_be_fresh':False,
},
'non_student': {
'dispense':'pseudo:7',
'desc':'Non-Student and not UWA Guild member',
'is_guild':False,
'is_student':False,
'must_be_fresh':False,
},
'lifer': {
'dispense':'',
'desc':'Life member',
'is_guild':False,
'is_student':False,
'must_be_fresh':False,
}
'oday': {
'dispense':'pseudo:11',
'desc':'O\' Day Special - first time members only',
'is_guild':True,
'is_student':True,
'must_be_fresh':True,
},
'student_and_guild': {
'dispense':'pseudo:10',
'desc':'Student and UWA Guild member',
'is_guild':True,
'is_student':True,
'must_be_fresh':False,
},
'student_only': {
'dispense':'pseudo:9',
'desc':'Student and not UWA Guild member',
'is_guild':False,
'is_student':True,
'must_be_fresh':False,
},
'guild_only': {
'dispense':'pseudo:8',
'desc':'Non-Student and UWA Guild member',
'is_guild':True,
'is_student':False,
'must_be_fresh':False,
},
'non_student': {
'dispense':'pseudo:7',
'desc':'Non-Student and not UWA Guild member',
'is_guild':False,
'is_student':False,
'must_be_fresh':False,
},
'lifer': {
'dispense':'',
'desc':'Life member',
'is_guild':False,
'is_student':False,
'must_be_fresh':False,
}
}
def get_membership_choices(is_renew=None, get_prices=True):
"""
turn MEMBERSHIP_TYPES into a list of choices used by Django
also dynamically fetch the prices from dispense (if possible)
"""
choices = []
for key, val in MEMBERSHIP_TYPES.items():
if (val['must_be_fresh'] and is_renew == True):
# if you have an account already, you don't qualify for the fresher special
continue
if (val['dispense'] == '' and is_renew == False):
# free memberships can only apply to life members, and they will have an existing membership somewhere
# so this option is only displayed on the renewal form
continue
else:
if get_prices:
price = get_item_price(val['dispense'])
else:
price = None
if price is not None:
desc = "%s ($%1.2f)" % (val['desc'], price / 100.0)
choices += [(key, desc)]
else:
# don't display the price
choices += [(key, val['desc'])]
return choices
"""
turn MEMBERSHIP_TYPES into a list of choices used by Django
also dynamically fetch the prices from dispense (if possible)
"""
choices = []
for key, val in MEMBERSHIP_TYPES.items():
if (val['must_be_fresh'] and is_renew == True):
# if you have an account already, you don't qualify for the fresher special
continue
if (val['dispense'] == '' and is_renew == False):
# free memberships can only apply to life members, and they will have an existing membership somewhere
# so this option is only displayed on the renewal form
continue
else:
if get_prices:
price = get_item_price(val['dispense'])
else:
price = None
if price is not None:
desc = "%s ($%1.2f)" % (val['desc'], price / 100.0)
choices += [(key, desc)]
else:
# don't display the price
choices += [(key, val['desc'])]
return choices
def get_membership_type(member):
best = 'non_student'
is_fresh = member.memberships.all().count() == 0
for i, t in MEMBERSHIP_TYPES.items():
if (t['must_be_fresh'] == is_fresh and t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
best = i
break
elif (t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
best = i
break
return best
best = 'non_student'
is_fresh = member.memberships.all().count() == 0
for i, t in MEMBERSHIP_TYPES.items():
if (t['must_be_fresh'] == is_fresh and t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
best = i
break
elif (t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
best = i
break
return best
def make_token():
return get_random_string(128)
return get_random_string(128)
PAYMENT_METHODS = [
('dispense', 'Existing dispense credit'),
('cash', 'Cash (in person)'),
('card', 'Tap-n-Go via Square (in person)'),
('online', 'Online payment via Square'),
('eft', 'Bank transfer'),
('', 'No payment')
('dispense', 'Existing dispense credit'),
('cash', 'Cash (in person)'),
('card', 'Tap-n-Go via Square (in person)'),
('online', 'Online payment via Square'),
('eft', 'Bank transfer'),
('', 'No payment')
]
class IncAssocMember (models.Model):
"""
Member record for data we are legally required to keep under Incorporations Act (and make available to members upon request)
Note: these data should only be changed administratively or with suitable validation since it must be up to date & accurate.
"""
first_name = models.CharField ('First name', max_length=200)
last_name = models.CharField ('Surname', max_length=200)
email_address = models.EmailField ('Email address', blank=False)
updated = models.DateTimeField ('IncA. info last updated', auto_now=True)
created = models.DateTimeField ('When created', auto_now_add=True)
def __str__ (self):
return "%s %s <%s>" % (self.first_name, self.last_name, self.email_address)
class Meta:
verbose_name = "Incorporations Act member data"
verbose_name_plural = verbose_name
"""
Member record for data we are legally required to keep under Incorporations Act (and make available to members upon request)
Note: these data should only be changed administratively or with suitable validation since it must be up to date & accurate.
"""
first_name = models.CharField ('First name', max_length=200)
last_name = models.CharField ('Surname', max_length=200)
email_address = models.EmailField ('Email address', blank=False)
updated = models.DateTimeField ('IncA. info last updated', auto_now=True)
created = models.DateTimeField ('When created', auto_now_add=True)
def __str__ (self):
return "%s %s <%s>" % (self.first_name, self.last_name, self.email_address)
class Meta:
verbose_name = "Incorporations Act member data"
verbose_name_plural = verbose_name
class Member (IncAssocMember):
"""
Member table: only latest information, one record per member
Some of this data may be required by the UWA Student Guild. Other stuff is just good to know,
and we don't _need_ to keep historical data for every current/past member.
Note: Privacy laws are a thing, unless people allow it then we cannot provide this info to members.
"""
# data to be entered by user and validated (mostly) manually
display_name = models.CharField ('Display name', max_length=200)
username = models.SlugField ('Username', max_length=32, null=False, blank=False, unique=True, validators=[RegexValidator(regex='^[a-z0-9._-]+$')])
phone_number = models.CharField ('Phone number', max_length=20, blank=False, validators=[RegexValidator(regex='^\+?[0-9() -]+$')])
is_student = models.BooleanField ('Student', default=True, blank=True, help_text="Tick this box if you are a current student at a secondary or tertiary institution in WA")
is_guild = models.BooleanField ('UWA Guild member', default=True, blank=True)
id_number = models.CharField ('Student email or Drivers License', max_length=255, blank=False, help_text="Student emails should end with '@student.*.edu.au' and drivers licences should be in the format '<AU state> 1234567'")
# data used internally by the system, not to be touched, seen or heard (except when it is)
member_updated = models.DateTimeField ('Internal UCC info last updated', auto_now=True)
login_token = models.CharField ('Temporary access key', max_length=128, null=True, editable=False, default=make_token)
email_confirm = models.BooleanField ('Email address confirmed', null=False, editable=False, default=False)
studnt_confirm = models.BooleanField ('Student status confirmed', null=False, editable=False, default=False)
guild_confirm = models.BooleanField ('Guild status confirmed', null=False, editable=False, default=False)
def __str__ (self):
if (self.display_name != "%s %s" % (self.first_name, self.last_name)):
name = "%s (%s %s)" % (self.display_name, self.first_name, self.last_name)
else:
name = self.display_name
return "[%s] %s" % (self.username, name)
class Meta:
verbose_name = "Internal UCC member record"
"""
Member table: only latest information, one record per member
Some of this data may be required by the UWA Student Guild. Other stuff is just good to know,
and we don't _need_ to keep historical data for every current/past member.
Note: Privacy laws are a thing, unless people allow it then we cannot provide this info to members.
"""
# data to be entered by user and validated (mostly) manually
display_name = models.CharField ('Display name', max_length=200)
username = models.SlugField ('Username', max_length=32, null=False, blank=False, unique=True, validators=[RegexValidator(regex='^[a-z0-9._-]+$')])
phone_number = models.CharField ('Phone number', max_length=20, blank=False, validators=[RegexValidator(regex='^\+?[0-9() -]+$')])
is_student = models.BooleanField ('Student', default=True, blank=True, help_text="Tick this box if you are a current student at a secondary or tertiary institution in WA")
is_guild = models.BooleanField ('UWA Guild member', default=True, blank=True)
id_number = models.CharField ('Student email or Drivers License', max_length=255, blank=False, help_text="Student emails should end with '@student.*.edu.au' and drivers licences should be in the format '<AU state> 1234567'")
# data used internally by the system, not to be touched, seen or heard (except when it is)
member_updated = models.DateTimeField ('Internal UCC info last updated', auto_now=True)
login_token = models.CharField ('Temporary access key', max_length=128, null=True, editable=False, default=make_token)
email_confirm = models.BooleanField ('Email address confirmed', null=False, editable=False, default=False)
studnt_confirm = models.BooleanField ('Student status confirmed', null=False, editable=False, default=False)
guild_confirm = models.BooleanField ('Guild status confirmed', null=False, editable=False, default=False)
# account info
def get_account_status(self):
return;
def __str__ (self):
if (self.display_name != "%s %s" % (self.first_name, self.last_name)):
name = "%s (%s %s)" % (self.display_name, self.first_name, self.last_name)
else:
name = self.display_name
return "[%s] %s" % (self.username, name)
class Meta:
verbose_name = "Internal UCC member record"
class Membership (models.Model):
"""
Membership table: store information related to individual (successful/accepted) signups/renewals
"""
"""
Membership table: store information related to individual (successful/accepted) signups/renewals
"""
member = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='memberships')
membership_type = models.CharField ('Membership type', max_length=20, blank=True, null=False, choices=get_membership_choices(get_prices=False))
payment_method = models.CharField ('Payment method', max_length=10, blank=True, null=True, choices=PAYMENT_METHODS, default=None)
approved = models.BooleanField ('Membership approved', default=False)
approver = models.ForeignKey (Member, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_memberships')
date_submitted = models.DateTimeField ('Date signed up')
date_paid = models.DateTimeField ('Date of payment', blank=True, null=True)
date_approved = models.DateTimeField ('Date approved', blank=True, null=True)
member = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='memberships')
membership_type = models.CharField ('Membership type', max_length=20, blank=True, null=False, choices=get_membership_choices(get_prices=False))
payment_method = models.CharField ('Payment method', max_length=10, blank=True, null=True, choices=PAYMENT_METHODS, default=None)
approved = models.BooleanField ('Membership approved', default=False)
approver = models.ForeignKey (Member, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_memberships')
date_submitted = models.DateTimeField ('Date signed up')
date_paid = models.DateTimeField ('Date of payment', blank=True, null=True)
date_approved = models.DateTimeField ('Date approved', blank=True, null=True)
def __str__ (self):
return "Member [%s] (%s) renewed membership on %s" % (self.member.username, self.member.display_name, self.date_submitted.strftime("%Y-%m-%d"))
def __str__ (self):
return "Member [%s] (%s) renewed membership on %s" % (self.member.username, self.member.display_name, self.date_submitted.strftime("%Y-%m-%d"))
def get_dispense_item(self):
return MEMBERSHIP_TYPES[self.membership_type]['dispense']
def get_dispense_item(self):
return MEMBERSHIP_TYPES[self.membership_type]['dispense']
class Meta:
verbose_name = "Membership renewal record"
ordering = ['approved', '-date_submitted']
class Meta:
verbose_name = "Membership renewal record"
ordering = ['approved', '-date_submitted']
class TokenConfirmation(models.Model):
""" keep track of email confirmation tokens etc. and which field to update """
member = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='token_confirmations')
confirm_token = models.CharField ('unique confirmation URL token', max_length=128, null=False, default=make_token)
model_field = models.CharField ('name of BooleanField to update on parent when confirmed', max_length=40, null=False, blank=False)
created = models.DateTimeField ('creation date', auto_now_add=True)
def mark_confirmed(self):
""" try to mark as confirmed, if error then silently fail """
try:
m = self.member
setattr(m, self.model_field)
m.save()
self.delete()
except Member.DoesNotExist as e:
pass
def get_absolute_url(self):
return reverse('memberdb:email_confirm', kwargs={'pk': self.id, 'token': self.confirm_token})
""" keep track of email confirmation tokens etc. and which field to update """
member = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='token_confirmations')
confirm_token = models.CharField ('unique confirmation URL token', max_length=128, null=False, default=make_token)
model_field = models.CharField ('name of BooleanField to update on parent when confirmed', max_length=40, null=False, blank=False)
created = models.DateTimeField ('creation date', auto_now_add=True)
def mark_confirmed(self):
""" try to mark as confirmed, if error then silently fail """
try:
m = self.member
setattr(m, self.model_field)
m.save()
self.delete()
except Member.DoesNotExist as e:
pass
def get_absolute_url(self):
return reverse('memberdb:email_confirm', kwargs={'pk': self.id, 'token': self.confirm_token})
......@@ -12,40 +12,54 @@ from .payments import log
DISPENSE_BIN = getattr(settings, 'DISPENSE_BIN', None)
if DISPENSE_BIN is None:
log.warning("DISPENSE_BIN is not defined! Lookups for prices will fallback to weird numbers (for testing)!")
log.warning("DISPENSE_BIN is not defined! Lookups for prices will fallback to weird numbers (for testing)!")
def run_dispense(*args):
if DISPENSE_BIN is None:
return None
cmd = [DISPENSE_BIN] + args
log.info("run_dispense: " + cmd)
try:
# get a string containing the output of the program
res = subprocess.check_output(cmd, timeout=4, universal_newlines=True)
except CalledProcessError as e:
log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output))
return None
except TimeoutExpired as e:
log.error(e)
return None
return res
if DISPENSE_BIN is None:
return None
cmd = [DISPENSE_BIN] + args
log.info("run_dispense: " + cmd)
try:
# get a string containing the output of the program
res = subprocess.check_output(cmd, timeout=4, universal_newlines=True)
except CalledProcessError as e:
log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output))
return None
except TimeoutExpired as e:
log.error(e)
return None
return res
def get_item_price(itemid):
""" gets the price of the given dispense item in cents """
if (itemid is None or itemid == ""):
return None
if DISPENSE_BIN is None:
return 2223
out = run_dispense('iteminfo', itemid)
if out is None:
return None
s = out.split() # get something like ['pseudo:7', '25.00', 'membership', '(non-student', 'and', 'non-guild)']
if (s[0] != itemid):
log.warning("get_item_price: got result for incorrect item: %s" + s)
return None
else:
# return the price as a number of cents
return int(float(s[0]) * 100)
""" gets the price of the given dispense item in cents """
if (itemid is None or itemid == ""):
return None
if DISPENSE_BIN is None:
return 2223
out = run_dispense('iteminfo', itemid)
if out is None:
return None
s = out.split() # get something like ['pseudo:7', '25.00', 'membership', '(non-student', 'and', 'non-guild)']
if (s[0] != itemid):
log.warning("get_item_price: got result for incorrect item: %s" + s)
return None
else:
# return the price as a number of cents
return int(float(s[0]) * 100)
def set_dispense_flag(user, flag, reason):
if DISPENSE_BIN is None:
log.warning("DISPENSE_BIN is not defined, user will not be disabled")
return False
cmd = [DISPENSE_BIN] + args
out = run_dispense('user', 'type', user, flag, reason)
s = out.split()
if s[2] != "updated":
# user was not updated
log.warning("set_dispense_flag: user was not updated with error: " + out)
return False;
return True;
......@@ -4,4 +4,5 @@
{% if not ms.approved %}
<a class="button" href="{{ member_approve }}">Approve</a>&nbsp;
{% endif %}
<a class="button" href="{{ account_create }}">Create Account</a>&nbsp;
</div>