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