Commit a9c51b91 authored by David Seaward's avatar David Seaward Committed by Gogs

Merge branch 'user_refactor' of david.seaward/purist_middleware into master

parents ccb06dad 98a4d9c8
......@@ -4,6 +4,8 @@
docs/api/
*.pid
categories.json
confusables.json
### Basic template
......
......@@ -27,17 +27,17 @@ Prerequisites
* RabbitMQ server
* Must be accessible at `amqp://guest:guest@localhost:5672//`
* This can be achieved with just `apt install rabbitmq-server`
* Additional dependency packages:
* Additional dependencies (Debian package names):
* `libsasl2-dev`
* `libldap2-dev`
* `libssl-dev`
* `python3-dev`
* `supervisor`
* Additional uWSGI packages:
* `uwsgi`
* `uwsgi-emperor`
* `uwsgi-plugin-python3`
* Python/Django packages: see `requires/requirements.txt`
* `virtualenv`
* Python/Django dependencies: see `requires/requirements.txt`
* External resources:
* LDAP database
* WooCommerce instance (REST API)
......
......@@ -6,6 +6,8 @@ SITE_TITLE = Title
SITE_BYLINE = Example byline
SITE_DOMAIN = example.com
DEBUG = True
DEBUG_ALL_ACCESS = True
DEBUG_CHANGE_PASSWORD = False
DEBUG_SKIP_ACTIVATION_COMMAND = True
ALLOWED_HOSTS = localhost
STATIC_ROOT = /var/opt/purist/account/static
......
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):
# this currently has no effect
def __init__(self, request=None, *args, **kwargs):
super(AuthenticationForm, self).__init__(request, *args, **kwargs)
self.fields["password"].label = _("Passphrase")
class RegistrationForm(BaseRegistrationForm):
......
......@@ -2,56 +2,72 @@
<!--
Copyright 2017 Purism SPC and contributors
https://plan.puri.st/app/account_web
SPDX-License-Identifier: CC-BY-SA-4.0
https://plan.puri.st/module/middleware
SPDX-License-Identifier: AGPL-3.0
-->
<html lang="en">
<head>
<link rel="stylesheet" href="{% static 'PuristFlex.css' %}"/>
<link rel="stylesheet" href="{% static 'layout.css' %}"/>
<link rel="stylesheet" href="{% static 'theme.css' %}"/>
<title>{{ site_title }} - {{ site_byline }}</title>
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}">
<meta name="application-name" content="{{ site_title }}">
<meta charset="UTF-8">
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}"/>
<meta name="application-name" content="{{ site_title }}"/>
<meta charset="UTF-8"/>
</head>
<body style="text-align: center">
<body>
<p style="text-align: right">
{% if request.user.is_authenticated %}
{% trans "Logged in as" %} {{ username }}<br/>
<a href="{% url 'profile' %}">{% trans "Profile" %}</a> |
<header>
{% if request.user.is_superuser %}
<a href="{% url 'admin:index' %}">{% trans "Admin" %}</a> |
{% endif %}
<div id="title_box">
<a href="{% url 'home' %}"><img class="logo" src="{% static 'logo.png' %}" alt="{{ site_title }}"/></a>
<div id="title_text">
<h1>{{ site_title }}</h1>
<p>{{ site_byline }}</p>
</div>
</div>
<a href="{% url 'auth_logout' %}">{% trans "Log out" %}</a>
{% else %}
{% trans "You are not logged in." %}<br/>
<a href="{% url 'auth_login' %}">{% trans "Log in" %}</a> {% trans "or" %}
<a href="{% url 'registration_register' %}">{% trans "register." %}</a>
{% endif %}
</p>
<div id="log_state">
{% if request.user.is_authenticated %}
{% trans "Logged in as" %} {{ username }}<br/>
<a href="{% url 'profile' %}">{% trans "Profile" %}</a> |
<h1><img style="max-height: 50vh; max-width: 50vw" src="{% static 'logo.png' %}" alt="{{ site_title }}"/></h1>
{% if request.user.is_superuser %}
<a href="{% url 'admin:index' %}">{% trans "Admin" %}</a> |
{% endif %}
<p>{{ site_byline }}</p>
<a href="{% url 'auth_logout' %}">{% trans "Log out" %}</a>
{% else %}
{% trans "You are not logged in." %}<br/>
<a href="{% url 'auth_login' %}">{% trans "Log in" %}</a> {% trans "or" %}
<a href="{% url 'registration_register' %}">{% trans "register." %}</a>
{% endif %}
</div>
<p>
<a href="https://plan.puri.st">plan</a> | <a href="https://code.puri.sm/purist">code</a> |
<strong>test</strong> | <a href="https://puri.st">deliver</a>
</p>
</header>
<p style="font-size: small">
Services provided by <a href="https://puri.sm">Purism SPC</a>. Stand-in logo from
<a href="https://plan.puri.st/project/overview/design/logo">Openclipart</a> [CC0].
</p>
<hr/>
<p style="font-size: small">
Purist services middleware. Copyright 2016 Purism SPC and contributors. Shared under AGPL-3.0+
<a href="{% url 'download-zip' %}">Download source</a>
</p>
<main>
<article>
</article>
</main>
<footer>
<div id="footer_block">
<p>
<em>Purist services</em> provided by <a href="https://puri.sm">Purism SPC</a><br/>
<em>Services middleware</em> copyright 2017 Purism SPC and contributors; shared under AGPL-3.0+
(<a href="https://plan.puri.st">project</a>, <a href="{% url 'download-zip' %}">source</a>, <a href="">javascript</a>)
</p>
</div>
</footer>
</body>
</html>
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-10 10:34
# Generated by Django 1.11.6 on 2017-10-09 23:34
from __future__ import unicode_literals
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
import ldapdb.models.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
name='LdapGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150, unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False,
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status')),
('is_active', models.BooleanField(default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
related_name='user_set', related_query_name='user', to='auth.Group',
verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.',
related_name='user_set', related_query_name='user',
to='auth.Permission', verbose_name='user permissions')),
('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': 'user',
'verbose_name_plural': 'users',
'abstract': False,
'verbose_name': 'LDAP group',
'verbose_name_plural': 'LDAP groups',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
),
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',
},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-10 13:40
from __future__ import unicode_literals
from django.db import migrations
import ldapregister.models
class Migration(migrations.Migration):
dependencies = [
('ldapregister', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', ldapregister.models.UserManager()),
],
),
]
# -*- 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',
},
),
]
......@@ -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.
......
......@@ -2,27 +2,24 @@
{% load i18n %} {% load static %}
<!--
Page content, copyright 2017 Purism SPC and contributors
https://plan.puri.st/app/account_web
SPDX-License-Identifier: CC-BY-SA-4.0
Original registration template, copyright 2015 Anders Hofstee and contributors
https://github.com/RatanShreshtha/django-registration-templates
SPDX-License-Identifier: MIT
Copyright 2017 Purism SPC and contributors
https://plan.puri.st/module/middleware
SPDX-License-Identifier: AGPL-3.0
-->
<html lang="en">
<head>
<link rel="stylesheet" href="{% static 'PuristFlex.css' %}"/>
<link rel="stylesheet" href="{% static 'layout.css' %}"/>
<link rel="stylesheet" href="{% static 'theme.css' %}"/>
<title>{% block title %}Base{% endblock %}</title>
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}">
<meta name="application-name" content="Purist">
<meta charset="UTF-8">
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}"/>
<meta name="application-name" content="Purist"/>
<meta charset="UTF-8"/>
</head>
<body>
<header style="justify-content: space-between;">
<header>
<div id="title_box">
<a href="{% url 'home' %}"><img class="logo" src="{% static 'logo.png' %}" alt="Purist"/></a>
<div id="title_text">
......@@ -47,11 +44,27 @@ SPDX-License-Identifier: MIT
</header>
<div id="main">
<hr/>
<main>
<article>
{% block content %}{% endblock %}
</article>
</div>
</main>
<footer>
<div id="footer_block">
<p>
<em>Purist services</em> provided by <a href="https://puri.sm">Purism SPC</a><br/>
<em>Services middleware</em> copyright 2017 Purism SPC and contributors; shared under AGPL-3.0+
(<a href="https://plan.puri.st">project</a>, <a href="{% url 'download-zip' %}">source</a>, <a href="">javascript</a>)
</p>
</div>
</footer>
</body>
</html>
......@@ -5,23 +5,20 @@
{% block byline %}{% trans 'Please fill in your credentials' %}{% endblock %}
{% block content %}
<br/>
<!-- Main Content -->
<main class="container">
<section class="row">
<section class="col-lg-4 col-lg-offset-4 col-md-4 col-md-offset-5">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
<section class="row">
<section class="form">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
<div class="submit-group">
<input type="submit" value="{% trans 'Log in' %}"/>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</section>
</div>
</form>
</section>
</main>
</section>
<p class="text-center">{% trans "No account yet?" %}
<a href="{% url 'registration_register' %}">{% trans "Register!" %}</a>
</p>
{% endblock %}
......@@ -8,3 +8,4 @@
{% block content %}
{% endblock %}
{% 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 %}
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans 'Closed' %}{% endblock %}
{% block header %}{% trans 'Closed' %}{% endblock %}
{% block byline %}{% trans 'Registration has been disabled on this instance' %}{% endblock %}
{% block content %}
{% endblock %}
......@@ -5,17 +5,18 @@
{% block byline %}{% trans 'Please fill in your registration details' %}{% endblock %}
{% block content %}
<br/>
<!-- Main Content -->
<main class="container">
<section class="row">
<section class="col-lg-4 col-lg-offset-4 col-md-4 col-md-offset-5">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
<input type="submit" value="{% trans 'Submit' %}"/>
</form>
<section class="row">
<section class="form">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
<div class="submit-group">
<input type="submit" value="{% trans 'Submit' %}"/>
</div>
</form>
</section>
</section>
</main>
</section>
{% endblock %}
from django.conf import settings
from django.shortcuts import render
# from django.contrib.auth import views as auth_views
def home(request):
......
......@@ -2,29 +2,32 @@
<!--
Copyright 2017 Purism SPC and contributors
https://plan.puri.st/app/account_web
SPDX-License-Identifier: CC-BY-SA-4.0
https://plan.puri.st/module/middleware
SPDX-License-Identifier: AGPL-3.0
-->
<html lang="en">
<head>
<link rel="stylesheet" href="{% static 'PuristFlex.css' %}"/>
<link rel="stylesheet" href="{% static 'layout.css' %}"/>
<link rel="stylesheet" href="{% static 'theme.css' %}"/>
<title>{% trans "User profile" %}</title>
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}">
<meta name="application-name" content="{{ site_title }}">
<meta charset="UTF-8">
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}"/>
<meta name="application-name" content="{{ site_title }}"/>
<meta charset="UTF-8"/>
</head>
<body>
<header style="justify-content: space-between;">
<header>
<div id="title_box">
<a href="{% url 'home' %}"><img class="logo" src="{% static 'logo.png' %}" alt="Purist"/></a>
<a href="{% url 'home' %}"><img class="logo" src="{% static 'logo.png' %}" alt="{{ site_title }}"/></a>
<div id="title_text">
<h1>{% trans "User profile" %}</h1>
<p>{% trans "Service credit and profile management" %}</p>
</div>
</div>
<div id="log_state">
{% if request.user.is_authenticated %}
{% trans "Logged in as" %} {{ username }}<br/>
......@@ -44,12 +47,14 @@ SPDX-License-Identifier: CC-BY-SA-4.0
</header>
<div id="main">
<hr/>
<main>
<article>
<h2>{% trans "Services" %}</h2>