diff --git a/README.md b/README.md index 4b13140029e457585b4edd4ec7d727227487a29f..dded13fe9b950062aa9a02e66ff8292dc6abcb87 100755 --- a/README.md +++ b/README.md @@ -60,3 +60,8 @@ SPDX-License-Identifier: GPL-3.0+ You should have received a copy of the GNU General Public License along with this program. If not, see . + +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) diff --git a/ldapregister/templates/ldapregister/placeholder b/TODO.md similarity index 100% rename from ldapregister/templates/ldapregister/placeholder rename to TODO.md diff --git a/ldapregister/__init__.py b/ldapregister/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..aef8adc211e89fe6d1ef555086f00b7ee9da817c 100755 --- a/ldapregister/__init__.py +++ b/ldapregister/__init__.py @@ -0,0 +1 @@ +default_app_config = 'ldapregister.apps.LdapRegisterConfig' diff --git a/ldapregister/admin.py b/ldapregister/admin.py index c3a3607600cc26f074d848b01f5c57314390c607..8f330084ae33ba2846edd064b91325dd213a7b2a 100755 --- a/ldapregister/admin.py +++ b/ldapregister/admin.py @@ -1,11 +1,30 @@ from django.contrib import admin 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): 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(LdapGroup, LdapGroupAdmin) +admin.site.register(LdapPerson, LdapPersonAdmin) diff --git a/ldapregister/apps.py b/ldapregister/apps.py index 6e533406e46ebb4769799d3638620b0328e2db57..2d746d2a6ca3598eb98b3247b29c41414536f088 100755 --- a/ldapregister/apps.py +++ b/ldapregister/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig -class LdapregisterConfig(AppConfig): +class LdapRegisterConfig(AppConfig): name = 'ldapregister' + verbose_name = 'LDAP register' diff --git a/ldapregister/jinja2/ldapregister/home.html b/ldapregister/jinja2/ldapregister/home.html new file mode 100644 index 0000000000000000000000000000000000000000..bee070a0605f5853311d0f43c412ae61b5208f4e --- /dev/null +++ b/ldapregister/jinja2/ldapregister/home.html @@ -0,0 +1,24 @@ + + + + + + Puri.st services + + + + +

Puri.st services

+ +

+ You are logged in as $name.
+ Log out +

+ +

+ You are not logged in.
+ Log in or register. +

+ + + diff --git a/ldapregister/migrations/0003_ldapgroup_ldapperson.py b/ldapregister/migrations/0003_ldapgroup_ldapperson.py new file mode 100755 index 0000000000000000000000000000000000000000..13d4f218b99283a3dfebfe1ae94f17796bde2289 --- /dev/null +++ b/ldapregister/migrations/0003_ldapgroup_ldapperson.py @@ -0,0 +1,45 @@ +# -*- 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', + }, + ), + ] diff --git a/ldapregister/models.py b/ldapregister/models.py index 8c19860c62bf256c0ae0b9181787e4dd32e47a34..c97f91c5497725c7a43e8edf736909d03dfd6c77 100755 --- a/ldapregister/models.py +++ b/ldapregister/models.py @@ -1,10 +1,201 @@ from django.contrib.auth.models import AbstractUser 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): - 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): + 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) + )), +) diff --git a/ldapregister/templates/registration/login.html b/ldapregister/templates/registration/login.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ldapregister/templates/registration/logout.html b/ldapregister/templates/registration/logout.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ldapregister/templates/registration/registration_form.html b/ldapregister/templates/registration/registration_form.html new file mode 100755 index 0000000000000000000000000000000000000000..8028b9b99627721bd55d49c717d48660948346e5 --- /dev/null +++ b/ldapregister/templates/registration/registration_form.html @@ -0,0 +1,13 @@ +{# 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 %} + + {% csrf_token %} + {{ form }} + + +
+{% endblock %} diff --git a/ldapregister/views.py b/ldapregister/views.py index 91ea44a218fbd2f408430959283f0419c921093e..4faf8ccd53aa1e88094dfb9542fd9b50f2dd991a 100755 --- a/ldapregister/views.py +++ b/ldapregister/views.py @@ -1,3 +1,10 @@ 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) diff --git a/purist_account/settings_local.txt b/purist_account/settings_local.txt index b020d519f632bc2a5df5c5edcc2e28e8532382b8..760bdd144e9761f392881ab986794a11f0162c80 100755 --- a/purist_account/settings_local.txt +++ b/purist_account/settings_local.txt @@ -6,10 +6,14 @@ FIXME: switch to storing strict yaml in /etc/ """ from .settings import * -from registration_defaults.settings import * +# from registration_defaults.settings import * import ldap from django_auth_ldap.config import LDAPSearch +# +# SECURITY +# + # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'local_secret' @@ -19,6 +23,10 @@ DEBUG = False # Required if DEBUG is False ALLOWED_HOSTS = ['example.com'] +# +# REGISTRATION APPLICATION +# + # INSTALLED_APPS = ["registration_defaults", ] + INSTALLED_APPS + ["ldapregister", ] INSTALLED_APPS += ["ldapregister", ] @@ -41,5 +49,31 @@ AUTH_LDAP_START_TLS = True AUTH_LDAP_BIND_DN = "cn=application,dc=example,dc=com" AUTH_LDAP_BIND_PASSWORD = "password" 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' + +# +# 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'] diff --git a/purist_account/urls.py b/purist_account/urls.py index 18fa9a119025a71feff6a77e4b1e99077e69f844..f5d680ba3cb9ec0dde884949b014613b109da9b4 100755 --- a/purist_account/urls.py +++ b/purist_account/urls.py @@ -15,12 +15,16 @@ Including another URLconf """ from django.conf.urls import include, url from django.contrib import admin +from django.views.generic import RedirectView from registration.backends.simple.views import RegistrationView +import ldapregister.views from ldapregister.forms import RegistrationForm urlpatterns = [ + url(r'^$', ldapregister.views.home_page, name='home_page'), 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/', include('registration.backends.simple.urls')), ] diff --git a/requirements.txt b/requirements.txt index 0276c79034fc0f4bf52d7f98c6aa866f368bda71..b4bae62629d7eb4a1374336583308c4b6b6d5f2b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ django-auth-ldap==1.2.9 django-registration==2.2 Jinja2==2.9.5 jinja2-django-tags==0.5 +pyasn1==0.2.3 Sphinx==1.5.3