Major update on the way to less users in LDAP

- Adds new app inside with models and moves related logic.
- Working InsideAuthBackend with group sync
- use passlib (<3) for MySQL hashes
- Renames main app to neuf_userinfo
- introduced local_settings.py
This commit is contained in:
nikolark committed 2015-02-14 22:54:36 +01:00
1 parent 6f2add3270
commit 79a14fca02
47 files changed
+828 -329

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
*.swp
*.pyc
settings.py
db.sqlite3
static/style/.sass-cache/
local_settings.py
venv
.idea
+2 -1
View File
@@ -12,9 +12,10 @@
## LDAP
sudo docker run -e LDAP_DOMAIN=neuf.no -e LDAP_ORGANISATION="Neuf" -e LDAP_ROOTPASS="toor" -p 389:389 -d osixia/openldap
ldapadd -D "cn=admin,dc=neuf,dc=no" -w "toor" -f testdata.ldif
ldapadd -D "cn=admin,dc=neuf,dc=no" -w "toor" -f test/testdata.ldif
# Verify import
ldapsearch -x -b dc=neuf,dc=no
# Login with test@example.com:test
# External dependencies:
- LDAP: Read the tutorial on [Howto setup a LDAP server with Docker](https://edb.neuf.no/wiki/index.php?title=Howto_setup_a_LDAP_server_with_Docker)
File renamed without changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+78
View File
@@ -0,0 +1,78 @@
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.db.models import Q
import logging
from inside import utils
from inside.models import InsideUser, InsideGroup, AuthLog
logger = logging.getLogger(__name__)
class InsideBackend(object):
"""
Authenticate against the inside database
"""
def authenticate(self, username=None, password=None):
password_hash = utils.mysql_create(password)
inside_user = InsideUser.objects.filter(
Q(password=password_hash) & (Q(ldap_username=username) | Q(email=username))
)
# AuthLog.objects.create()
if not len(inside_user) == 1:
return None
inside_user = inside_user[0]
try:
user = User.objects.get(username=inside_user.ldap_username)
except User.DoesNotExist:
# Create a new user.
user = User(username=inside_user.ldap_username, password=password)
# Sync detail and groups from Inside to local Django install (for display and permissions)
user = self._update_user_details(user, inside_user)
user.save()
user = self._update_groups(user, inside_user)
return user
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def _update_user_details(self, user, inside_user):
sync_attrs = {
'firstname': 'first_name',
'lastname': 'last_name',
'email': 'email'
}
for inside_attr, local_attr in sync_attrs.iteritems():
setattr(user, local_attr, getattr(inside_user, inside_attr))
return user
def _update_groups(self, user, inside_user):
existing_groups = list(user.groups.all())
inside_groups = InsideGroup.objects.filter(user_rels__user=inside_user).exclude(posix_group='')
inside_groups = inside_groups.values_list('posix_group', flat=True)
# Add new relationships
for group in inside_groups:
g, created = Group.objects.get_or_create(name=group)
user.groups.add(g)
if not settings.INSIDE_GROUPS_SYNC_DELETE:
return user
# Remove old relationships
for g in existing_groups:
if g.name not in inside_groups:
user.groups.remove(g)
return user
+471
View File
@@ -0,0 +1,471 @@
# coding: utf-8
from __future__ import unicode_literals
from django.db import models
class Action(models.Model):
connection_name = 'inside'
name = models.CharField(unique=True, max_length=50)
text = models.CharField(max_length=255)
class Meta:
managed = False
db_table = 'din_action'
class ActionGroupRelationship(models.Model):
connection_name = 'inside'
action = models.ForeignKey(Action)
group = models.ForeignKey('inside.InsideGroup')
class Meta:
managed = False
db_table = 'din_actiongrouprelationship'
class BugReport(models.Model):
connection_name = 'inside'
date = models.DateTimeField()
type = models.CharField(max_length=45)
filename = models.CharField(max_length=45)
query = models.TextField()
referer = models.TextField()
useragent = models.CharField(max_length=255)
user_id = models.IntegerField(blank=True, null=True)
comment = models.TextField()
title = models.CharField(max_length=50)
active = models.IntegerField()
status_comment = models.TextField()
class Meta:
managed = False
db_table = 'din_bugreport'
class Division(models.Model):
connection_name = 'inside'
name = models.CharField(unique=True, max_length=50)
nicename = models.CharField(max_length=20)
text = models.TextField()
phone = models.CharField(max_length=8)
email = models.CharField(max_length=120)
office = models.CharField(max_length=3)
user_id_contact = models.ForeignKey('inside.InsideUser', db_column='user_id_contact')
url = models.CharField(max_length=120)
divisioncategory = models.ForeignKey('inside.DivisionCategory')
picture = models.CharField(max_length=7)
hidden = models.IntegerField()
updated = models.DateTimeField(blank=True, null=True)
class Meta:
managed = False
db_table = 'din_division'
class DivisionCategory(models.Model):
connection_name = 'inside'
title = models.CharField(unique=True, max_length=50)
text = models.TextField()
class Meta:
managed = False
db_table = 'din_divisioncategory'
class Document(models.Model):
connection_name = 'inside'
type = models.CharField(max_length=60)
name = models.CharField(max_length=120)
size = models.BigIntegerField()
date = models.DateTimeField()
documentcategory = models.ForeignKey('inside.DocumentCategory')
tags = models.CharField(max_length=255)
class Meta:
managed = False
db_table = 'din_document'
class DocumentCategory(models.Model):
connection_name = 'inside'
title = models.CharField(unique=True, max_length=50)
text = models.TextField()
class Meta:
managed = False
db_table = 'din_documentcategory'
class DocumentData(models.Model):
connection_name = 'inside'
data = models.TextField()
document = models.ForeignKey(Document)
class Meta:
managed = False
db_table = 'din_documentdata'
class InsideGroup(models.Model):
connection_name = 'inside'
name = models.CharField(unique=True, max_length=50)
text = models.TextField()
division = models.ForeignKey(Division, blank=True, null=True)
admin = models.CharField(max_length=1)
mailinglist = models.CharField(max_length=50)
posix_group = models.CharField(max_length=128)
class Meta:
managed = False
db_table = 'din_group'
class MemberCard(models.Model):
connection_name = 'inside'
userid = models.IntegerField(db_column='userId', blank=True, null=True) # Field name made lowercase.
ordered = models.DateTimeField(blank=True, null=True)
produced = models.DateTimeField(blank=True, null=True)
delivered = models.DateTimeField(blank=True, null=True)
active = models.IntegerField()
class Meta:
managed = False
db_table = 'din_membercard'
class Order(models.Model):
connection_name = 'inside'
user = models.ForeignKey('inside.InsideUser')
timestamp = models.DateTimeField()
order_status = models.ForeignKey('inside.OrderStatus')
order_deliverystatus_id = models.IntegerField()
comment = models.TextField()
class Meta:
managed = False
db_table = 'din_order'
class OrderDeliveryStatus(models.Model):
connection_name = 'inside'
order_deliverystatus_id = models.IntegerField(primary_key=True)
value = models.CharField(max_length=45)
class Meta:
managed = False
db_table = 'din_order_deliverystatus'
class OrderItem(models.Model):
connection_name = 'inside'
product = models.ForeignKey('inside.Product')
quantity = models.IntegerField()
order = models.ForeignKey(Order)
discount = models.IntegerField()
comment = models.TextField()
class Meta:
managed = False
db_table = 'din_order_item'
class OrderStatus(models.Model):
connection_name = 'inside'
value = models.CharField(max_length=45)
class Meta:
managed = False
db_table = 'din_order_status'
class Position(models.Model):
connection_name = 'inside'
name = models.CharField(max_length=50)
division = models.ForeignKey(Division)
text = models.TextField()
class Meta:
managed = False
db_table = 'din_position'
class InsidePostalCode(models.Model):
connection_name = 'inside'
postnummer = models.CharField(primary_key=True, max_length=4)
poststed = models.CharField(max_length=32)
kommunenr = models.CharField(max_length=4)
kommunenavn = models.CharField(max_length=30)
kategori = models.CharField(max_length=1)
class Meta:
managed = False
db_table = 'din_postnummer'
class Product(models.Model):
connection_name = 'inside'
title = models.CharField(max_length=255)
description = models.TextField()
price = models.IntegerField()
allow_comment = models.IntegerField()
display_in_shop = models.IntegerField()
picture = models.CharField(max_length=10)
class Meta:
managed = False
db_table = 'din_product'
class InsideSettings(models.Model):
connection_name = 'inside'
name = models.CharField(primary_key=True, max_length=45)
value = models.CharField(max_length=45)
class Meta:
managed = False
db_table = 'din_settings'
class SmsReceived(models.Model):
connection_name = 'inside'
smsid = models.IntegerField(primary_key=True)
userid = models.IntegerField(blank=True, null=True)
gsm = models.CharField(max_length=15)
codeword = models.CharField(max_length=20)
message = models.CharField(max_length=160)
operator = models.CharField(max_length=20)
shortno = models.CharField(max_length=15)
action = models.CharField(max_length=30)
ip = models.CharField(db_column='IP', max_length=15) # Field name made lowercase.
date = models.DateTimeField()
simulation = models.IntegerField()
class Meta:
managed = False
db_table = 'din_sms_received'
class SmsSent(models.Model):
connection_name = 'inside'
smsid = models.IntegerField(primary_key=True)
response_to = models.IntegerField()
msgid = models.IntegerField()
sender = models.CharField(max_length=15)
receiver = models.CharField(max_length=15)
countrycode = models.IntegerField()
message = models.CharField(max_length=160)
operator = models.CharField(max_length=20, blank=True)
codeword = models.CharField(max_length=20, blank=True)
billing_price = models.IntegerField()
use_dlr = models.IntegerField()
date = models.DateTimeField()
simulation = models.IntegerField()
activation_code = models.CharField(max_length=10, blank=True)
class Meta:
managed = False
db_table = 'din_sms_sent'
class Transaction(models.Model):
connection_name = 'inside'
id_string = models.CharField(max_length=25)
user = models.ForeignKey('inside.InsideUser')
order = models.ForeignKey(Order)
timestamp = models.DateTimeField()
status = models.CharField(max_length=8, blank=True)
amount = models.IntegerField()
class Meta:
managed = False
db_table = 'din_transaction'
class TransactionCheck(models.Model):
connection_name = 'inside'
order_id = models.IntegerField(primary_key=True)
inserted = models.DateTimeField()
order_ref = models.CharField(max_length=255)
order_ref2 = models.CharField(max_length=255, blank=True)
class Meta:
managed = False
db_table = 'din_transaction_check'
class UsedCardNo(models.Model):
connection_name = 'inside'
date = models.DateTimeField()
class Meta:
managed = False
db_table = 'din_usedcardno'
class InsideUser(models.Model):
connection_name = 'inside'
cardno = models.IntegerField(unique=True, blank=True, null=True)
username = models.CharField(unique=True, max_length=12)
password = models.CharField(max_length=50)
firstname = models.CharField(max_length=120)
lastname = models.CharField(max_length=120)
addresstype = models.CharField(max_length=3)
valid_address = models.IntegerField()
email = models.CharField(unique=True, max_length=120)
birthdate = models.DateField()
placeofstudy = models.IntegerField(db_column='placeOfStudy') # Field name made lowercase.
passwordreset = models.CharField(db_column='passwordReset', max_length=1) # Field name made lowercase.
expires = models.DateField(blank=True, null=True)
division_id_request = models.ForeignKey(Division, db_column='division_id_request', blank=True, null=True)
cardproduced = models.IntegerField(db_column='cardProduced') # Field name made lowercase.
carddelivered = models.IntegerField(db_column='cardDelivered') # Field name made lowercase.
laststicker = models.CharField(db_column='lastSticker', max_length=9) # Field name made lowercase.
migrated = models.DateTimeField(blank=True, null=True)
ldap_username = models.CharField(unique=True, max_length=20, blank=True)
source = models.CharField(max_length=255)
registration_status = models.CharField(max_length=255)
created = models.DateTimeField(blank=True, null=True)
membership_trial = models.CharField(max_length=255)
def __unicode__(self):
if not self.ldap_username:
return 'User {}'.format(self.pk)
return '{} ({})'.format(self.ldap_username, self.pk)
class Meta:
managed = False
db_table = 'din_user'
class UserAddressInt(models.Model):
connection_name = 'inside'
user = models.ForeignKey('inside.InsideUser', primary_key=True)
street = models.CharField(max_length=255)
zipcode = models.CharField(max_length=10)
city = models.CharField(max_length=50)
state = models.CharField(max_length=50)
country = models.CharField(max_length=50)
class Meta:
managed = False
db_table = 'din_useraddressint'
class UserAddressNo(models.Model):
connection_name = 'inside'
user = models.ForeignKey('inside.InsideUser', primary_key=True)
street = models.CharField(max_length=255)
zipcode = models.ForeignKey('inside.PostalCode', db_column='zipcode')
class Meta:
managed = False
db_table = 'din_useraddressno'
class UserGroupRelationship(models.Model):
connection_name = 'inside'
user = models.ForeignKey('inside.InsideUser', related_name='group_rels')
group = models.ForeignKey('inside.InsideGroup', related_name='user_rels')
class Meta:
managed = False
db_table = 'din_usergrouprelationship'
class UserPhoneNumber(models.Model):
connection_name = 'inside'
user = models.ForeignKey('inside.InsideUser', primary_key=True)
number = models.CharField(max_length=16)
validated = models.IntegerField()
class Meta:
managed = False
db_table = 'din_userphonenumber'
class UserUpdate(models.Model):
connection_name = 'inside'
date = models.DateTimeField(auto_now_add=True)
user_updated = models.ForeignKey(
'inside.InsideUser',
db_column='user_id_updated',
related_name='updates')
comment = models.CharField(max_length=255)
user_updated_by = models.ForeignKey(
'inside.InsideUser',
db_column='user_id_updated_by',
blank=True,
null=True,
related_name='admin_updates')
class Meta:
managed = False
db_table = 'din_userupdate'
class AuthLog(models.Model):
connection_name = 'inside'
username = models.CharField(max_length=80)
password = models.IntegerField()
time = models.DateTimeField()
error = models.CharField(max_length=100)
class Meta:
managed = False
db_table = 'inside_auth_log'
class PostalCode(models.Model):
connection_name = 'inside'
postnummer = models.CharField(primary_key=True, max_length=4)
poststed = models.CharField(max_length=32)
kommunenr = models.CharField(max_length=4)
kommunenavn = models.CharField(max_length=30)
kategori = models.CharField(max_length=1)
class Meta:
managed = False
db_table = 'postnummer'
class PlaceOfStudy(models.Model):
connection_name = 'inside'
navn = models.CharField(unique=True, max_length=120)
class Meta:
managed = False
db_table = 'studiesteder'
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import connections
import logging
from passlib.hash import mysql41
from inside.models import InsideUser, UserUpdate
logger = logging.getLogger(__name__)
def mysql_create(raw_password):
return mysql41.encrypt(raw_password)
def log_inside_userupdate(username, message):
try:
user = InsideUser.objects.get(ldap_username=username)
except InsideUser.DoesNotExist:
logger.warning('Could not log update in Inside for user with username {}'.format(username))
return
UserUpdate.objects.create(user_updated=user, comment=message, user_updated_by=user)
def set_inside_password(username, raw_password):
hashed_password = mysql_create(raw_password)
try:
user = InsideUser.objects.get(ldap_username=username)
except InsideUser.DoesNotExist:
logger.warning('Could not set password in Inside for user with username {}'.format(username))
return
user.password = hashed_password
user.save()
log_inside_userupdate(username, "Satt passord på nytt via brukerinfo.neuf.no.")
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
-77
View File
@@ -1,77 +0,0 @@
from django.contrib.auth.models import User, Group
from django.db import connections
class InsideBackend(object):
"""
Authenticate against the inside database
"""
def authenticate(self, username=None, password=None):
row = None
cur = connections['inside'].cursor()
try:
cur.execute("SELECT id,ldap_username,firstname,lastname,email FROM din_user WHERE password=PASSWORD(%s) AND (ldap_username=%s OR email=%s)", [password, username, username])
row = cur.fetchone()
except Exception as e:
# FIXME errorhandling
print "Auth ERROR: {}".format(e)
raise e
finally:
cur.close()
if row:
username = row[1]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# Create a new user.
user = User(username=username, password=password)
user = self._update_user_details(user, row) # Saves user object
user = self._update_groups(user, row)
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def _update_user_details(self, user, row):
uid, username, first_name, last_name, email = row
user.first_name = first_name
user.last_name = last_name
user.email = email
user.save()
return user
def _update_groups(self, user, user_row):
groups = None
cur = connections['inside'].cursor()
user_id = user_row[0]
existing_groups = list(user.groups.all())
try:
cur.execute("SELECT g.posix_group FROM din_user AS u LEFT JOIN din_usergrouprelationship AS ugr ON u.id = ugr.user_id LEFT JOIN din_group AS g ON ugr.group_id = g.id WHERE u.id = %s AND g.posix_group != ''", (int(user_row[0]),))
groups = map(lambda x: x[0], cur.fetchall())
for group in groups:
g, created = Group.objects.get_or_create(name=group)
user.groups.add(g)
# Remove old relationships
# TODO: if setting is set. not tested
for g in existing_groups:
if g.name not in groups:
user.groups.remove(g)
except Exception as e:
# FIXME errorhandling
print "ERROR: {}".format(e)
finally:
cur.close()
return user
-23
View File
@@ -1,23 +0,0 @@
"""
This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test".
Replace these with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}
-102
View File
@@ -1,102 +0,0 @@
# coding: utf-8
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from subprocess import Popen, PIPE
import datetime
import re
from models import Radcheck
import passwd
def get_kerberos_principal(username):
principal = u"{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = u" -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)
return None
else:
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 = u"{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = u" -p {0} -w {1} -q 'change_password -pw {2} {3}'".format(settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL,
settings.KERBEROS_PASSWORD,
raw_password,
principal)
# Note: Running this shell command could need 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)
return False
else:
return True
def set_radius_password(username, raw_password):
"""
Set password in Radius mysql backend.
Create user if one with an NT-Password does not exist.
"""
try:
radius_user = Radcheck.objects.get(username=username)
except ObjectDoesNotExist:
radius_user = Radcheck(username=username, attribute='NT-Password', op=':=')
# Some have clear text passwords!
if radius_user.attribute != 'NT-Password':
radius_user.attribute = 'NT-Password'
radius_user.value = passwd.radius_create(raw_password)
radius_user.save()
return True
def log_inside_userupdate(username, message):
from django.db import connections
c = connections['inside'].cursor()
# TODO errorhandling
try:
c.execute("SELECT id FROM din_user WHERE username= %s", [username])
row = c.fetchone()
if row is not None:
user_id = row[0]
c.execute(
"INSERT INTO din_userupdate (date, user_id_updated, comment, user_id_updated_by) VALUES (NOW(), %s, %s, %s)",
[user_id, message, user_id])
finally:
c.close()
return True
def set_inside_password(username, raw_password):
from django.db import connections
c = connections['inside'].cursor()
# TODO errorhandling
try:
c.execute("UPDATE din_user SET password=PASSWORD(%s) WHERE username=%s", [raw_password, username])
finally:
c.close()
log_inside_userupdate(username, u"Satt passord på nytt via brukerinfo.neuf.no.")
return True
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
View File
Whitespace-only changes.
+3 -1
View File
@@ -1,11 +1,13 @@
from django.contrib import admin
from main.models import LdapGroup, LdapUser
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']
@@ -1,4 +1,4 @@
from django_auth_ldap.backend import LDAPBackend, _LDAPUser
from django_auth_ldap.backend import LDAPBackend
class LDAPUsernameBackend(LDAPBackend):
+24 -21
View File
@@ -1,4 +1,5 @@
# coding: utf-8
from __future__ import unicode_literals
from django.contrib.auth.forms import SetPasswordForm, PasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site
@@ -9,50 +10,53 @@ 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 models import *
from validators import PasswordValidator
import utils
from neuf_userinfo.utils import set_kerberos_password, set_radius_password
class LDAPSetPasswordForm(SetPasswordForm):
class NeufSetPasswordForm(SetPasswordForm):
def save(self, commit=True):
# set kerberos password
set_krb5 = utils.set_kerberos_password(self.user.username, self.cleaned_data['new_password1'])
# set radius password
set_radius = utils.set_radius_password(self.user.username, self.cleaned_data['new_password1'])
# set inside password
set_inside = utils.set_inside_password(self.user.username, self.cleaned_data['new_password1'])
# Membership database
set_inside_password(self.user.username, self.cleaned_data['new_password1'])
# Lookup the Ldap user with the identical username (1-to-1).
# Active services
set_kerberos_password(self.user.username, self.cleaned_data['new_password1'])
set_radius_password(self.user.username, self.cleaned_data['new_password1'])
# LDAP: Lookup the Ldap user with the identical username (1-to-1).
self.user = LdapUser.objects.get(username=self.user.username)
# set ldap password
self.user.set_password(self.cleaned_data['new_password1'])
if commit:
self.user.save()
# TODO: Log the result of the above.
return self.user
def clean_new_password1(self):
raw_password = self.cleaned_data.get('new_password1')
# Validation here
MinLengthValidator(8)(raw_password)
PasswordValidator(raw_password)
return raw_password
class LDAPPasswordChangeForm(LDAPSetPasswordForm):
class NeufPasswordChangeForm(NeufSetPasswordForm):
pass
class LDAPPasswordResetForm(PasswordResetForm):
class NeufPasswordResetForm(PasswordResetForm):
EMAIL_ERROR_MSG = "That e-mail address doesn't have an associated user account. Are you sure you've registered?"
def clean_email(self):
email = self.cleaned_data["email"].lower()
self.users_cache = LdapUser.objects.filter(email=email)
self.users_cache = InsideUser.objects.filter(email=email)
if len(self.users_cache) == 0:
raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?"))
raise forms.ValidationError(_(self.EMAIL_ERROR_MSG))
return email
def save(self, domain_override=None,
@@ -88,8 +92,7 @@ class LDAPPasswordResetForm(PasswordResetForm):
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 ?
# Workaround for http://bugs.python.org/issue1368247
email = email.encode('utf-8')
send_mail(subject, email, from_email, [user.email])
+16 -6
View File
@@ -1,8 +1,9 @@
from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField
import datetime
from django.db import models
import ldapdb.models
from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField
import passwd
import datetime
class LdapUser(ldapdb.models.Model):
"""
@@ -12,7 +13,7 @@ class LdapUser(ldapdb.models.Model):
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=People,dc=neuf,dc=no" # CONFIG
base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG
object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount']
# inetOrgPerson
@@ -25,9 +26,9 @@ class LdapUser(ldapdb.models.Model):
photo = ImageField(db_column='jpegPhoto')
# posixAccount
id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form
id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form
group = IntegerField(db_column='gidNumber')
gecos = CharField(db_column='gecos')
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)
@@ -55,6 +56,7 @@ class LdapUser(ldapdb.models.Model):
def __unicode__(self):
return self.full_name
class LdapGroup(ldapdb.models.Model):
"""
Class for representing an LDAP group entry.
@@ -63,7 +65,7 @@ class LdapGroup(ldapdb.models.Model):
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG
base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG
object_classes = ['posixGroup']
# posixGroup attributes
@@ -116,9 +118,11 @@ class Radacct(models.Model):
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'
@@ -126,9 +130,11 @@ class Radcheck(models.Model):
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'
@@ -140,6 +146,7 @@ class Radgroupcheck(models.Model):
class Meta:
db_table = u'radgroupcheck'
class Radgroupreply(models.Model):
connection_name = 'radius'
@@ -151,6 +158,7 @@ class Radgroupreply(models.Model):
class Meta:
db_table = u'radgroupreply'
class Radpostauth(models.Model):
connection_name = 'radius'
@@ -162,6 +170,7 @@ class Radpostauth(models.Model):
class Meta:
db_table = u'radpostauth'
class Radreply(models.Model):
connection_name = 'radius'
@@ -173,6 +182,7 @@ class Radreply(models.Model):
class Meta:
db_table = u'radreply'
class Radusergroup(models.Model):
connection_name = 'radius'
@@ -7,6 +7,7 @@ from base64 import decodestring as decode
# - 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
salt = os.urandom(4)
@@ -14,6 +15,7 @@ def ldap_create(raw_password, hash_type='ssha'):
h.update(salt)
return "{SSHA}" + encode(h.digest() + salt)[:-1]
def ldap_validate(raw_password, challenge_password):
# challenge_password is hash from db
# raw_password is the cleartext password.
@@ -24,6 +26,7 @@ def ldap_validate(raw_password, challenge_password):
hr.update(salt)
return digest == hr.digest()
def radius_create(raw_password):
# found in the wild.
nt_password = hashlib.new('md4', raw_password.encode('utf-16le')).hexdigest()
+1 -1
View File
@@ -1,6 +1,7 @@
def is_radius_model(model):
return hasattr(model, 'connection_name')
class Router(object):
"""
A router to point database operations on models to the right db.
@@ -28,4 +29,3 @@ class Router(object):
if is_radius_model(model):
return model.connection_name
return None
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 481 B

After

Width:  |  Height:  |  Size: 481 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 923 B

After

Width:  |  Height:  |  Size: 923 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
View File
@@ -0,0 +1 @@
from django.test import TestCase
+78
View File
@@ -0,0 +1,78 @@
# coding: utf-8
from __future__ import unicode_literals
import datetime
from django.conf import settings
import logging
import passwd
import re
from subprocess import Popen, PIPE
from models import Radcheck
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
else:
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 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 = passwd.radius_create(raw_password)
radius_user.save()
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
File renamed without changes.
+34 -30
View File
@@ -1,26 +1,23 @@
# coding: utf-8
import json
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.models import User
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404, resolve_url
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.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
import json
from inside.models import InsideUser
from forms import LDAPPasswordResetForm
from models import *
import utils
def index(request):
if request.method == "POST":
form = AuthenticationForm(data=request.POST)
@@ -29,12 +26,13 @@ def index(request):
login(request, form.get_user())
else:
if request.user.is_authenticated():
return HttpResponseRedirect( reverse('main.views.profile') )
return HttpResponseRedirect(reverse('neuf_userinfo.views.profile'))
else:
form = AuthenticationForm(request)
return render_to_response('public/index.html', locals(), context_instance=RequestContext(request))
@login_required
def profile(request):
username = request.user.username
@@ -42,7 +40,7 @@ def profile(request):
ldap_user = None
try:
ldap_user = LdapUser.objects.get(username=username)
except:
except LdapUser.DoesNotExist:
return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request))
ldap_groups = LdapGroup.objects.filter(usernames__contains=username)
@@ -50,12 +48,13 @@ def profile(request):
return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request))
@permission_required('main.is_superuser')
def user_profile(request, username):
ldap_user = None
try:
ldap_user = LdapUser.objects.get(username=username)
except:
except LdapUser.DoesNotExist:
return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request))
groups = LdapGroup.objects.filter(usernames__contains=username)
@@ -63,37 +62,42 @@ def user_profile(request, username):
return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request))
#todo security
# 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'] }
status = {
'active': True,
'last_successful_auth': last_succ_auth,
'last_modified': krb5_principal['Last modified']
}
else:
status = { 'active' : False }
status = {'active': False}
return HttpResponse(json.dumps(status), content_type='application/javascript; charset=utf8')
#todo security
# TODO security
def wireless_status(request):
username = request.GET.get('username')
try:
radius_user = Radcheck.objects.get(username=request.GET.get('username'))
# get last authentication
last_auth = Radpostauth.objects.filter(username__iexact=request.GET.get('username'), reply='Access-Accept').order_by('authdate')
if len(last_auth) != 0:
status = { 'active' : True,
'last_successful_auth': last_auth[0].authdate.strftime('%Y-%m-%d %H:%M:%S'),
'hash': radius_user.attribute }
else:
status = { 'active' : True, 'hash': radius_user.attribute }
except ObjectDoesNotExist:
status = { 'active' : False }
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_to_response('registration/logout.html', locals(), context_instance=RequestContext(request))
return render(request, 'registration/logout.html')
# Doesn't need csrf_protect since no-one can guess the URL
@@ -119,8 +123,8 @@ def password_reset_confirm(request, uidb64=None, token=None,
post_reset_redirect = resolve_url(post_reset_redirect)
try:
uid = urlsafe_base64_decode(uidb64)
user = LdapUser.objects.get(pk=uid) # Diff line vs internal django view
except (TypeError, ValueError, OverflowError, LdapUser.DoesNotExist):
user = InsideUser.objects.get(pk=uid) # Diff line vs internal django view
except (TypeError, ValueError, OverflowError, InsideUser.DoesNotExist):
user = None
if user is not None and token_generator.check_token(user, token):
+8 -7
View File
@@ -1,15 +1,17 @@
import json, codecs, subprocess, os
import json
import codecs
import subprocess
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "userinfosite.settings")
from django.conf import settings
from main.models import *
from neuf_userinfo.models import LdapGroup, LdapUser
PHP_SCRIPT_PATH = '/var/www/neuf.no/userinfo/scripts'
OUT_FILENAME = os.path.join(PHP_SCRIPT_PATH,"users_in_group_active.json")
OUT_FILENAME = os.path.join(PHP_SCRIPT_PATH, "users_in_group_active.json")
usernames = LdapGroup.objects.get(name="dns-aktiv").usernames
usernames = LdapGroup.objects.get(posix_name="dns-aktiv").usernames
users = LdapUser.objects.filter(username__in=usernames)
users_out = [[u.username,u.first_name,u.last_name,u.email] for u in users]
users_out = [[u.username, u.first_name, u.last_name, u.email] for u in users]
out_file = codecs.open(OUT_FILENAME, "w", encoding="utf-8")
@@ -29,4 +31,3 @@ for load_path in wp_load_paths:
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
script_response = proc.stdout.read()
print script_response,
+1 -1
View File
@@ -13,7 +13,7 @@
{% if active %}
<li class="{% if active = "profile" %}active{% endif %}"><a href="/accounts/profile"><i class="icon-home icon-white"></i> Din konto</a></li>
<li class="{% if active = "password_change" %}active{% endif %}"><a href="{% url 'django.contrib.auth.views.password_change' %}"><i class="icon-cog icon-white"></i> Endre passord</a></li>
<li class="{% if active = "logout" %}active{% endif %}"><a href="{% url 'main.views.logout' %}">Logg ut</a></li>
<li class="{% if active = "logout" %}active{% endif %}"><a href="{% url 'neuf_userinfo.views.logout' %}">Logg ut</a></li>
{% if request.user.is_superuser %}
<li><a href="/admin">Administrasjon</a></li>
{% endif %}
+1 -1
View File
@@ -7,5 +7,5 @@
{%endblock%}
{% block content %}
Du har blitt logget ut, vil du <a href="{% url "main.views.index" %}">logge inn</a> igjen?
Du har blitt logget ut, vil du <a href="{% url "neuf_userinfo.views.index" %}">logge inn</a> igjen?
{% endblock %}
@@ -1,2 +1,2 @@
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 "main.views.password_reset_confirm" uidb64=uid token=token %}
{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %}
+2 -2
View File
@@ -9,8 +9,8 @@ dn: uid=test,ou=People,dc=neuf,dc=no
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: testesen
gecos: testesen
uid: test
gecos: test
loginShell: /bin/bash
shadowExpire: -1
shadowFlag: 0
@@ -35,12 +35,12 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
)
INSTALLED_APPS += (
'django_extensions',
'south',
# Our app
'main',
)
INSTALLED_APPS += (
'neuf_userinfo',
'inside',
)
MIDDLEWARE_CLASSES = (
@@ -67,38 +67,29 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# Example PostgreSQL config:
#'ENGINE': 'django.db.backends.postgresql_psycopg2',
#'NAME': 'dusken_test',
#'USER': 'postgres',
#'PASSWORD': '',
#'HOST': '127.0.0.1',
#'PORT': '',
},
'ldap': {
'ENGINE': 'ldapdb.backends.ldap',
'NAME': 'ldap://localhost/',
'USER': 'uid=admin,ou=People,dc=neuf,dc=no',
'PASSWORD': '',
'USER': 'uid=test,ou=People,dc=neuf,dc=no',
'PASSWORD': 'test',
},
'radius': {
'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'radius', # Or path to database file if using sqlite3.
'USER': 'radius', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
'ENGINE': 'django.db.backends.mysql',
'NAME': 'radius',
'USER': 'radius',
'PASSWORD': '',
'HOST': 'localhost',
},
'inside': {
'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'dev_inside', # Or path to database file if using sqlite3.
'USER': 'dev', # Not used with sqlite3.
'PASSWORD': 'dev', # Not used with sqlite3.
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dev_inside',
'USER': 'dev',
'PASSWORD': 'dev',
'HOST': 'localhost',
}
}
DATABASE_ROUTERS = ['main.router.Router']
DATABASE_ROUTERS = ['neuf_userinfo.router.Router']
# Internationalization
# https://docs.djangoproject.com/en/1.6/topics/i18n/
@@ -110,14 +101,10 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.6/howto/static-files/
STATIC_ROOT = 'staticfiles'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
# Uploads
# TODO S3
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
@@ -126,18 +113,19 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SITE_ID = 1
EMAIL_HOST = 'snes.neuf.no'
EMAIL_HOST = 'mx.neuf.no'
EMAIL_PORT = 25
DEFAULT_FROM_EMAIL = 'noreply@neuf.no'
AUTHENTICATION_BACKENDS = (
'main.backends.LDAPEmailBackend',
'main.backends.LDAPUsernameBackend',
# 'inside.authentication.InsideBackend',
'neuf_userinfo.backends.LDAPEmailBackend',
'neuf_userinfo.backends.LDAPUsernameBackend',
'django.contrib.auth.backends.ModelBackend',
)
# No cleaningladies.
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_AGE = 60 * 15 # in seconds
SESSION_COOKIE_AGE = 60 * 15 # in seconds
# LDAP server URI and BIND_DN, same as db-settings
AUTH_LDAP_U_SERVER_URI = DATABASES['ldap']['NAME']
@@ -162,7 +150,7 @@ AUTH_LDAP_U_USER_FLAGS_BY_GROUP = {
}
# Group to profile flag mappings, not used.
AUTH_LDAP_U_PROFILE_FLAGS_BY_GROUP = {
#"is_edb": "cn=edb,ou=Groups,dc=neuf,dc=no"
# "is_edb": "cn=edb,ou=Groups,dc=neuf,dc=no"
}
# User attribute mappings
AUTH_LDAP_U_USER_ATTR_MAP = {
@@ -203,3 +191,11 @@ logger.setLevel(logging.DEBUG)
KERBEROS_REALM = "NEUF.NO"
KERBEROS_PASSWORD_CHANGING_PRINCIPAL = 'brukerinfo'
KERBEROS_PASSWORD = ''
# Inside
INSIDE_GROUPS_SYNC_DELETE = True # When user logs in, groups are synced (locally in Django), this deletes aswell
try:
from local_settings import *
except ImportError:
pass
+22 -17
View File
@@ -1,35 +1,40 @@
from django.conf.urls import include, patterns, url
from django.contrib import admin
from main.forms import LDAPPasswordChangeForm, LDAPSetPasswordForm, LDAPPasswordResetForm
from neuf_userinfo.forms import NeufPasswordChangeForm, NeufSetPasswordForm, NeufPasswordResetForm
admin.autodiscover()
import settings
urlpatterns = patterns('',
urlpatterns = patterns(
'',
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^accounts/profile/$', 'main.views.profile'),
url(r'^accounts/profile/(?P<username>\w+)$', 'main.views.user_profile'),
url(r'^userstatus/client/$', 'neuf_userinfo.views.client_status'),
url(r'^userstatus/wireless/$', 'neuf_userinfo.views.wireless_status'),
url(r'^$', 'neuf_userinfo.views.index'),
)
# User account views
urlpatterns += patterns(
'',
url(r'^accounts/profile/$', 'neuf_userinfo.views.profile'),
url(r'^accounts/profile/(?P<username>\w+)$', 'neuf_userinfo.views.user_profile'),
url(r'^accounts/login/$', 'django.contrib.auth.views.login'),
url(r'^accounts/logout/$', 'main.views.logout'),
url(r'^accounts/logout/$', 'neuf_userinfo.views.logout'),
# Change password
url(r'^accounts/password/change$', 'django.contrib.auth.views.password_change',
{ 'password_change_form': LDAPPasswordChangeForm }),
{'password_change_form': NeufPasswordChangeForm}),
url(r'^accounts/password/change/done$', 'django.contrib.auth.views.password_change_done', name='password_change_done'),
# Reset password (forgot password)
url(r'^accounts/password/reset$', 'django.contrib.auth.views.password_reset',
{ 'password_reset_form' : LDAPPasswordResetForm,
'from_email' : settings.DEFAULT_FROM_EMAIL }),
{'password_reset_form': NeufPasswordResetForm, 'from_email': settings.DEFAULT_FROM_EMAIL}),
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<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'main.views.password_reset_confirm',
{ 'set_password_form': LDAPSetPasswordForm }),
'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'),
url(r'^userstatus/client/$', 'main.views.client_status'),
url(r'^userstatus/wireless/$', 'main.views.wireless_status'),
url(r'^$', 'main.views.index'),
)
from django.contrib.staticfiles.urls import staticfiles_urlpatterns # for dev
urlpatterns += staticfiles_urlpatterns()
)