From 3462e2865c1feea446ba52abd5e84d1f8a1cd9c1 Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Mon, 16 Feb 2015 23:09:52 +0100 Subject: [PATCH] Major work - Django 1.7 and other mode upgrades - Move code for each service to it's own app - Password decrypt should now work (RIJNDAEL-256 !#%!@) --- neuf_kerberos/__init__.py | 0 neuf_kerberos/admin.py | 3 + neuf_kerberos/migrations/__init__.py | 0 neuf_kerberos/models.py | 3 + neuf_kerberos/tests.py | 3 + neuf_kerberos/utils.py | 54 +++ neuf_kerberos/views.py | 20 + neuf_ldap/__init__.py | 0 neuf_ldap/admin.py | 17 + neuf_ldap/migrations/__init__.py | 0 neuf_ldap/models.py | 70 ++++ neuf_ldap/tests.py | 3 + neuf_userinfo/passwd.py => neuf_ldap/utils.py | 23 +- neuf_ldap/views.py | 3 + neuf_radius/__init__.py | 0 neuf_radius/admin.py | 3 + neuf_radius/migrations/__init__.py | 0 neuf_radius/models.py | 106 +++++ neuf_radius/tests.py | 3 + neuf_radius/utils.py | 28 ++ neuf_radius/views.py | 20 + neuf_userinfo/admin.py | 16 - neuf_userinfo/forms.py | 63 ++- neuf_userinfo/models.py | 196 +-------- neuf_userinfo/rijndael.py | 378 ++++++++++++++++++ neuf_userinfo/utils.py | 99 ++--- neuf_userinfo/views.py | 90 +++-- requirements.txt | 11 +- .../registration/password_reset_email.html | 7 +- .../registration/password_reset_email.txt | 4 + userinfosite/settings.py | 6 +- userinfosite/urls.py | 17 +- 32 files changed, 893 insertions(+), 353 deletions(-) create mode 100644 neuf_kerberos/__init__.py create mode 100644 neuf_kerberos/admin.py create mode 100644 neuf_kerberos/migrations/__init__.py create mode 100644 neuf_kerberos/models.py create mode 100644 neuf_kerberos/tests.py create mode 100644 neuf_kerberos/utils.py create mode 100644 neuf_kerberos/views.py create mode 100644 neuf_ldap/__init__.py create mode 100644 neuf_ldap/admin.py create mode 100644 neuf_ldap/migrations/__init__.py create mode 100644 neuf_ldap/models.py create mode 100644 neuf_ldap/tests.py rename neuf_userinfo/passwd.py => neuf_ldap/utils.py (56%) create mode 100644 neuf_ldap/views.py create mode 100644 neuf_radius/__init__.py create mode 100644 neuf_radius/admin.py create mode 100644 neuf_radius/migrations/__init__.py create mode 100644 neuf_radius/models.py create mode 100644 neuf_radius/tests.py create mode 100644 neuf_radius/utils.py create mode 100644 neuf_radius/views.py create mode 100644 neuf_userinfo/rijndael.py create mode 100644 templates/registration/password_reset_email.txt diff --git a/neuf_kerberos/__init__.py b/neuf_kerberos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_kerberos/admin.py b/neuf_kerberos/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/neuf_kerberos/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/neuf_kerberos/migrations/__init__.py b/neuf_kerberos/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_kerberos/models.py b/neuf_kerberos/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/neuf_kerberos/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/neuf_kerberos/tests.py b/neuf_kerberos/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/neuf_kerberos/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/neuf_kerberos/utils.py b/neuf_kerberos/utils.py new file mode 100644 index 0000000..3ab0324 --- /dev/null +++ b/neuf_kerberos/utils.py @@ -0,0 +1,54 @@ +import datetime +from django.conf import settings +import logging +import re +from subprocess import Popen, PIPE + +logger = logging.getLogger(__name__) + + +def get_kerberos_principal(username): + principal = "{0}@{1}".format(username, settings.KERBEROS_REALM) + kadmin_query = " -p '{0}' -w {1} -q 'get_principal {2}'".format( + settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL, + settings.KERBEROS_PASSWORD, + principal) + p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE) + output, error = p.communicate() + if error: + # Can not init with KDC? + # KADM5_UNK_PRINC (principal does not exist) + # KADM5_AUTH_GET (requires the get (inquire) privilege) + logger.error(error) + return None + + cleaned_output = {} + match = re.findall(r'([\w ]*): (.+)', output) + for key, value in match: + cleaned_output[key] = value + return cleaned_output + + +def set_kerberos_password(username, raw_password): + principal = "{0}@{1}".format(username, settings.KERBEROS_REALM) + kadmin_query = " -p {0} -w {1} -q 'change_password -pw {2} {3}'".format( + settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL, + settings.KERBEROS_PASSWORD, + raw_password, + principal) + # FIXME: Running this shell command needs escaping: http://qntm.org/bash + p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE) + output, error = p.communicate() + if error: + # Can not init with KDC? + # KADM5_AUTH_MODIFY (requires the modify privilege) + logger.error(error) + + +def format_krb5_date(date): + try: + formatted_date = datetime.datetime.strptime(date, '%a %b %d %H:%M:%S %Z %Y') + formatted_date = formatted_date.strftime('%Y-%m-%d %H:%M:%S') + except ValueError: + formatted_date = 'never' + return formatted_date diff --git a/neuf_kerberos/views.py b/neuf_kerberos/views.py new file mode 100644 index 0000000..9e612e5 --- /dev/null +++ b/neuf_kerberos/views.py @@ -0,0 +1,20 @@ +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse + +from neuf_kerberos.utils import get_kerberos_principal, format_krb5_date + + +@login_required +def client_status(request): + krb5_principal = get_kerberos_principal(request.GET.get('username')) + if not krb5_principal: + return JsonResponse({'active': False}) + + last_succ_auth = format_krb5_date(krb5_principal['Last successful authentication']) + status = { + 'active': True, + 'last_successful_auth': last_succ_auth, + 'last_modified': krb5_principal['Last modified'] + } + + return JsonResponse(status) \ No newline at end of file diff --git a/neuf_ldap/__init__.py b/neuf_ldap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_ldap/admin.py b/neuf_ldap/admin.py new file mode 100644 index 0000000..d19f9cb --- /dev/null +++ b/neuf_ldap/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from neuf_ldap.models import LdapGroup, LdapUser + + +class LdapGroupAdmin(admin.ModelAdmin): + exclude = ['dn', 'usernames'] + list_display = ['name', 'gid'] + search_fields = ['name'] + + +class LdapUserAdmin(admin.ModelAdmin): + exclude = ['dn', 'password', 'photo'] + list_display = ['username', 'first_name', 'last_name', 'email', 'id'] + search_fields = ['first_name', 'last_name', 'full_name', 'username'] + +admin.site.register(LdapGroup, LdapGroupAdmin) +admin.site.register(LdapUser, LdapUserAdmin) diff --git a/neuf_ldap/migrations/__init__.py b/neuf_ldap/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_ldap/models.py b/neuf_ldap/models.py new file mode 100644 index 0000000..a02629d --- /dev/null +++ b/neuf_ldap/models.py @@ -0,0 +1,70 @@ +import datetime +import ldapdb.models +from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField + +from neuf_ldap.utils import ldap_create, ldap_validate + + +class LdapUser(ldapdb.models.Model): + """ Represents an LDAP posixAccount,inetOrgPerson entry. """ + + connection_name = 'ldap' + + # LDAP meta-data + base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG + object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount'] + + # inetOrgPerson + first_name = CharField(db_column='givenName') + last_name = CharField(db_column='sn') + full_name = CharField(db_column='cn') + email = CharField(db_column='mail') + phone = CharField(db_column='telephoneNumber', blank=True) + mobile_phone = CharField(db_column='mobile', blank=True) + photo = ImageField(db_column='jpegPhoto') + + # posixAccount + id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form + group = IntegerField(db_column='gidNumber') + gecos = CharField(db_column='gecos') + home_directory = CharField(db_column='homeDirectory') + login_shell = CharField(db_column='loginShell', default='/bin/bash') + username = CharField(db_column='uid', primary_key=True) + password = CharField(db_column='userPassword') + + # TODO shadowAccount + + # ugly 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 + + self.password = ldap_create(raw_password) + self.save() + + def check_password(self, raw_password): + return ldap_validate(raw_password, self.password) + + def __unicode__(self): + return self.username + + +class LdapGroup(ldapdb.models.Model): + """ Represents an LDAP posixGroup entry. """ + + connection_name = 'ldap' + + # LDAP meta-data + base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG + 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') + + def __unicode__(self): + return self.name \ No newline at end of file diff --git a/neuf_ldap/tests.py b/neuf_ldap/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/neuf_ldap/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/neuf_userinfo/passwd.py b/neuf_ldap/utils.py similarity index 56% rename from neuf_userinfo/passwd.py rename to neuf_ldap/utils.py index 88a4d75..3d23f30 100644 --- a/neuf_userinfo/passwd.py +++ b/neuf_ldap/utils.py @@ -2,14 +2,17 @@ import hashlib import os from base64 import encodestring as encode from base64 import decodestring as decode +import os.path -# Ref: -# - http://www.openldap.org/faq/data/cache/347.html -# - http://www.openldap.org/doc/admin24/security.html +""" + Ref: + - http://www.openldap.org/faq/data/cache/347.html + - http://www.openldap.org/doc/admin24/security.html +""" def ldap_create(raw_password, hash_type='ssha'): - # Generate a salted SHA hash + # Generate a salted SHA1 hash salt = os.urandom(4) h = hashlib.sha1(raw_password) h.update(salt) @@ -27,7 +30,11 @@ def ldap_validate(raw_password, challenge_password): return digest == hr.digest() -def radius_create(raw_password): - # found in the wild. - nt_password = hashlib.new('md4', raw_password.encode('utf-16le')).hexdigest() - return nt_password +def set_ldap_password(username, raw_password): + from neuf_ldap.models import LdapUser + try: + # LDAP: Lookup the Ldap user with the identical username (1-to-1). + user = LdapUser.objects.get(username=username) + user.set_password(raw_password) + except LdapUser.DoesNotExist: + pass \ No newline at end of file diff --git a/neuf_ldap/views.py b/neuf_ldap/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/neuf_ldap/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/neuf_radius/__init__.py b/neuf_radius/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_radius/admin.py b/neuf_radius/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/neuf_radius/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/neuf_radius/migrations/__init__.py b/neuf_radius/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neuf_radius/models.py b/neuf_radius/models.py new file mode 100644 index 0000000..eae2c5f --- /dev/null +++ b/neuf_radius/models.py @@ -0,0 +1,106 @@ +from django.db import models + + +class Radacct(models.Model): + connection_name = 'radius' + + radacctid = models.BigIntegerField(primary_key=True) + acctsessionid = models.CharField(max_length=192) + acctuniqueid = models.CharField(max_length=96) + username = models.CharField(max_length=192) + groupname = models.CharField(max_length=192) + realm = models.CharField(max_length=192, blank=True) + nasipaddress = models.CharField(max_length=45) + nasportid = models.CharField(max_length=45, blank=True) + nasporttype = models.CharField(max_length=96, blank=True) + acctstarttime = models.DateTimeField(null=True, blank=True) + acctstoptime = models.DateTimeField(null=True, blank=True) + acctsessiontime = models.IntegerField(null=True, blank=True) + acctauthentic = models.CharField(max_length=96, blank=True) + connectinfo_start = models.CharField(max_length=150, blank=True) + connectinfo_stop = models.CharField(max_length=150, blank=True) + acctinputoctets = models.BigIntegerField(null=True, blank=True) + acctoutputoctets = models.BigIntegerField(null=True, blank=True) + calledstationid = models.CharField(max_length=150) + callingstationid = models.CharField(max_length=150) + acctterminatecause = models.CharField(max_length=96) + servicetype = models.CharField(max_length=96, blank=True) + framedprotocol = models.CharField(max_length=96, blank=True) + framedipaddress = models.CharField(max_length=45) + acctstartdelay = models.IntegerField(null=True, blank=True) + acctstopdelay = models.IntegerField(null=True, blank=True) + xascendsessionsvrkey = models.CharField(max_length=30, blank=True) + + class Meta: + db_table = u'radacct' + + +class Radcheck(models.Model): + connection_name = 'radius' + + username = models.CharField(max_length=192) + attribute = models.CharField(max_length=192) + op = models.CharField(max_length=6) + value = models.CharField(max_length=759) + + class Meta: + db_table = u'radcheck' + + +class Radgroupcheck(models.Model): + connection_name = 'radius' + + groupname = models.CharField(max_length=192) + attribute = models.CharField(max_length=192) + op = models.CharField(max_length=6) + value = models.CharField(max_length=759) + + class Meta: + db_table = u'radgroupcheck' + + +class Radgroupreply(models.Model): + connection_name = 'radius' + + groupname = models.CharField(max_length=192) + attribute = models.CharField(max_length=192) + op = models.CharField(max_length=6) + value = models.CharField(max_length=759) + + class Meta: + db_table = u'radgroupreply' + + +class Radpostauth(models.Model): + connection_name = 'radius' + + username = models.CharField(max_length=192) + pass_field = models.CharField(max_length=192, db_column='pass') + reply = models.CharField(max_length=96) + authdate = models.DateTimeField() + + class Meta: + db_table = u'radpostauth' + + +class Radreply(models.Model): + connection_name = 'radius' + + username = models.CharField(max_length=192) + attribute = models.CharField(max_length=192) + op = models.CharField(max_length=6) + value = models.CharField(max_length=759) + + class Meta: + db_table = u'radreply' + + +class Radusergroup(models.Model): + connection_name = 'radius' + + username = models.CharField(max_length=192) + groupname = models.CharField(max_length=192) + priority = models.IntegerField() + + class Meta: + db_table = u'radusergroup' \ No newline at end of file diff --git a/neuf_radius/tests.py b/neuf_radius/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/neuf_radius/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/neuf_radius/utils.py b/neuf_radius/utils.py new file mode 100644 index 0000000..3e3740b --- /dev/null +++ b/neuf_radius/utils.py @@ -0,0 +1,28 @@ +import hashlib +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 + + +def set_radius_password(username, raw_password): + """ + Set password in Radius mysql backend. + Create user if one with an NT-Password does not exist. + """ + pw_type = 'NT-Password' + radius_defaults = {'attribute': pw_type, 'op': ':='} + radius_user, created = Radcheck.objects.get_or_create( + username=username, + defaults=radius_defaults) + + # Some have clear text passwords! + if radius_user.attribute != pw_type: + radius_user.attribute = pw_type + + radius_user.value = radius_create(raw_password) + radius_user.save() + diff --git a/neuf_radius/views.py b/neuf_radius/views.py new file mode 100644 index 0000000..0507bbc --- /dev/null +++ b/neuf_radius/views.py @@ -0,0 +1,20 @@ +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from neuf_radius.models import Radcheck, Radpostauth + + +@login_required +def wireless_status(request): + username = request.GET.get('username') + try: + radius_user = Radcheck.objects.get(username=username) + except Radcheck.DoesNotExist: + return JsonResponse({'active': False}) + + # get last authentication + last_auth = Radpostauth.objects.filter(username__iexact=username, reply='Access-Accept').order_by('authdate') + status = {'active': True, 'hash': radius_user.attribute} + if len(last_auth) != 0: + status['last_successful_auth'] = last_auth[0].authdate.strftime('%Y-%m-%d %H:%M:%S') + + return JsonResponse(status) \ No newline at end of file diff --git a/neuf_userinfo/admin.py b/neuf_userinfo/admin.py index b1b498f..694323f 100644 --- a/neuf_userinfo/admin.py +++ b/neuf_userinfo/admin.py @@ -1,17 +1 @@ from django.contrib import admin -from neuf_userinfo.models import LdapGroup, LdapUser - - -class LdapGroupAdmin(admin.ModelAdmin): - exclude = ['dn', 'usernames'] - list_display = ['name', 'gid'] - search_fields = ['name'] - - -class LdapUserAdmin(admin.ModelAdmin): - exclude = ['dn', 'password', 'photo'] - list_display = ['username', 'first_name', 'last_name', 'email', 'id'] - search_fields = ['first_name', 'last_name', 'full_name', 'username'] - -admin.site.register(LdapGroup, LdapGroupAdmin) -admin.site.register(LdapUser, LdapUserAdmin) diff --git a/neuf_userinfo/forms.py b/neuf_userinfo/forms.py index b216d8b..0b43d15 100644 --- a/neuf_userinfo/forms.py +++ b/neuf_userinfo/forms.py @@ -1,6 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals -from django.contrib.auth.forms import SetPasswordForm, PasswordResetForm +from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site from django.core.validators import MinLengthValidator @@ -9,32 +9,40 @@ from django.template import loader from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext as _ -import sys from inside.models import InsideUser from inside.utils import set_inside_password -from neuf_userinfo.models import LdapUser -from neuf_userinfo.utils import set_kerberos_password, set_radius_password + +from neuf_kerberos.utils import set_kerberos_password +from neuf_radius.utils import set_radius_password +from neuf_ldap.utils import set_ldap_password from neuf_userinfo.validators import PasswordValidator class NeufSetPasswordForm(SetPasswordForm): + """ + Saves a user password in each of the following services: + - Inside (all) + - Kerberos + - LDAP + - RADIUS + + Note: self.user is a local Django User object (which does not have a usable password) + """ def save(self, commit=True): + username = self.user.username + password = self.cleaned_data['new_password1'] + # Membership database - set_inside_password(self.user.username, self.cleaned_data['new_password1']) + set_inside_password(username, password) # Active services - set_kerberos_password(self.user.username, self.cleaned_data['new_password1']) - set_radius_password(self.user.username, self.cleaned_data['new_password1']) + set_kerberos_password(username, password) + set_radius_password(username, password) + set_ldap_password(username, password) - # LDAP: Lookup the Ldap user with the identical username (1-to-1). - self.user = LdapUser.objects.get(username=self.user.username) - self.user.set_password(self.cleaned_data['new_password1']) - if commit: - self.user.save() - - return self.user + return self.user # Local Django User def clean_new_password1(self): raw_password = self.cleaned_data.get('new_password1') @@ -49,12 +57,13 @@ class NeufPasswordChangeForm(NeufSetPasswordForm): pass -class NeufPasswordResetForm(PasswordResetForm): +class NeufPasswordResetForm(forms.Form): EMAIL_ERROR_MSG = "That e-mail address doesn't have an associated user account. Are you sure you've registered?" + email = forms.EmailField(label=_("Email"), max_length=254) def clean_email(self): email = self.cleaned_data["email"].lower() - self.users_cache = InsideUser.objects.filter(email=email) + self.users_cache = InsideUser.objects.filter(email=email) # From Membership database (Inside) if len(self.users_cache) == 0: raise forms.ValidationError(_(self.EMAIL_ERROR_MSG)) return email @@ -63,15 +72,24 @@ class NeufPasswordResetForm(PasswordResetForm): subject_template_name='registration/password_reset_subject.txt', email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, - from_email=None, request=None): + from_email=None, request=None, html_email_template_name=None): """ - Generates a one-use only link for resetting password and sends to the user. + Generates a one-use only link for resetting password and sends to the + user. Note: this is the same form as django.contrib.auth.forms.PasswordResetForm with some changes for looking up a custom user object. """ from django.core.mail import send_mail + # UserModel = get_user_model() + # email = self.cleaned_data["email"] + # active_users = UserModel._default_manager.filter( + # email__iexact=email, is_active=True) for user in self.users_cache: + # Make sure that no email is sent to a user that actually has + # a password marked as unusable + # if not user.has_usable_password(): + # continue if not domain_override: current_site = get_current_site(request) site_name = current_site.name @@ -91,8 +109,9 @@ class NeufPasswordResetForm(PasswordResetForm): # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) email = loader.render_to_string(email_template_name, c) - if sys.version_info < (2, 6, 6): - # Workaround for http://bugs.python.org/issue1368247 - email = email.encode('utf-8') - send_mail(subject, email, from_email, [user.email]) + if html_email_template_name: + html_email = loader.render_to_string(html_email_template_name, c) + else: + html_email = None + send_mail(subject, email, from_email, [user.email], html_message=html_email) diff --git a/neuf_userinfo/models.py b/neuf_userinfo/models.py index 1a288d1..0f02227 100644 --- a/neuf_userinfo/models.py +++ b/neuf_userinfo/models.py @@ -1,195 +1 @@ -import datetime -from django.db import models -import ldapdb.models -from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField -import passwd - - -class LdapUser(ldapdb.models.Model): - """ - Class for representing an LDAP user entry. - """ - - connection_name = 'ldap' - - # LDAP meta-data - base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG - object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount'] - - # inetOrgPerson - first_name = CharField(db_column='givenName') - last_name = CharField(db_column='sn') - full_name = CharField(db_column='cn') - email = CharField(db_column='mail') - phone = CharField(db_column='telephoneNumber', blank=True) - mobile_phone = CharField(db_column='mobile', blank=True) - photo = ImageField(db_column='jpegPhoto') - - # posixAccount - id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form - group = IntegerField(db_column='gidNumber') - gecos = CharField(db_column='gecos') - home_directory = CharField(db_column='homeDirectory') - login_shell = CharField(db_column='loginShell', default='/bin/bash') - username = CharField(db_column='uid', primary_key=True) - password = CharField(db_column='userPassword') - - # ugly 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 = "!" - else: - self.password = passwd.ldap_create(raw_password) - self.save() - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. - """ - return passwd.ldap_validate(raw_password, self.password) - - def __str__(self): - return self.username - - def __unicode__(self): - return self.full_name - - -class LdapGroup(ldapdb.models.Model): - """ - Class for representing an LDAP group entry. - """ - - connection_name = 'ldap' - - # LDAP meta-data - base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG - 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') - - def __str__(self): - return self.name - - def __unicode__(self): - return self.name - - -# This is an auto-generated Django model module. -# You'll have to do the following manually to clean this up: -# * Rearrange models' order -# * Make sure each model has one field with primary_key=True -# Feel free to rename the models, but don't rename db_table values or field names. -# -# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]' -# into your database. - -class Radacct(models.Model): - connection_name = 'radius' - - radacctid = models.BigIntegerField(primary_key=True) - acctsessionid = models.CharField(max_length=192) - acctuniqueid = models.CharField(max_length=96) - username = models.CharField(max_length=192) - groupname = models.CharField(max_length=192) - realm = models.CharField(max_length=192, blank=True) - nasipaddress = models.CharField(max_length=45) - nasportid = models.CharField(max_length=45, blank=True) - nasporttype = models.CharField(max_length=96, blank=True) - acctstarttime = models.DateTimeField(null=True, blank=True) - acctstoptime = models.DateTimeField(null=True, blank=True) - acctsessiontime = models.IntegerField(null=True, blank=True) - acctauthentic = models.CharField(max_length=96, blank=True) - connectinfo_start = models.CharField(max_length=150, blank=True) - connectinfo_stop = models.CharField(max_length=150, blank=True) - acctinputoctets = models.BigIntegerField(null=True, blank=True) - acctoutputoctets = models.BigIntegerField(null=True, blank=True) - calledstationid = models.CharField(max_length=150) - callingstationid = models.CharField(max_length=150) - acctterminatecause = models.CharField(max_length=96) - servicetype = models.CharField(max_length=96, blank=True) - framedprotocol = models.CharField(max_length=96, blank=True) - framedipaddress = models.CharField(max_length=45) - acctstartdelay = models.IntegerField(null=True, blank=True) - acctstopdelay = models.IntegerField(null=True, blank=True) - xascendsessionsvrkey = models.CharField(max_length=30, blank=True) - - class Meta: - db_table = u'radacct' - - -class Radcheck(models.Model): - connection_name = 'radius' - - username = models.CharField(max_length=192) - attribute = models.CharField(max_length=192) - op = models.CharField(max_length=6) - value = models.CharField(max_length=759) - - class Meta: - db_table = u'radcheck' - - -class Radgroupcheck(models.Model): - connection_name = 'radius' - - groupname = models.CharField(max_length=192) - attribute = models.CharField(max_length=192) - op = models.CharField(max_length=6) - value = models.CharField(max_length=759) - - class Meta: - db_table = u'radgroupcheck' - - -class Radgroupreply(models.Model): - connection_name = 'radius' - - groupname = models.CharField(max_length=192) - attribute = models.CharField(max_length=192) - op = models.CharField(max_length=6) - value = models.CharField(max_length=759) - - class Meta: - db_table = u'radgroupreply' - - -class Radpostauth(models.Model): - connection_name = 'radius' - - username = models.CharField(max_length=192) - pass_field = models.CharField(max_length=192, db_column='pass') - reply = models.CharField(max_length=96) - authdate = models.DateTimeField() - - class Meta: - db_table = u'radpostauth' - - -class Radreply(models.Model): - connection_name = 'radius' - - username = models.CharField(max_length=192) - attribute = models.CharField(max_length=192) - op = models.CharField(max_length=6) - value = models.CharField(max_length=759) - - class Meta: - db_table = u'radreply' - - -class Radusergroup(models.Model): - connection_name = 'radius' - - username = models.CharField(max_length=192) - groupname = models.CharField(max_length=192) - priority = models.IntegerField() - - class Meta: - db_table = u'radusergroup' - +from django.db import models \ No newline at end of file diff --git a/neuf_userinfo/rijndael.py b/neuf_userinfo/rijndael.py new file mode 100644 index 0000000..9fed6f0 --- /dev/null +++ b/neuf_userinfo/rijndael.py @@ -0,0 +1,378 @@ +""" +Ref: http://wiki.birth-online.de/_media/snippets/python/rijndael.py + +A pure python (slow) implementation of rijndael with a decent interface + +To include - + +from rijndael import rijndael + +To do a key setup - + +r = rijndael(key, block_size = 16) + +key must be a string of length 16, 24, or 32 +blocksize must be 16, 24, or 32. Default is 16 + +To use - + +ciphertext = r.encrypt(plaintext) +plaintext = r.decrypt(ciphertext) + +If any strings are of the wrong length a ValueError is thrown +""" + +# ported from the Java reference code by Bram Cohen, April 2001 +# this code is public domain, unless someone makes +# an intellectual property claim against the reference +# code, in which case it can be made public domain by +# deleting all the comments and renaming all the variables + +import copy +import string + +shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], + [[0, 0], [1, 5], [2, 4], [3, 3]], + [[0, 0], [1, 7], [3, 5], [4, 4]]] + +# [keysize][block_size] +num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} + +A = [[1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1]] + +# produce log and alog tables, needed for multiplying in the +# field GF(2^m) (generator = 3) +alog = [1] +for i in range(255): + j = (alog[-1] << 1) ^ alog[-1] + if j & 0x100 != 0: + j ^= 0x11B + alog.append(j) + +log = [0] * 256 +for i in range(1, 255): + log[alog[i]] = i + +# multiply two elements of GF(2^m) +def mul(a, b): + if a == 0 or b == 0: + return 0 + return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] + +# substitution box based on F^{-1}(x) +box = [[0] * 8 for i in range(256)] +box[1][7] = 1 +for i in range(2, 256): + j = alog[255 - log[i]] + for t in range(8): + box[i][t] = (j >> (7 - t)) & 0x01 + +B = [0, 1, 1, 0, 0, 0, 1, 1] + +# affine transform: box[i] <- B + A*box[i] +cox = [[0] * 8 for i in range(256)] +for i in range(256): + for t in range(8): + cox[i][t] = B[t] + for j in range(8): + cox[i][t] ^= A[t][j] * box[i][j] + +# S-boxes and inverse S-boxes +S = [0] * 256 +Si = [0] * 256 +for i in range(256): + S[i] = cox[i][0] << 7 + for t in range(1, 8): + S[i] ^= cox[i][t] << (7-t) + Si[S[i] & 0xFF] = i + +# T-boxes +G = [[2, 1, 1, 3], + [3, 2, 1, 1], + [1, 3, 2, 1], + [1, 1, 3, 2]] + +AA = [[0] * 8 for i in range(4)] + +for i in range(4): + for j in range(4): + AA[i][j] = G[i][j] + AA[i][i+4] = 1 + +for i in range(4): + pivot = AA[i][i] + if pivot == 0: + t = i + 1 + while AA[t][i] == 0 and t < 4: + t += 1 + assert t != 4, 'G matrix must be invertible' + for j in range(8): + AA[i][j], AA[t][j] = AA[t][j], AA[i][j] + pivot = AA[i][i] + for j in range(8): + if AA[i][j] != 0: + AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255] + for t in range(4): + if i != t: + for j in range(i+1, 8): + AA[t][j] ^= mul(AA[i][j], AA[t][i]) + AA[t][i] = 0 + +iG = [[0] * 4 for i in range(4)] + +for i in range(4): + for j in range(4): + iG[i][j] = AA[i][j + 4] + +def mul4(a, bs): + if a == 0: + return 0 + r = 0 + for b in bs: + r <<= 8 + if b != 0: + r = r | mul(a, b) + return r + +T1 = [] +T2 = [] +T3 = [] +T4 = [] +T5 = [] +T6 = [] +T7 = [] +T8 = [] +U1 = [] +U2 = [] +U3 = [] +U4 = [] + +for t in range(256): + s = S[t] + T1.append(mul4(s, G[0])) + T2.append(mul4(s, G[1])) + T3.append(mul4(s, G[2])) + T4.append(mul4(s, G[3])) + + s = Si[t] + T5.append(mul4(s, iG[0])) + T6.append(mul4(s, iG[1])) + T7.append(mul4(s, iG[2])) + T8.append(mul4(s, iG[3])) + + U1.append(mul4(t, iG[0])) + U2.append(mul4(t, iG[1])) + U3.append(mul4(t, iG[2])) + U4.append(mul4(t, iG[3])) + +# round constants +rcon = [1] +r = 1 +for t in range(1, 30): + r = mul(2, r) + rcon.append(r) + +del A +del AA +del pivot +del B +del G +del box +del log +del alog +del i +del j +del r +del s +del t +del mul +del mul4 +del cox +del iG + +class rijndael: + def __init__(self, key, block_size = 16): + if block_size != 16 and block_size != 24 and block_size != 32: + raise ValueError('Invalid block size: ' + str(block_size)) + if len(key) != 16 and len(key) != 24 and len(key) != 32: + raise ValueError('Invalid key size: ' + str(len(key))) + self.block_size = block_size + + ROUNDS = num_rounds[len(key)][block_size] + BC = block_size // 4 + # encryption round keys + Ke = [[0] * BC for i in range(ROUNDS + 1)] + # decryption round keys + Kd = [[0] * BC for i in range(ROUNDS + 1)] + ROUND_KEY_COUNT = (ROUNDS + 1) * BC + KC = len(key) // 4 + + # copy user material bytes into temporary ints + tk = [] + for i in range(0, KC): + tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) | + (ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3])) + + # copy values into round key arrays + t = 0 + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t // BC][t % BC] = tk[j] + Kd[ROUNDS - (t // BC)][t % BC] = tk[j] + j += 1 + t += 1 + tt = 0 + rconpointer = 0 + while t < ROUND_KEY_COUNT: + # extrapolate using phi (the round key evolution function) + tt = tk[KC - 1] + tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ + (S[ tt & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) ^ \ + (rcon[rconpointer] & 0xFF) << 24 + rconpointer += 1 + if KC != 8: + for i in range(1, KC): + tk[i] ^= tk[i-1] + else: + for i in range(1, KC // 2): + tk[i] ^= tk[i-1] + tt = tk[KC // 2 - 1] + tk[KC // 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) << 24 + for i in range(KC // 2 + 1, KC): + tk[i] ^= tk[i-1] + # copy values into round key arrays + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t // BC][t % BC] = tk[j] + Kd[ROUNDS - (t // BC)][t % BC] = tk[j] + j += 1 + t += 1 + # inverse MixColumn where needed + for r in range(1, ROUNDS): + for j in range(BC): + tt = Kd[r][j] + Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \ + U2[(tt >> 16) & 0xFF] ^ \ + U3[(tt >> 8) & 0xFF] ^ \ + U4[ tt & 0xFF] + self.Ke = Ke + self.Kd = Kd + + def encrypt(self, plaintext): + if len(plaintext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Ke = self.Ke + + BC = self.block_size // 4 + ROUNDS = len(Ke) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][0] + s2 = shifts[SC][2][0] + s3 = shifts[SC][3][0] + a = [0] * BC + # temporary work array + t = [] + # plaintext to ints + key + for i in range(BC): + t.append((ord(plaintext[i * 4 ]) << 24 | + ord(plaintext[i * 4 + 1]) << 16 | + ord(plaintext[i * 4 + 2]) << 8 | + ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i]) + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^ + T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Ke[ROUNDS][i] + result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return ''.join(map(chr, result)) + + def decrypt(self, ciphertext): + if len(ciphertext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext))) + Kd = self.Kd + + BC = self.block_size // 4 + ROUNDS = len(Kd) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][1] + s2 = shifts[SC][2][1] + s3 = shifts[SC][3][1] + a = [0] * BC + # temporary work array + t = [0] * BC + # ciphertext to ints + key + for i in range(BC): + t[i] = (ord(ciphertext[i * 4 ]) << 24 | + ord(ciphertext[i * 4 + 1]) << 16 | + ord(ciphertext[i * 4 + 2]) << 8 | + ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i] + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^ + T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Kd[ROUNDS][i] + result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return ''.join(map(chr, result)) + +def encrypt(key, block): + return rijndael(key, len(block)).encrypt(block) + +def decrypt(key, block): + return rijndael(key, len(block)).decrypt(block) + +def test(): + def t(kl, bl): + b = 'b' * bl + r = rijndael('a' * kl, bl) + assert r.decrypt(r.encrypt(b)) == b + t(16, 16) + t(16, 24) + t(16, 32) + t(24, 16) + t(24, 24) + t(24, 32) + t(32, 16) + t(32, 24) + t(32, 32) \ No newline at end of file diff --git a/neuf_userinfo/utils.py b/neuf_userinfo/utils.py index 50f3702..fa07394 100644 --- a/neuf_userinfo/utils.py +++ b/neuf_userinfo/utils.py @@ -1,78 +1,51 @@ # coding: utf-8 from __future__ import unicode_literals -import datetime -from django.conf import settings +import base64 +import rijndael import logging -import passwd -import re -from subprocess import Popen, PIPE - -from models import Radcheck logger = logging.getLogger(__name__) +""" +Rijndael stuff, thank you SO! +Ref: http://stackoverflow.com/questions/8217269/decrypting-strings-in-python-that-were-encrypted-with-mcrypt-rijndael-256-in-php#answers-header +""" -def get_kerberos_principal(username): - principal = "{0}@{1}".format(username, settings.KERBEROS_REALM) - kadmin_query = " -p '{0}' -w {1} -q 'get_principal {2}'".format( - settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL, - settings.KERBEROS_PASSWORD, - principal) - p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE) - output, error = p.communicate() - if error: - # Can not init with KDC? - # KADM5_UNK_PRINC (principal does not exist) - # KADM5_AUTH_GET (requires the get (inquire) privilege) - logger.error(error) - return None - else: - cleaned_output = {} - match = re.findall(r'([\w ]*): (.+)', output) - for key, value in match: - cleaned_output[key] = value - return cleaned_output +KEY_SIZE = 16 +BLOCK_SIZE = 32 -def set_kerberos_password(username, raw_password): - principal = "{0}@{1}".format(username, settings.KERBEROS_REALM) - kadmin_query = " -p {0} -w {1} -q 'change_password -pw {2} {3}'".format( - settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL, - settings.KERBEROS_PASSWORD, - raw_password, - principal) - # FIXME: Running this shell command needs escaping: http://qntm.org/bash - p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE) - output, error = p.communicate() - if error: - # Can not init with KDC? - # KADM5_AUTH_MODIFY (requires the modify privilege) - logger.error(error) +def encrypt_rijndael(key, plaintext): + padded_key = key.ljust(KEY_SIZE, '\0') + padded_text = plaintext + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE) * '\0' + + # could also be one of + #if len(plaintext) % BLOCK_SIZE != 0: + # padded_text = plaintext.ljust((len(plaintext) / BLOCK_SIZE) + 1 * BLOCKSIZE), '\0') + # -OR- + #padded_text = plaintext.ljust((len(plaintext) + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE)), '\0') + + r = rijndael.rijndael(padded_key, BLOCK_SIZE) + + ciphertext = '' + for start in range(0, len(padded_text), BLOCK_SIZE): + ciphertext += r.encrypt(padded_text[start:start+BLOCK_SIZE]) + + encoded = base64.b64encode(ciphertext) + + return encoded -def set_radius_password(username, raw_password): - """ - Set password in Radius mysql backend. - Create user if one with an NT-Password does not exist. - """ - pw_type = 'NT-Password' - radius_defaults = {'attribute': pw_type, 'op': ':='} - radius_user, created = Radcheck.objects.get_or_create( - username=username, - defaults=radius_defaults) +def decrypt_rijndael(key, encoded): + padded_key = key.ljust(KEY_SIZE, '\0') - # Some have clear text passwords! - if radius_user.attribute != pw_type: - radius_user.attribute = pw_type + ciphertext = base64.b64decode(encoded) - radius_user.value = passwd.radius_create(raw_password) - radius_user.save() + r = rijndael.rijndael(padded_key, BLOCK_SIZE) + padded_text = '' + for start in range(0, len(ciphertext), BLOCK_SIZE): + padded_text += r.decrypt(ciphertext[start:start+BLOCK_SIZE]) -def format_krb5_date(date): - try: - formatted_date = datetime.datetime.strptime(date, '%a %b %d %H:%M:%S %Z %Y') - formatted_date = formatted_date.strftime('%Y-%m-%d %H:%M:%S') - except ValueError: - formatted_date = 'never' - return formatted_date + plaintext = padded_text.split('\x00', 1)[0] + return plaintext \ No newline at end of file diff --git a/neuf_userinfo/views.py b/neuf_userinfo/views.py index 03dd8c4..6184b29 100644 --- a/neuf_userinfo/views.py +++ b/neuf_userinfo/views.py @@ -1,21 +1,22 @@ # coding: utf-8 +from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm from django.contrib.auth import login, logout as auth_logout from django.contrib.auth.tokens import default_token_generator from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import render_to_response, resolve_url, render from django.template import RequestContext from django.template.response import TemplateResponse from django.utils.http import urlsafe_base64_decode from django.views.decorators.cache import never_cache from django.views.decorators.debug import sensitive_post_parameters -import json -from inside.models import InsideUser +from django.views.generic import View -from models import * -import utils +from inside.models import InsideUser +from neuf_ldap.models import LdapGroup, LdapUser +from neuf_userinfo.utils import decrypt_rijndael def index(request): @@ -35,6 +36,7 @@ def index(request): @login_required def profile(request): + # FIXME: Allmost identical to below username = request.user.username groups = request.user.groups.all() ldap_user = None @@ -63,38 +65,6 @@ def user_profile(request, username): return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request)) -# TODO security -def client_status(request): - krb5_principal = utils.get_kerberos_principal(request.GET.get('username')) - if krb5_principal: - last_succ_auth = utils.format_krb5_date(krb5_principal['Last successful authentication']) - status = { - 'active': True, - 'last_successful_auth': last_succ_auth, - 'last_modified': krb5_principal['Last modified'] - } - else: - status = {'active': False} - return HttpResponse(json.dumps(status), content_type='application/javascript; charset=utf8') - - -# TODO security -def wireless_status(request): - username = request.GET.get('username') - try: - radius_user = Radcheck.objects.get(username=username) - except Radcheck.ObjectDoesNotExist: - return HttpResponse(json.dumps({'active': False}), content_type='application/javascript; charset=utf8') - - # get last authentication - last_auth = Radpostauth.objects.filter(username__iexact=username, reply='Access-Accept').order_by('authdate') - status = {'active': True, 'hash': radius_user.attribute} - if len(last_auth) != 0: - status['last_successful_auth'] = last_auth[0].authdate.strftime('%Y-%m-%d %H:%M:%S') - - return HttpResponse(json.dumps(status), content_type='application/javascript; charset=utf8') - - def logout(request): auth_logout(request) return render(request, 'registration/logout.html') @@ -148,3 +118,49 @@ def password_reset_confirm(request, uidb64=None, token=None, context.update(extra_context) return TemplateResponse(request, template_name, context, current_app=current_app) + + +class AddNewUserView(View): + ATTRS = ['username', 'firstname', 'lastname', 'email', 'password', 'groups'] + + def get(self, request): + """ + - Create new user in LDAP (username, first_name, last_name, email) + - add to groups (usergroup, dns-alle), it not exists, create + - Set LDAP password + - Create automount entry + - Create kerberos principal and set password + - Create homedir on wii + - Set RADIUS password + """ + if not self._validate_api_key(request.GET.get('api_key', '')): + return HttpResponse('Invalid api_key') + if not self._validate_attributes(request): + return HttpResponse('Missing at least one required attribute: '.format(', '.join(self.ATTRS))) + + user_data = self._get_user(request.GET) + + # TODO: you are here + + result = {'yousir': 'are a scholar!'} + return JsonResponse(result) + + def _validate_api_key(self, api_key): + if api_key == '' or api_key != settings.INSIDE_USERSYNC_API_KEY: + return False + + return True + + def _validate_attributes(self, request): + for a in self.ATTRS: + if a not in request.GET: + return False + + return True + + def _get_user(self, params): + user = {a: params[a] for a in self.ATTRS} + user['password'] = decrypt_rijndael(settings.INSIDE_USERSYNC_ENC_KEY, user['password']) + user['groups'] = user['groups'].strip().split(',') + + return user diff --git a/requirements.txt b/requirements.txt index 0022318..4b5e10b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,8 @@ -Django==1.6.5 +Django==1.7.4 django-auth-ldap==1.2.0 -django-extensions==1.3.8 +django-extensions==1.5.0 django-ldapdb==0.3.2 -dj-static==0.0.5 -MySQL-python==1.2.5 +dj-static==0.0.6 +mysqlclient==1.3.5 +passlib==1.6.2 python-ldap==2.4.15 -six==1.7.3 -South==1.0 diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html index ce4a7b0..2ee054a 100644 --- a/templates/registration/password_reset_email.html +++ b/templates/registration/password_reset_email.html @@ -1,2 +1,5 @@ -Noen har bedt om at passordet for brukeren {{ user.username }} med epostadresse {{ email }} skal endres. Vennligst følg lenken nedenfor for å sette et nytt passord: -{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %} +{% comment %} +ldap_username is actually just the Inside username [sic]. +{% endcomment %}Noen har bedt om at passordet for brukeren {{ user.ldap_username }} med epostadresse {{ email }} skal endres.
+
+Klikk her for å sette et nytt passord. diff --git a/templates/registration/password_reset_email.txt b/templates/registration/password_reset_email.txt new file mode 100644 index 0000000..a1ae3d2 --- /dev/null +++ b/templates/registration/password_reset_email.txt @@ -0,0 +1,4 @@ +{% comment %} +ldap_username is actually just the Inside username [sic]. +{% endcomment %}Noen har bedt om at passordet for brukeren {{ user.ldap_username }} med epostadresse {{ email }} skal endres. Vennligst følg lenken nedenfor for å sette et nytt passord: +{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %} diff --git a/userinfosite/settings.py b/userinfosite/settings.py index 6737fc8..0aca2a2 100644 --- a/userinfosite/settings.py +++ b/userinfosite/settings.py @@ -36,10 +36,12 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'django_extensions', - 'south', ) INSTALLED_APPS += ( 'neuf_userinfo', + 'neuf_ldap', + 'neuf_radius', + 'neuf_kerberos', 'inside', ) @@ -194,6 +196,8 @@ KERBEROS_PASSWORD = '' # Inside INSIDE_GROUPS_SYNC_DELETE = True # When user logs in, groups are synced (locally in Django), this deletes aswell +INSIDE_USERSYNC_API_KEY = '' +INSIDE_USERSYNC_ENC_KEY = '' try: from local_settings import * diff --git a/userinfosite/urls.py b/userinfosite/urls.py index fc07715..0c88003 100644 --- a/userinfosite/urls.py +++ b/userinfosite/urls.py @@ -1,6 +1,8 @@ from django.conf.urls import include, patterns, url from django.contrib import admin from neuf_userinfo.forms import NeufPasswordChangeForm, NeufSetPasswordForm, NeufPasswordResetForm +from neuf_userinfo.views import AddNewUserView + admin.autodiscover() import settings @@ -10,8 +12,8 @@ urlpatterns = patterns( url(r'^admin/', include(admin.site.urls)), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^userstatus/client/$', 'neuf_userinfo.views.client_status'), - url(r'^userstatus/wireless/$', 'neuf_userinfo.views.wireless_status'), + url(r'^userstatus/client/$', 'neuf_kerberos.views.client_status'), + url(r'^userstatus/wireless/$', 'neuf_radius.views.wireless_status'), url(r'^$', 'neuf_userinfo.views.index'), ) @@ -30,11 +32,20 @@ urlpatterns += patterns( # Reset password (forgot password) url(r'^accounts/password/reset$', 'django.contrib.auth.views.password_reset', - {'password_reset_form': NeufPasswordResetForm, 'from_email': settings.DEFAULT_FROM_EMAIL}), + {'password_reset_form': NeufPasswordResetForm, + 'from_email': settings.DEFAULT_FROM_EMAIL, + 'email_template_name': 'registration/password_reset_email.txt', + 'html_email_template_name': 'registration/password_reset_email.html'}), url(r'^accounts/password/reset/done$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'), # Set password url(r'^accounts/password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'neuf_userinfo.views.password_reset_confirm', {'set_password_form': NeufSetPasswordForm}), url(r'^accounts/password/reset/complete$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'), +) + +# Rewrite of usersync.neuf.no +urlpatterns += patterns( + '', + url(r'^usersync/$', AddNewUserView.as_view(), name='usersync'), ) \ No newline at end of file