Commit 97c73f74 authored by David Seaward's avatar David Seaward

refactor core models and utilties: move from general-purpose apps to new app

* move User model, custom AuthenticationBackend and custom PassphraseValidator
* recreate migrations for first use
* add more registration/password templates
* new settings flags: DEBUG_ALL_ACCESS and DEBUG_CHANGE_PASSWORD
* minor UX tweaks
parent 7544397e
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, LdapGroup, LdapPerson
from .models import LdapGroup, LdapPerson
#
# Declare admin models
#
class UserAdmin(BaseUserAdmin):
pass
class LdapGroupAdmin(admin.ModelAdmin):
exclude = ['dn', 'objectClass']
list_display = ['cn', 'description', ]
......@@ -25,6 +18,5 @@ class LdapPersonAdmin(admin.ModelAdmin):
# Register admin models
#
admin.site.register(User, UserAdmin)
admin.site.register(LdapGroup, LdapGroupAdmin)
admin.site.register(LdapPerson, LdapPersonAdmin)
from django.utils.translation import ugettext_lazy as _
from registration.forms import RegistrationForm as BaseRegistrationForm
from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
from django.contrib.auth import get_user_model
from .models import User
User = get_user_model()
class AuthenticationForm(BaseAuthenticationForm):
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-09 23:34
from __future__ import unicode_literals
from django.db import migrations, models
import ldapdb.models.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='LdapGroup',
fields=[
('dn', models.CharField(max_length=200, primary_key=False, serialize=False)),
('cn', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
('description', ldapdb.models.fields.CharField(db_column='description', max_length=200)),
('members', ldapdb.models.fields.ListField(db_column='member')),
],
options={
'verbose_name': 'LDAP group',
'verbose_name_plural': 'LDAP groups',
},
),
migrations.CreateModel(
name='LdapPerson',
fields=[
('dn', models.CharField(max_length=200, primary_key=False, serialize=False)),
('uid', ldapdb.models.fields.CharField(db_column='uid', max_length=200, primary_key=True, 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': 'LDAP person',
'verbose_name_plural': 'LDAP people',
},
),
]
......@@ -6,121 +6,13 @@ import pyasn1.codec.ber.encoder
import pyasn1.type.namedtype
import pyasn1.type.univ
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as BaseUserManager
from django.core.exceptions import ValidationError
from django.db import connections, router
from django.utils.crypto import salted_hmac
from django.utils.translation import ugettext as _
from ldapdb.models.fields import CharField, ListField
from limitmonitor import models as limitmonitor_models
log = logging.getLogger(__name__)
class WordCountValidator(object):
def __init__(self, min_words=3):
self.min_words = min_words
def validate(self, password, user=None):
is_valid_count = password.count(' ') > (self.min_words - 1)
if not is_valid_count:
raise ValidationError(self.get_help_text())
def get_help_text(self):
return _('Passphrase must contain at least three words. Use spaces between words.')
class UserManager(BaseUserManager):
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, email, password, **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, email, password, **extra_fields)
class User(AbstractUser):
objects = UserManager()
REQUIRED_FIELDS = []
def __init__(self, *args, **kwargs):
user = super(User, self).__init__(*args, **kwargs)
return user
@classmethod
def normalize_username(cls, username):
username = super(User, cls).normalize_username(username)
return username.lower()
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 get_identity(self):
# FIXME: this doesn't belong in ldapregister
# associated with https://code.puri.sm/purist/account_web/issues/25
return self.get_username() + "@" + settings.SITE_DOMAIN
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()
# create any missing limits
# FIXME: this make ldapregister dependant on limitmonitor
# https://code.puri.sm/purist/account_web/issues/25
limitmonitor_models.create_missing_user_limits(self)
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.
......
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans 'Changed' %}{% endblock %}
{% block header %}{% trans 'Changed' %}{% endblock %}
{% block byline %}{% trans 'Your passphrase has been changed' %}{% endblock %}
{% block content %}
{% endblock %}
{% extends "base.html" %} {% load i18n %} {% load crispy_forms_tags %}
{% block title %}{% trans 'Change passphrase' %}{% endblock %}
{% block header %}{% trans 'Change passphrase' %}{% endblock %}
{% block byline %}{% trans 'Please enter your old passphrase, then your new passphrase twice' %}{% endblock %}
{% block content %}
<!-- Main Content -->
<section class="row">
<section class="form">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
<div class="submit-group">
<input type="submit" value="{% trans 'Change my passphrase' %}"/>
</div>
</form>
</section>
</section>
{% endblock %}
......@@ -2,9 +2,9 @@
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans 'Registration closed' %}{% endblock %}
{% block header %}{% trans 'Registration closed' %}{% endblock %}
{% block byline %}{% trans 'Services are not yet available' %}{% endblock %}
{% block title %}{% trans 'Closed' %}{% endblock %}
{% block header %}{% trans 'Closed' %}{% endblock %}
{% block byline %}{% trans 'Registration has been disabled on this instance' %}{% endblock %}
{% block content %}
{% endblock %}
......
from django.conf import settings
from django.shortcuts import render
# from django.contrib.auth import views as auth_views
def home(request):
......
......@@ -83,8 +83,10 @@ SPDX-License-Identifier: AGPL-3.0
<h2>{% trans "Profile management" %}</h2>
<ul>
{% if DEBUG_CHANGE_PASSWORD %}
<li><a href="{% url 'auth_password_change' %}">{% trans "Change password" %}</a></li>
<li><a href="{{ link_subscription }}" target="_blank">{% trans "Manage subscriptions" %}</a></li>
{% endif %}
</ul>
<h2>{% trans "Downloads" %}</h2>
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-09 23:34
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Credit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time_credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('volume_credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('old_expiry_date', models.DateTimeField(blank=True, default=None, null=True)),
('old_time_total', models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=6, null=True)),
('old_volume_total', models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=6, null=True)),
('new_expiry_date', models.DateTimeField(blank=True, default=None, null=True)),
('new_time_total', models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=6, null=True)),
('new_volume_total', models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=6, null=True)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('updated_date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.CreateModel(
name='ExternalBundle',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parser', models.CharField(choices=[('WOO1', 'WooCommerce v1'), ('WOOSUB1', 'WooCommerce Subscription v1')], max_length=30)),
('external_key', models.CharField(max_length=30)),
('service', models.CharField(choices=[('TUNNEL', 'Tunnel'), ('COMMUNICATION', 'Communication')], max_length=30)),
('time_credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('volume_credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('updated_date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.CreateModel(
name='ExternalCredit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parser', models.CharField(choices=[('WOO1', 'WooCommerce v1'), ('WOOSUB1', 'WooCommerce Subscription v1')], max_length=30)),
('external_key', models.CharField(max_length=30)),
('label', models.CharField(max_length=30)),
('bundle_key', models.CharField(max_length=30)),
('bundle_label', models.CharField(max_length=30)),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=6)),
('account_name', models.CharField(default='', max_length=30)),
('additional_data', models.TextField(default='')),
('error_message', models.TextField(default='')),
('is_converted', models.BooleanField(default=False)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('updated_date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.CreateModel(
name='Limit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('service', models.CharField(choices=[('TUNNEL', 'Tunnel'), ('COMMUNICATION', 'Communication')], max_length=30)),
('renewal_date', models.DateTimeField(blank=True, default=None, null=True)),
('expiry_date', models.DateTimeField(blank=True, default=None, null=True)),
('volume_total', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('time_total', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('is_active', models.BooleanField(default=False)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('updated_date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-09 23:34
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('limitmonitor', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='limit',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='credit',
name='external',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='limitmonitor.ExternalCredit'),
),
migrations.AddField(
model_name='credit',
name='limit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='limitmonitor.Limit'),
),
]
......@@ -23,6 +23,7 @@ def userlimit(request):
has_limit["NONE"] = none_limit # true if no limits are active
render_data = {
"DEBUG_CHANGE_PASSWORD": settings.DEBUG_CHANGE_PASSWORD,
"username": username,
"site_title": settings.SITE_TITLE,
"site_byline": settings.SITE_BYLINE,
......
default_app_config = 'purist.apps.PuristConfig'
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
#
# Declare admin models
#
class UserAdmin(BaseUserAdmin):
pass
#
# Register admin models
#
admin.site.register(User, UserAdmin)
from django.apps import AppConfig
class PuristConfig(AppConfig):
name = 'purist'
......@@ -8,21 +8,24 @@ from django.utils.translation import ungettext
log = logging.getLogger(__name__)
class Backend(BaseBackend):
class AuthenticationBackend(BaseBackend):
def __init__(self, *args, **kwargs):
super(Backend, self).__init__(*args, **kwargs)
super(AuthenticationBackend, self).__init__(*args, **kwargs)
def ldap_to_django_username(self, username):
return username.lower().replace(".", "")
# def ldap_to_django_username(self, username):
# return username.lower().replace(".", "")
class Validator(BaseValidator):
class PassphraseValidator(BaseValidator):
# TODO: bundle in all the other validators from django.contrib.auth.password_validation
def __init__(self, *args, **kwargs):
super(Validator, self).__init__(*args, **kwargs)
def __init__(self, min_length=15, *args, **kwargs):
super(PassphraseValidator, self).__init__(min_length=15, *args, **kwargs)
def get_help_text(self):
return ungettext(
"A good passphrase is made of at least three long words.",
"A good passphrase is made of at least three long words.",
0
)
import logging
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as BaseUserManager
from django.utils.crypto import salted_hmac
from django.utils import timezone
from django.conf import settings
from limitmonitor import models as limitmonitor_models
from limitmonitor.task_resources import common as limitmonitor_common
from ldapregister.models import LdapPerson
log = logging.getLogger(__name__)
class UserManager(BaseUserManager):
def create_user(self, username, email=None, password=None, **extra_fields):
"""Create regular users in LDAP, with no Django password."""
user = super(UserManager, self).create_user(username, email, password, **extra_fields)
if settings.DEBUG_ALL_ACCESS:
ssh = limitmonitor_common.get_openvpn_ssh_connection()
renewal_date = timezone.now() + timezone.timedelta(weeks=5200)
for limit in limitmonitor_models.Limit.objects.filter(user=user, service="TUNNEL"):
limitmonitor_common.activate(ssh, limit, renewal_date=renewal_date)
def create_superuser(self, username, email=None, password=None, **extra_fields):
"""Create superusers with a Django password."""
super(UserManager, self).create_superuser(username, email, password, **extra_fields)
class User(AbstractUser):
objects = UserManager()
REQUIRED_FIELDS = []
def __init__(self, *args, **kwargs):
user = super(User, self).__init__(*args, **kwargs)
return user
@classmethod
def normalize_username(cls, username):
username = super(User, cls).normalize_username(username)
return username.lower()
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 get_identity(self):
# TODO: associated with https://code.puri.sm/purist/account_web/issues/25
return self.get_username() + "@" + settings.SITE_DOMAIN
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 instead)
self.set_unusable_password()
# create any missing limits
limitmonitor_models.create_missing_user_limits(self)
def set_password(self, raw_password):
# force null Django 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!
from django.test import TestCase
# Create your tests here.
from django.shortcuts import render
# Create your views here.
......@@ -23,16 +23,18 @@ SECRET_KEY = secret_config("DJANGO_SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config("DEBUG", cast=bool)
DEBUG_ALL_ACCESS = config("DEBUG_ALL_ACCESS", cast=bool)
DEBUG_CHANGE_PASSWORD = config("DEBUG_CHANGE_PASSWORD", cast=bool)
DEBUG_SKIP_ACTIVATION_COMMAND = config("DEBUG_SKIP_ACTIVATION_COMMAND", cast=bool)
# Required if DEBUG is False
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
#
# REGISTRATION APPLICATION
# INSTALLED APPLICATIONS
#
INSTALLED_APPS += ["crispy_forms", "django_agpl", "django_celery_beat", "ldapregister", "limitmonitor", ]
INSTALLED_APPS += ["crispy_forms", "django_agpl", "django_celery_beat", "ldapregister", "limitmonitor", "purist"]
#
# AGPL APPLICATION
......@@ -66,21 +68,12 @@ REG_GROUP_OBJECT_CLASSES = config("REG_GROUP_OBJECT_CLASSES", cast=Csv())
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 15,
}
},
{
'NAME': 'ldapregister.models.WordCountValidator',
'OPTIONS': {
'min_words': 3,
}
'NAME': 'purist.custom.PassphraseValidator',
},
]
AUTHENTICATION_BACKENDS = (
'django_auth_ldap.backend.LDAPBackend',
'purist.custom.AuthenticationBackend',
)
AUTH_LDAP_SERVER_URI = config("AUTH_LDAP_SERVER_URI")
......@@ -93,7 +86,7 @@ BASE_DN = config("AUTH_LDAP_USER_SEARCH_BASE_DN")
AUTH_LDAP_USER_SEARCH = LDAPSearch(BASE_DN, 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 = 'purist.User'
#
# DATABASE
......
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