Merge branch 'william' into 'master'

William

See merge request downhill/vuejs-userservice!2
This commit is contained in:
wcolmenares 2019-08-20 02:41:25 +02:00
commit 7936785ca8
255 changed files with 2094 additions and 1074 deletions

42
.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
*.log
db.sqlite3
*.pyc
*.DS_Store
build/
*.egg_info
#editors && utilites.
*.swp
*~
__pycache__/
.ropeproject/
#django
local_settings.py
!.keep
media/
!media/keep
/CACHE/
/static/
\#*#
.\#*
*~
secret-key
node_modules/
*.db
ungleich.db
*~*
secret-key
*.psd
.idea/
.env
*.mo
venv/
dal/ldap_max_uid_file

View file

@ -1,15 +0,0 @@
from django.db import models
# Basic DB to correlate tokens, users and creation time
class ResetToken(models.Model):
# users wouldn't use usernames >100 chars
user = models.CharField(max_length=100)
# Not so sure about tokens, better make it big
# should be <100, but big usernames make bigger tokens
# if I read that correctly
token = models.CharField(max_length=255)
# creation time in epoch (UTC)
# BigInt just so we are save for the next few decades ;)
creation = models.BigIntegerField()

View file

@ -1,254 +0,0 @@
"""
Django settings for dal project.
Generated by 'django-admin startproject' using Django 1.10.7.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from configparser import ConfigParser
config = ConfigParser()
config.read('userservice.conf')
# LDAP config
AUTH_LDAP_SERVER_URI = config['LDAP']['LDAPSERVER']
# The search user
AUTH_LDAP_BIND_DN = config['LDAP']['SEARCHUSER']
# The password for the search user
AUTH_LDAP_BIND_PASSWORD = config.get('LDAP','SEARCHUSERPASSWORD', raw=True)
# Search union over two ou
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
LDAPSearch("ou=users,dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"),
LDAPSearch("ou=customers,dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"),
)
# Basic User
#AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=ungleich,dc=ch"
# Search over just one ou
#AUTH_LDAP_USER_SEARCH = LDAPSearch( LDAPSearch("ou=users,dc=ungleich,dc=ch",
# ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
# )
# Maps some user keys since ldap has extensive infos
#AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn"}
# Maps some profile keys since ldap has extensive infos
#AUTH_LDAP_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"}
# LDAP config end
# Django nameko config
# Where's the Rabbitmq at
NAMEKO_CONFIG = {
'AMQP_URI': 'amqp://%s' % config['System']['RABBITMQ']
}
# Standard pool size
NAMEKO_POOL_SIZE = 4
# Django nameko config end
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_ROOT = os.path.dirname('/home/downhill/ungleich/vuejsuserservice/dal/dal/static/')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'rn=f&ecp#&#escxpk!0e%a$i3sbm$z@5+g4h9q+w7-83*f2f-i'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrap3',
'sekizai',
'dal',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Backend for auth
AUTHENTICATION_BACKENDS = (
'django_auth_ldap.backend.LDAPBackend',
# we only use LDAP for this service, so no auth against the standard DB
# 'django.contrib.auth.backends.ModelBackend',
)
ROOT_URLCONF = 'dal.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
],
},
},
]
WSGI_APPLICATION = 'dal.wsgi.application'
# Django Bootstrap - Settings
# Added Configuration for bootstrap static files to load over https.
BOOTSTRAP3 = {
# The URL to the jQuery JavaScript file
'jquery_url': '//code.jquery.com/jquery.min.js',
# The Bootstrap base URL
'base_url': '//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/',
# The complete URL to the Bootstrap CSS file
# (None means derive it from base_url)
'css_url': None,
# The complete URL to the Bootstrap CSS file (None means no theme)
'theme_url': None,
# The complete URL to the Bootstrap JavaScript file
# (None means derive it from base_url)
'javascript_url': None,
# Put JavaScript in the HEAD section of the HTML document
# (only relevant if you use bootstrap3.html)
'javascript_in_head': False,
# Include jQuery with Bootstrap JavaScript
# (affects django-bootstrap3 template tags)
'include_jquery': False,
# Label class to use in horizontal forms
'horizontal_label_class': 'col-md-3',
# Field class to use in horizontal forms
'horizontal_field_class': 'col-md-9',
# Set HTML required attribute on required fields
'set_required': True,
# Set HTML disabled attribute on disabled fields
'set_disabled': False,
# Set placeholder attributes to label if no placeholder is provided
'set_placeholder': True,
# Class to indicate required (better to set this in your Django form)
'required_css_class': '',
# Class to indicate error (better to set this in your Django form)
'error_css_class': 'has-error',
# Class to indicate success, meaning the field has valid input
# (better to set this in your Django form)
'success_css_class': 'has-success',
# Renderers (only set these if you have studied the source and understand
# the inner workings)
'formset_renderers': {
'default': 'bootstrap3.renderers.FormsetRenderer',
},
'form_renderers': {
'default': 'bootstrap3.renderers.FormRenderer',
},
'field_renderers': {
'default': 'bootstrap3.renderers.FieldRenderer',
'inline': 'bootstrap3.renderers.InlineFieldRenderer',
},
}
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'

File diff suppressed because one or more lines are too long

View file

@ -1,14 +0,0 @@
<title> Userdata changed. </title>
<h2> The data for {{user}} has been changed. </h2>
<br><br>
<ul>
<li> Username: {{user}} </li>
<li> Firstname: {{firstname}} </li>
<li> Lastname: {{lastname}} </li>
<li> Email: {{email}} </li>
</ul>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,7 +0,0 @@
<title> Password for {{user}} changed. </title>
<h2> The password for {{user}} has been changed. </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,20 +0,0 @@
<title> Changing the password for {{user}} </title>
<h2> Changing the password for {{user}} </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>
<br><br>
To change the password for {{user}}, please supply
<form action={% url 'change_password' %} method="post">
{% csrf_token %}
<br>The old password:<br>
<input type="password" name="oldpassword" id="oldpassword">
<br><br>The new password (at least 8 characters):<br>
<input type="password" name="password1" id="password1">
<br>Please repeat the new Password:<br>
<input type="password" name="password2" id="password2">
<br><br>
<input type="submit" value="Submit">
</form>

View file

@ -1,19 +0,0 @@
<title> Changing user data for {{user}} </title>
<h2> Changing user data for {{user}} </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>
<br><br>
<form action={% url 'change_data' %} method="post">
{% csrf_token %}
<br>Firstname:<br>
<input type="text" name="firstname" id="firstname" value="{{firstname}}">
<br><br>Lastname:<br>
<input type="text" name="lastname" id="lastname" value="{{lastname}}">
<br><br>Email:<br>
<input type="text" name="email" id="email" value="{{email}}">
<br><br>
<input type="submit" value="Submit">
</form>

View file

@ -1,18 +0,0 @@
<title> Deleting an Account </title>
<h2> Deleting an Account </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>
<br><br>
To delete an account, please type the username and password below:
<form action={% url 'account_delete' %} method="post">
{% csrf_token %}
<br><br>Username:<br>
<input type="text" name="username" id="username">
<br><br>Password:<br>
<input type="password" name="password" id="password">
<br><br>
<input type="submit" value="Submit">
</form>

View file

@ -1,7 +0,0 @@
<title> Deleted user {{user}} </title>
<h2> The user {{user}} was deleted from our system. </h2>
<br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,17 +0,0 @@
<title> An error has occurred! </title>
<h2> We are sorry, an error has occured while handling your request. </h2>
While trying to {{service}}, an error was encountered: {{error}}
<br><br>
You can try to:
<br>
{% if urlname %}
<form action={% url urlname %} method="get">
<input type="submit" value="Go back and try again">
</form>
<br>or<br>
{% endif %}
<form action={% url 'index' %} method="get">
<input type="submit" value="Go to the indexpage">
</form>

View file

@ -1,44 +0,0 @@
{% load i18n %}
<style>
.col-lg-12 {
background-color: grey;
color: white;
}
.list-inline {
background-color: grey;
color: white;
}
</style>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-12">
<ul class="list-inline">
<li class="col-lg-12">
<a href="#">Home</a>
</li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#about">How it works</a></li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#about">Your infrastructure</a></li>
<li>&sdot;</li>
<li>
<a href="#about">Our infrastructure</a></li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#services">Pricing</a>
</li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#contact">Contact</a>
</li>
</ul>
<p class="copyright text-muted small">Copyright &copy; ungleich glarus ag {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
</div>
</div>
</div>
</footer>

View file

@ -1,45 +0,0 @@
{% extends "base_short.html" %}
{% load staticfiles bootstrap3 %}
{% block content %}
<style>
.auth-footer {
color: black;
}
a:link { color: #000000 }
</style>
<div class="auth_container">
<div class="auth_bg"></div>
<div class="auth_center">
<div class="auth_content">
<div class="auth-box">
<h2 class="section-heading allcaps"> Login </h2>
{% include 'includes/_messages.html' %}
<form action={% url 'index' %} method="post" class="form" nonvalidated>
{% csrf_token %}
<div class="text-center">
<div class="form-group"><label class="sr-only control-label" for="username">Username</label><input class="form-control" type="text" name="username" id="username" placeholder="Username"></div>
<div class="form-group"><label class="sr-only control-label" for="password">Password</label><input class="form-control" type="password" name="password" id="password" placeholder="Password"></div>
<br><br>
<button type="submit" class="btn choice-btn"> Log in </button>
</div>
</form>
<div class="auth-footer">
<div>
Don't have an account yet?&nbsp;
<a href={% url 'register' %}> Sign up </a>
</div>
<div>
or <a href={% url 'reset_password' %}> Reset your password </a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,11 +0,0 @@
<title> Login failed! </title>
<h2> Sorry, but your login has failed </h2>
<br><br>This service runs for our LDAP users, so maybe you don't already have an LDAP account with us? If so, please register one.
<form action={% url 'register' %} method="get">
<input type="submit" value="Register an user">
</form>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,7 +0,0 @@
<title> You must be logged in to access this page </title>
<h2> You must be logged in to access this page </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,27 +0,0 @@
<title> Register an user at ungleich </title>
<h2> Register an user at ungleich </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>
<br><br>
To register yourself an user, please fill out the fields below:
<br>
<form action={% url 'register' %} method="post">
{% csrf_token %}
<br>Username (alphanumeric):<br>
<input type="text" name="username" id="username">
<br>Password (at least 8 characters):<br>
<input type="password" name="password1" id="password1">
<br>Please confirm your Password:<br>
<input type="password" name="password2" id="password2">
<br>Firstname:<br>
<input type="text" name="firstname" id="firstname">
<br>Lastname:<br>
<input type="text" name="lastname" id="lastname">
<br>Emailaddress:<br>
<input type="text" name="email" id="email">
<br>
<input type="submit" value="Submit">
</form>

View file

@ -1,13 +0,0 @@
<title> Password reset </title>
<h2> Password reset </h2>
<br><br>
To reset your password, please enter your username below. You will get an email with a link to change your password.
<br><br>
<form action={% url 'reset_password' %} method="post">
{% csrf_token %}
Username:<br>
<input type="text" name="user" id="user">
<br>
<input type="submit" value="Submit">
</form>

View file

@ -1,14 +0,0 @@
<title> Set new password for {{user}} </title>
<h2> Please set new password for {{user}} </h2>
<br><br>
<form action={% url 'reset' %} method="post">
{% csrf_token %}
New Password:<br>
<input type="password" name="password1" id="password1">
<br>Please confirm new password:<br>
<input type="password" name="password2" id="password2">
<br>
<input type="hidden" name="user" id="user" value="{{user}}">
<input type="submit" value="Submit">
</form>

View file

@ -1,10 +0,0 @@
<title> Reset request processed and confirmation email sent </title>
<h2> Reset request processed and confirmation email sent </h2>
<br><br>
You will shortly get the confirmation email to confirm that you wish to reset the password for {{user}}.<br>
Please follow the link in the email to reset your password.
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to indexpage">
</form>

View file

@ -1,7 +0,0 @@
<title> User {{ user }} created. </title>
<h2> User {{ user }} was successfully created. </h2>
<br><br>
<form action={% url 'index' %} method="get">
<input type="submit" value="Back to Indexpage">
</form>

View file

@ -1,24 +0,0 @@
<title> Options for {{user}} </title>
<h2> Welcome, {{user}} </h2>
<br><br>
You have the following options:
<br>
<form action={% url 'change_data' %} method="get">
<input type="submit" value="Change your userdata">
</form>
<br>
<form action={% url 'change_password' %} method="get">
<input type="submit" value="Change your password">
</form>
<br>
<form action={% url 'reset_password' %} method="get">
<input type="submit" value="Reset your password">
</form>
<br>
<form action={% url 'account_delete' %} method="get">
<input type="submit" value="Delete your account">
</form>
<form action={% url 'logout' %} method="get">
<input type="submit" value="Logout">
</form>

View file

@ -1,437 +0,0 @@
# Imports from django
from django.shortcuts import render
from django.views.generic import View
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseRedirect
from django.core.validators import validate_email, ValidationError
from django.urls import reverse_lazy
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.mail import EmailMessage
from .models import ResetToken
# Imports for the extra stuff not in django
# django_nameko is an extra module, so gets put in here
from base64 import b64encode, b64decode
from datetime import datetime
from django_nameko import get_pool
from random import choice, randint
import string
from configparser import ConfigParser
config = ConfigParser()
config.read('userservice.conf')
# Check to see if the username is already taken
# Helper function, not to be set up as a view
# Check the LDAP if the user exists
def check_user_exists(username):
with get_pool().next() as rpc:
return rpc.userlookup.lookup(username)
# To trick the tokengenerator to work with us, because we don't really
# have the expected user Class since we are reading the user from a form
# We store the tokens and don't have to use the check function,
# some one time data works fine.
class LastLogin():
def replace(self, microsecond=0, tzinfo=None):
return randint(1,100000)
class PseudoUser():
# easiest way to handle the check for lastlogin
last_login = LastLogin()
# random alphanumeric strings for primary key and password, just used for token generation
pk = ''.join(choice(string.ascii_letters + string.digits) for _ in range(20))
password = ''.join(choice(string.ascii_letters + string.digits) for _ in range(30))
# The index page
# If there's a session open, it will give the user the options he/she/it can do, if not,
# it will show a landing page explaining what this is and prompt them to login
class Index(View):
# Basic binary choice, if it is an authenticated user, go straight to the options page,
# if not, then show the landing page
def get(self, request):
if request.user.is_authenticated:
return render(request, 'useroptions.html', { 'user': request.user } )
return render(request, 'landing.html')
# Basically does the same as the GET request, just with trying to login the user beforehand
# Shows an errorpage if authentication fails, since just looping to the landing page
# would be frustrating
def post(self, request):
username = request.POST.get('username')
password = request.POST.get('password')
pwd = r'%s' % password
user = authenticate(request, username=username, password=pwd)
if user is not None:
login(request, user)
return render(request, 'useroptions.html', { 'user': user } )
return render(request, 'loginfailed.html')
# Registering a user
class Register(View):
# Someone wants to register, throw up the page for that
def get(self, request):
return render(request, 'registeruser.html')
# Someone filled out the register page, do some basic checks and throw it at nameko
def post(self, request):
# message for the error template
service = 'register an user'
# urlname for 'go back' on the errorpage
urlname = 'register'
username = request.POST.get('username')
if username == "" or not username:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Please supply a username.' } )
# Check to see if username is already taken
# isalnum() may be a bit harsh, but is the most logical choice to make sure it's a username we
# can use
if not username.isalnum():
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Username has to be alphanumeric.' } )
elif check_user_exists(username):
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'User already exists.' } )
password1 = request.POST.get('password1')
password2 = request.POST.get('password2')
# check if the supplied passwords match
if password1 != password2:
return render(request, 'error.html', { 'urlname': urlname, 'service': service,
'error': 'Your passwords did not match. Please supply the same password twice.' } )
# check for at least a bit of length on the password
if len(password1) < 8:
return render(request, 'error.html', { 'urlname': urlname, 'service': service,
'error': 'Your password is too short, please use a longer one. At least 8 characters.' } )
email = request.POST.get('email')
# Is the emailaddress valid?
try:
validate_email(email)
except ValidationError:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'The supplied email address is invalid.' } )
firstname = request.POST.get('firstname')
lastname = request.POST.get('lastname')
if firstname == "" or not firstname or lastname == "" or not lastname:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Please enter your firstname and lastname.' } )
# throw it to nameko to create the user
with get_pool().next() as rpc:
# so nothing strange happens if there are escapable chars
pwd = r'%s' % password1
result = rpc.createuser.create_user(username, pwd, firstname, lastname, email)
if result == True:
return render(request, 'usercreated.html', { 'user': username } )
else:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': result } )
# Change user data for logged in users
class ChangeData(View):
# provide the form for the change request
def get(self, request):
urlname = 'change_data'
service = 'get default data for logged in user'
if not request.user.is_authenticated:
return render(request, 'mustbeloggedin.html')
user = request.user
login(request, user)
# get basic data (firstname, lastname, email)
with get_pool().next() as rpc:
(state, firstname, lastname, email) = rpc.getuserdata.get_data(str(request.user))
# If it throws an error, the errormessage gets put into firstname.. not great naming, but works best this way
if state == "error":
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': firstname } )
# The template puts the old data as standard in the fields
else:
return render(request, 'changeuserdata.html', { 'user': str(request.user), 'firstname': firstname, 'lastname': lastname, 'email': email } )
# get the change request
def post(self, request):
# variables for the error page
service = 'change user data'
urlname = 'change_data'
# Only logged in users may change data
if not request.user.is_authenticated:
return render(request, 'mustbeloggedin.html')
user = str(request.user)
firstname = request.POST.get('firstname')
lastname = request.POST.get('lastname')
email = request.POST.get('email')
# Some sanity checks for the supplied data
if firstname == "":
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Please enter a firstname.' } )
elif lastname == "":
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Please enter a lastname.' } )
elif email == "":
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Please enter an email.' } )
try:
validate_email(email)
except ValidationError:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'The supplied email address is invalid.' } )
# Trying to change the data
with get_pool().next() as rpc:
result = rpc.changeuserdata.change_data(user, firstname, lastname, email)
# Data change worked
if result == True:
return render(request, 'changeddata.html', { 'user': user, 'firstname': firstname, 'lastname': lastname, 'email': email } )
# Data change did not work, display error
else:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': result } )
# Resets the password for a user
# Sends email to the user with a link to reset the password
class ResetPassword(View):
# Presents the form with some information
def get(self, request):
return render(request, 'resetpassword.html')
# gets the data from confirming the reset request and checks if it was not a misclick
# (by having the user type in his username)
def post(self, request):
urlname = 'reset_password'
service = 'send a password reset request'
user = request.POST.get('user')
# First, check if the user exists
if not check_user_exists(user):
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'The user does not exist.' } )
# user exists, so try to get email
with get_pool().next() as rpc:
(state, tmp1, tmp2, email) = rpc.getuserdata.get_data(user)
# Either error with the datalookup or no email provided
if state == "error" or email == 'No email given' or not email:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Unable to retrieve email address for user.' } )
# Try to send the email out
emailsend = self.email(user, email)
# Email got sent out
if emailsend == True:
return render(request, 'send_resetrequest.html', { 'user': user } )
# Error while trying to send email
else:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': emailsend } )
# Sends an email to the user with the 24h active link for a password reset
def email(self, user, email):
# getting epoch for the time now in UTC to spare us headache with timezones
creationtime = int(datetime.utcnow().timestamp())
# Construct the data for the email
email_from = 'Userservice at ungleich <%s>' % config['EMAIL']['EMAILFROM']
to = ['%s <%s>' % (user, email)]
subject = 'Password reset request for %s' % user
link = self.build_reset_link(user, creationtime)
body = 'This is an automated email which was triggered by a reset request for the user %s. Please do not reply to this email.\n' % user
body += 'If you received this email in error, please disregard it. If you get multiple emails like this, please contact us to look into potential abuse.\n'
body += 'To reset your password, please follow the link below:\n'
body += '%s\n\n' % link
body += 'The link will remain active for 24 hours.\n'
# Build the email
mail = EmailMessage(
subject=subject,
body=body,
from_email=email_from,
to=to
)
try:
mail.send()
result = True
except:
result = "An error occurred while trying to send the mail."
return result
# Builds the reset link for the email and puts the token into the database
def build_reset_link(self, user, epochutc):
# set up the data
host = 'account-staging.ungleich.ch'
tokengen = PasswordResetTokenGenerator()
# create some noise for use in the tokengenerator
pseudouser = PseudoUser()
token = tokengen.make_token(pseudouser)
buser = bytes(user, 'utf-8')
userpart = b64encode(buser)
# create entry into the database
newdbentry = ResetToken(user=user, token=token, creation=epochutc)
newdbentry.save()
# set up the link
link = 'https://%s/reset/%s/%s/' % (host, userpart.decode('utf-8'), token)
return link
# Catch the resetrequest URL and check it
class ResetRequest(View):
# Gets the URL with user in b64 and the token, and checks it
# Also cleans the database
def get(self, request, user=None, token=None):
# Cleans up outdated tokens
# If we expect quite a bit of old tokens, maybe somewhere else is better,
# but for now we don't really expect many unused tokens
self.clean_db()
# If user and token are not supplied by django, it was called from somewhere else, so it's
# invalid
if user == None or token == None:
return HttpResponse('Invalid URL.', status=404)
# extract user from b64 format
tmp_user = bytes(user, 'utf-8')
user = b64decode(tmp_user)
user_clean = user.decode('utf-8')
# set checks_out = True if token is found in database
dbentries = ResetToken.objects.all().filter(user=user_clean)
for entry in dbentries:
if entry.token == token:
# found the token, now delete it since it's used
checks_out = True
entry.delete()
# No token was found
if not checks_out:
return HttpResponse('Invalid URL.', status=404)
# Token was found, supply the form
else:
return render(request, 'resetpasswordnew.html', { 'user': user_clean } )
# Gets the post form with the new password and sets it
def post(self, request):
service = 'reset the password'
# get the supplied passwords
password1 = request.POST.get("password1")
password2 = request.POST.get("password2")
# get the hidden value of user
user = request.POST.get("user")
# some checks over the supplied data
if user == "" or not user:
return render(request, 'error.html', { 'service': service, 'error': 'Something went wrong. Did you use the supplied form?' } )
if password1 == "" or not password1 or password2 == "" or not password2:
return render(request, 'error.html', { 'service': service, 'error': 'Please supply a password and confirm it.' } )
if password1 != password2:
return render(request, 'error.html', { 'service': service, 'error': 'The supplied passwords do not match.' } )
if len(password1) < 8:
return render(request, 'error.html', { 'service': service, 'error': 'The password is too short, please use a longer one. At least 8 characters.' } )
# everything checks out, now change the password
with get_pool().next() as rpc:
pwd = r'%s' % password1
result = rpc.changepassword.change_password(user, pwd)
# password change successfull
if result == True:
return render(request, 'changedpassword.html', { 'user': user } )
# Something went wrong while changing the password
else:
return render(request, 'error.html', { 'service': service, 'error': result } )
# Cleans up outdated tokens
def clean_db(self):
# cutoff time is set to 24h hours
# using utcnow() to have no headache with timezones
cutoff = int(datetime.utcnow().timestamp()) - (24*60*60)
# Get all tokens older than 24 hours
oldtokens = ResetToken.objects.all().filter(creation__lt=cutoff)
for token in oldtokens:
# delete all tokens older than 24 hours
token.delete()
return True
# The logged in user can change the password here
class ChangePassword(View):
# Presents the page for a logged in user
def get(self, request):
if not request.user.is_authenticated:
return render(request, 'mustbeloggedin.html')
return render(request, 'changepassword.html', { 'user': request.user } )
# Does some checks on the supplied data and changes the password
def post(self, request):
# Variables for the error page
urlname = 'change_password'
service = 'change the password'
if not request.user.is_authenticated:
return render(request, 'mustbeloggedin.html')
login(request, request.user)
user = str(request.user)
oldpassword = request.POST.get('oldpassword')
check = authenticate(request, username=user, password=oldpassword)
# Is the right password for the user supplied?
if check is None:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Wrong password for the user.' } )
password1 = request.POST.get('password1')
password2 = request.POST.get('password2')
# Are both passwords from the form the same?
if password1 != password2:
return render(request, 'error.html', { 'urlname': urlname, 'service': service,
'error': 'Please check if you typed the same password both times for the new password' } )
# Check for password length
if len(password1) < 8:
return render(request, 'error.html', { 'urlname': urlname, 'service': service,
'error': 'The password is too short, please use a longer one. At least 8 characters.' } )
with get_pool().next() as rpc:
# Trying to change the password
pwd = r'%s' % password1
result = rpc.changepassword.change_password(user, pwd)
# Password was changed
if result == True:
return render(request, 'changedpassword.html', { 'user': user } )
# Password not changed, instead got some kind of error
else:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': result } )
# Deletes an account
class DeleteAccount(View):
# Show the basic form for deleting an account
def get(self, request):
return render(request, 'deleteaccount.html')
# Reads the filled out form
def post(self, request):
# Variables for error page
urlname = 'account_delete'
service = 'delete an account'
# Does the user exist?
username = request.POST.get('username')
if not check_user_exists(username):
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Unknown user.' } )
# Do user and password match?
password = request.POST.get('password')
pwd = r'%s' % password
check = authenticate(request, username=username, password=pwd)
if check is None:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': 'Wrong password for user.' } )
# Try to delete the user
with get_pool().next() as rpc:
result = rpc.deleteuser.delete_user(username)
# User deleted
if result == True:
logout(request)
return render(request, 'deleteduser.html', { 'user': username } )
# User not deleted, got some kind of error
else:
return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': result } )
# Log out the session
class LogOut(View):
def get(self, request):
logout(request)
return HttpResponse("You have been logged out.", status=200)

10
dal/env.sample Normal file
View file

@ -0,0 +1,10 @@
# Create .env to be loaded automatically
LDAPSERVER="ldap://ldap1.ungleich.ch ldap://ldap2.ungleich.ch"
LDAPSEARCHUSER="user here"
LDAPSEARCHUSERPASSWORD="password here"
# Space separated list of search bases for users
LDAPSEARCH="ou=users,dc=ungleich,dc=ch ou=customers,dc=ungleich,dc=ch"
LDAPCREATE="ou=customers,dc=ungleich,dc=ch"

31
dal/forms.py Normal file
View file

@ -0,0 +1,31 @@
from django import forms
from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _
class LoginForm(forms.Form):
username = forms.CharField(widget=forms.TextInput())
password = forms.CharField(widget=forms.PasswordInput())
class Meta:
fields = ['username', 'password']
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if self.errors:
return self.cleaned_data
is_auth = authenticate(username=username, password=password)
if not is_auth:
raise forms.ValidationError(
_("Your username and/or password were incorrect.")
)
# elif is_auth.validated == 0:
# raise forms.ValidationError(
# _("Your account is not activated yet.")
# )
return self.cleaned_data
def clean_username(self):
username = self.cleaned_data.get('username')
return username

View file

@ -0,0 +1,37 @@
# Generated by Django 2.1.7 on 2019-02-24 17:35
import dal.models
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),
]
operations = [
migrations.CreateModel(
name='ResetToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.CharField(max_length=100)),
('token', models.CharField(max_length=255)),
('creation', models.BigIntegerField()),
],
),
migrations.CreateModel(
name='UserAccountValidationDetail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('validated', models.IntegerField(choices=[(0, 'Not validated'), (1, 'Validated')], default=0)),
('validation_slug', models.CharField(db_index=True, default=dal.models.get_validation_slug, max_length=50, unique=True)),
('date_validation_started', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

32
dal/models.py Normal file
View file

@ -0,0 +1,32 @@
from django.db import models
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
# Basic DB to correlate tokens, users and creation time
class ResetToken(models.Model):
# users wouldn't use usernames >100 chars
user = models.CharField(max_length=100)
# Not so sure about tokens, better make it big
# should be <100, but big usernames make bigger tokens
# if I read that correctly
token = models.CharField(max_length=255)
# creation time in epoch (UTC)
# BigInt just so we are save for the next few decades ;)
creation = models.BigIntegerField()
def get_validation_slug():
return make_password(None)
class UserAccountValidationDetail(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0)
validation_slug = models.CharField(
db_index=True, unique=True, max_length=50,
default=get_validation_slug
)
date_validation_started = models.DateTimeField(auto_now_add=True)

218
dal/settings.py Normal file
View file

@ -0,0 +1,218 @@
"""
Django settings for dal project.
Generated by 'django-admin startproject' using Django 1.10.7.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
from decouple import config, Csv
import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
# LDAP setup
LDAP_SERVER = config('LDAP_SERVER')
AUTH_LDAP_SERVER_URI = config('LDAPSERVER')
LDAP_ADMIN_DN = config('LDAP_ADMIN_DN')
LDAP_ADMIN_PASSWORD = config('LDAP_ADMIN_PASSWORD')
AUTH_LDAP_BIND_DN = LDAP_ADMIN_DN
AUTH_LDAP_BIND_PASSWORD = LDAP_ADMIN_PASSWORD
AUTH_LDAP_SERVER = AUTH_LDAP_SERVER_URI
LDAP_CUSTOMER_DN = config('LDAP_CUSTOMER_DN')
LDAP_USERS_DN = config('LDAP_USERS_DN')
LDAP_CUSTOMER_GROUP_ID = config('LDAP_CUSTOMER_GROUP_ID', cast=int)
LDAP_MAX_UID_FILE_PATH = config(
'LDAP_MAX_UID_FILE_PATH',
default=os.path.join(os.path.abspath(
os.path.dirname(__file__)), 'ldap_max_uid_file'
)
)
LDAP_DEFAULT_START_UID = config('LDAP_DEFAULT_START_UID', cast=int)
# Search union over OUs
search_base = config('LDAPSEARCH').split()
search_base_ldap = [ LDAPSearch(x, ldap.SCOPE_SUBTREE, "(uid=%(user)s)") for x in search_base ]
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*search_base_ldap)
AUTH_LDAP_START_TLS = config('LDAP_USE_TLS', default=False, cast=bool)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEBUG = config('DEBUG', default=False, cast=bool)
EMAIL_FROM_ADDRESS = config('EMAIL_FROM_ADDRESS')
EMAIL_HOST = config("EMAIL_HOST", default="localhost")
EMAIL_PORT = config("EMAIL_PORT", default=25, cast=int)
EMAIL_USE_TLS = config("EMAIL_USE_TLS", default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrap3',
'dal',
'rest_framework'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
AUTHENTICATION_BACKENDS = (
'django_auth_ldap.backend.LDAPBackend',
)
ROOT_URLCONF = 'dal.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'dal.wsgi.application'
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
############################# To be fixed
STATIC_ROOT= os.path.join(BASE_DIR, 'static/')
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
SECRET_KEY = config('SECRET_KEY')
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
ENTIRE_SEARCH_BASE = config("ENTIRE_SEARCH_BASE")
LOGGING = {
'disable_existing_loggers': False,
'version': 1,
'formatters': {
'standard': {
'format': '%(asctime)s %(levelname)s %(name)s %(funcName)s %(lineno)d: %(message)s'
}
},
'handlers': {
'default': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/debug.log',
'maxBytes': 1024*1024*5,
'backupCount': 10,
'formatter': 'standard',
},
'console': {
'class': 'logging.StreamHandler',
},
},
}
if config('ENABLE_DEBUG_LOG', default=False, cast=bool):
loggers_dict = {}
modules_to_log_list = config(
'MODULES_TO_LOG', default='django', cast=Csv()
)
for custom_module in modules_to_log_list:
logger_item = {
custom_module: {
'handlers': ['default'],
'level': 'INFO',
'propagate': True
}
}
loggers_dict.update(logger_item)
LOGGING['loggers'] = loggers_dict
if 'ldap3' in modules_to_log_list:
from ldap3.utils.log import (
set_library_log_detail_level, OFF, BASIC, NETWORK, EXTENDED
)
set_library_log_detail_level(BASIC)
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

Before

Width:  |  Height:  |  Size: 394 KiB

After

Width:  |  Height:  |  Size: 394 KiB

View file

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

Before

Width:  |  Height:  |  Size: 685 KiB

After

Width:  |  Height:  |  Size: 685 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Some files were not shown because too many files have changed in this diff Show more