Commit 7dc10b21 authored by David Seaward's avatar David Seaward

create ldap user on registration (prototype integration of django-registration and django-ldapdb)

parent 5c67da2e
...@@ -60,3 +60,8 @@ SPDX-License-Identifier: GPL-3.0+ ...@@ -60,3 +60,8 @@ SPDX-License-Identifier: GPL-3.0+
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
Also includes code portions from:
* https://github.com/yourcelf/django-registration-defaults (Copyright 2010 Charlie DeTar, Expat/MIT)
* https://github.com/asyd/pyldap_orm/blob/master/pyldap_orm/controls.py (Copyright 2016 Bruno Bonfils, Apache 2.0)
default_app_config = 'ldapregister.apps.LdapRegisterConfig'
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User from .models import User, LdapGroup, LdapPerson
#
# Declare admin models
#
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
pass pass
class LdapGroupAdmin(admin.ModelAdmin):
exclude = ['dn', 'objectClass']
list_display = ['cn', 'description', ]
class LdapPersonAdmin(admin.ModelAdmin):
exclude = ['dn', 'objectClass']
list_display = ['uid', 'description', ]
#
# Register admin models
#
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(LdapGroup, LdapGroupAdmin)
admin.site.register(LdapPerson, LdapPersonAdmin)
from django.apps import AppConfig from django.apps import AppConfig
class LdapregisterConfig(AppConfig): class LdapRegisterConfig(AppConfig):
name = 'ldapregister' name = 'ldapregister'
verbose_name = 'LDAP register'
<!DOCTYPE html>
<html lang="en">
<head>
<!-- load static from static files -->
<meta charset="UTF-8">
<title>Puri.st services</title>
</head>
<body>
<h1>Puri.st services</h1>
<p>
You are logged in as $name.<br />
<a href="/accounts/logout/">Log out</a>
</p>
<p>
You are not logged in.<br />
<a href="/accounts/login/">Log in</a> or <a href="/accounts/register/">register</a>.
</p>
</body>
</html>
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-17 12:51
from __future__ import unicode_literals
from django.db import migrations, models
import ldapdb.models.fields
class Migration(migrations.Migration):
dependencies = [
('ldapregister', '0002_auto_20170310_1340'),
]
operations = [
migrations.CreateModel(
name='LdapGroup',
fields=[
('dn', models.CharField(max_length=200, primary_key=True, serialize=False)),
# ('cn', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
('cn', ldapdb.models.fields.CharField(db_column='cn', max_length=200, serialize=False)),
('description', ldapdb.models.fields.CharField(db_column='description', max_length=200)),
('members', ldapdb.models.fields.ListField(db_column='member')),
],
options={
'verbose_name_plural': 'LDAP groups',
'verbose_name': 'LDAP group',
},
),
migrations.CreateModel(
name='LdapPerson',
fields=[
('dn', models.CharField(max_length=200, primary_key=True, serialize=False)),
# ('uid', ldapdb.models.fields.CharField(db_column='uid', max_length=200, primary_key=True, serialize=False)),
('uid', ldapdb.models.fields.CharField(db_column='uid', max_length=200, serialize=False)),
('cn', ldapdb.models.fields.CharField(db_column='cn', max_length=200)),
('description', ldapdb.models.fields.CharField(db_column='description', max_length=200)),
('sn', ldapdb.models.fields.CharField(db_column='sn', max_length=200)),
],
options={
'verbose_name_plural': 'LDAP people',
'verbose_name': 'LDAP person',
},
),
]
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as BaseUserManager from django.contrib.auth.models import UserManager as BaseUserManager
from ldapdb.models.fields import CharField, ListField
import ldap
import ldapdb.models
import logging
import pyasn1.type.univ
import pyasn1.type.namedtype
import pyasn1.codec.ber.encoder
from django.db import connections, router
from django.utils.crypto import salted_hmac
log = logging.getLogger(__name__)
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
pass
def create_user(self, username, email=None, password=None, **extra_fields):
"""Create regular users in LDAP, and with no Django password."""
super(UserManager, self).create_user(username, None, None, **extra_fields)
def create_superuser(self, username, email=None, password=None, **extra_fields):
"""Create superusers with a Django password."""
super(UserManager, self).create_superuser(username, None, None, **extra_fields)
class User(AbstractUser): class User(AbstractUser):
objects = UserManager() objects = UserManager()
REQUIRED_FIELDS = ['email']
def __init__(self, *args, **kwargs):
return super(User, self).__init__(*args, **kwargs)
def validate_unique(self, exclude=None):
return super(User, self).validate_unique(exclude)
def get_ldap(self):
return LdapPerson.objects.get(uid=self.get_username())
def has_ldap(self):
result = LdapPerson.objects.filter(uid=self.get_username())
return len(result) == 1
def create_ldap(self):
username = self.get_username()
LdapPerson.objects.create(uid=username, cn=username, sn=username)
def set_ldap_password(self, raw_password):
ldap_person = self.get_ldap()
ldap_person.change_password(raw_password)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# save django user
super(User, self).save(force_insert, force_update, using, update_fields)
# create LDAP user (if required)
if not self.has_ldap():
self.create_ldap()
# force null password (will use LDAP password)
self.set_unusable_password()
def set_password(self, raw_password):
# force null password (will use LDAP password)
self.set_unusable_password()
# create LDAP user (if required)
if self.get_username():
if not self.has_ldap():
self.create_ldap()
# set LDAP password
self.set_ldap_password(raw_password)
def get_session_auth_hash(self):
"""
Return an HMAC of the password field.
"""
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
return salted_hmac(key_salt, self.get_username()).hexdigest() #FIXME: should use LDAP password value!
class LdapGroup(ldapdb.models.Model):
"""
Class for representing an LDAP group entry.
"""
class Meta:
verbose_name = "LDAP group"
verbose_name_plural = "LDAP groups"
# LDAP meta-data
base_dn = "dc=comms,dc=nodomain"
object_classes = ['groupOfNames', ]
# LDAP group attributes
cn = CharField(db_column='cn', max_length=200, primary_key=True)
description = CharField(db_column='description', max_length=200)
members = ListField(db_column='member')
def __str__(self):
return self.name
def __unicode__(self):
return self.name
class LdapPerson(ldapdb.models.Model):
"""
Class for representing an LDAP person entry.
"""
class Meta:
verbose_name = "LDAP person"
verbose_name_plural = "LDAP people"
# LDAP meta-data
base_dn = "ou=people,dc=comms,dc=nodomain"
object_classes = ['inetOrgPerson', 'organizationalPerson', 'person', ]
# Minimal attributes
uid = CharField(db_column='uid', max_length=200, primary_key=True)
cn = CharField(db_column='cn', max_length=200)
description = CharField(db_column='description', max_length=200)
sn = CharField(db_column='sn', max_length=200)
def __str__(self):
return self.uid
def __unicode__(self):
return self.uid
def change_password(self, raw_password, using=None):
# dig into the ldapdb primitives
using = using or router.db_for_write(self.__class__, instance=self)
connection = connections[using]
cursor = connection._cursor()
# call pyldap_orm password modification
cursor.connection.extop_s(PasswordModify(self.dn, raw_password))
# The following code taken from https://github.com/asyd/pyldap_orm/blob/master/pyldap_orm/controls.py
# Copyright 2016 Bruno Bonfils
# SPDX-License-Identifier: Apache-2.0 (no NOTICE file)
class PasswordModify(ldap.extop.ExtendedRequest):
"""
Implements RFC 3062, LDAP Password Modify Extended Operation
Reference: https://www.ietf.org/rfc/rfc3062.txt
"""
def __init__(self, identity, new, current=None):
self.requestName = '1.3.6.1.4.1.4203.1.11.1'
self.identity = identity
self.new = new
self.current = current
def encodedRequestValue(self):
request = self.PasswdModifyRequestValue()
request.setComponentByName('userIdentity', self.identity)
if self.current is not None:
request.setComponentByName('oldPasswd', self.current)
request.setComponentByName('newPasswd', self.new)
return pyasn1.codec.ber.encoder.encode(request)
class PasswdModifyRequestValue(pyasn1.type.univ.Sequence):
"""
PyASN1 representation of:
PasswdModifyRequestValue ::= SEQUENCE {
userIdentity [0] OCTET STRING OPTIONAL
oldPasswd [1] OCTET STRING OPTIONAL
newPasswd [2] OCTET STRING OPTIONAL }
"""
componentType = pyasn1.type.namedtype.NamedTypes(
pyasn1.type.namedtype.OptionalNamedType(
'userIdentity',
pyasn1.type.univ.OctetString().subtype(
implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 0)
)),
pyasn1.type.namedtype.OptionalNamedType(
'oldPasswd',
pyasn1.type.univ.OctetString().subtype(
implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 1)
)),
pyasn1.type.namedtype.OptionalNamedType(
'newPasswd',
pyasn1.type.univ.OctetString().subtype(
implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 2)
)),
)
{# The following code adapted from https://github.com/yourcelf/django-registration-defaults Copyright 2010 Charlie DeTar SPDX-License-Identifier: MIT (aka Expat) #}
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Register for an account" %}{% endblock %}
{% block content %}
<table>
<form method='post' action=''>{% csrf_token %}
{{ form }}
<tr><td></td><td><input type="submit" value="{% trans 'Register' %}" /></td></tr>
</form>
</table>
{% endblock %}
from django.shortcuts import render from django.shortcuts import render
# from registration.backends.simple.views import
# Create your views here. def home_page(request):
render_data = {
"username": request.user.get_username(),
}
return render(request, 'ldapregister/home.html', render_data)
...@@ -6,10 +6,14 @@ FIXME: switch to storing strict yaml in /etc/ ...@@ -6,10 +6,14 @@ FIXME: switch to storing strict yaml in /etc/
""" """
from .settings import * from .settings import *
from registration_defaults.settings import * # from registration_defaults.settings import *
import ldap import ldap
from django_auth_ldap.config import LDAPSearch from django_auth_ldap.config import LDAPSearch
#
# SECURITY
#
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'local_secret' SECRET_KEY = 'local_secret'
...@@ -19,6 +23,10 @@ DEBUG = False ...@@ -19,6 +23,10 @@ DEBUG = False
# Required if DEBUG is False # Required if DEBUG is False
ALLOWED_HOSTS = ['example.com'] ALLOWED_HOSTS = ['example.com']
#
# REGISTRATION APPLICATION
#
# INSTALLED_APPS = ["registration_defaults", ] + INSTALLED_APPS + ["ldapregister", ] # INSTALLED_APPS = ["registration_defaults", ] + INSTALLED_APPS + ["ldapregister", ]
INSTALLED_APPS += ["ldapregister", ] INSTALLED_APPS += ["ldapregister", ]
...@@ -41,5 +49,31 @@ AUTH_LDAP_START_TLS = True ...@@ -41,5 +49,31 @@ AUTH_LDAP_START_TLS = True
AUTH_LDAP_BIND_DN = "cn=application,dc=example,dc=com" AUTH_LDAP_BIND_DN = "cn=application,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD = "password" AUTH_LDAP_BIND_PASSWORD = "password"
AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
# must match `base_dn` and primary key in `ldapregister.models.LdapPerson`
AUTH_USER_MODEL = 'ldapregister.User' AUTH_USER_MODEL = 'ldapregister.User'
#
# DATABASE
#
# See also:
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# and https://pypi.python.org/pypi/django-ldapdb/
# (re-uses LDAP connection details from authentication settings, you can change this)
DATABASES = {
'ldap': {
'ENGINE': 'ldapdb.backends.ldap',
'NAME': AUTH_LDAP_SERVER_URI,
'USER': AUTH_LDAP_BIND_DN,
'PASSWORD': AUTH_LDAP_BIND_PASSWORD,
'TLS': AUTH_LDAP_START_TLS,
},
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
},
}
DATABASE_ROUTERS = ['ldapdb.router.Router']
...@@ -15,12 +15,16 @@ Including another URLconf ...@@ -15,12 +15,16 @@ Including another URLconf
""" """
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.generic import RedirectView
from registration.backends.simple.views import RegistrationView from registration.backends.simple.views import RegistrationView
import ldapregister.views
from ldapregister.forms import RegistrationForm from ldapregister.forms import RegistrationForm
urlpatterns = [ urlpatterns = [
url(r'^$', ldapregister.views.home_page, name='home_page'),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^accounts/profile/$', RedirectView.as_view(url='/')),
url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'), url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'),
url(r'^accounts/', include('registration.backends.simple.urls')), url(r'^accounts/', include('registration.backends.simple.urls')),
] ]
...@@ -3,4 +3,5 @@ django-auth-ldap==1.2.9 ...@@ -3,4 +3,5 @@ django-auth-ldap==1.2.9
django-registration==2.2 django-registration==2.2
Jinja2==2.9.5 Jinja2==2.9.5
jinja2-django-tags==0.5 jinja2-django-tags==0.5
pyasn1==0.2.3
Sphinx==1.5.3 Sphinx==1.5.3
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment