Merge branch 'william' into 'master'

request the username in login instead of the email

See merge request ungleich-public/ungleich-user!6
This commit is contained in:
wcolmenares 2019-08-27 17:25:50 +02:00
commit b7011b0574
12 changed files with 311 additions and 66 deletions

View File

@ -4,18 +4,18 @@ from django.utils.translation import ugettext_lazy as _
class LoginForm(forms.Form):
email = forms.CharField(widget=forms.TextInput())
username = forms.CharField(widget=forms.TextInput())
password = forms.CharField(widget=forms.PasswordInput())
class Meta:
fields = ['email', 'password']
fields = ['username', 'password']
def clean(self):
email = self.cleaned_data.get('email')
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if self.errors:
return self.cleaned_data
is_auth = authenticate(username=email, password=password)
is_auth = authenticate(username=username, password=password)
if not is_auth:
raise forms.ValidationError(
_("Your username and/or password were incorrect.")
@ -26,6 +26,6 @@ class LoginForm(forms.Form):
# )
return self.cleaned_data
def clean_email(self):
email = self.cleaned_data.get('email')
return email
def clean_username(self):
username = self.cleaned_data.get('username')
return username

View File

@ -65,6 +65,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'bootstrap3',
'dal',
'rest_framework'
]
MIDDLEWARE = [
@ -208,3 +209,10 @@ if config('ENABLE_DEBUG_LOG', default=False, cast=bool):
set_library_log_detail_level, OFF, BASIC, NETWORK, EXTENDED
)
set_library_log_detail_level(BASIC)
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}

View File

@ -35,6 +35,8 @@
<button type="submit" class="btn choice-btn btn-block">
{% trans "Change Password" %}
</button>
<hr>
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
</div>
</form>
</div>

View File

@ -34,7 +34,13 @@
{% trans "Change User Data" %}
</button>
</div>
<hr>
<div class="text-center">
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
{% extends "base_short.html" %}
{% load i18n staticfiles bootstrap3 %}
{% block title %}
<title> Verify your email. </title>
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="auth-bg"></div>
<div class="auth-center">
<div class="auth-content">
<div class="auth-box">
<h1 class="section-heading allcaps">{% trans " Check your email " %}</h1>
<p class="text-center">{% trans "In order to complete the sign up process, please check your email and follow the activation instructions." %}</p>
<form action="{% url 'index' %}" method="get" class="form" novalidated>
<div class="text-center">
<button type="submit" class="btn choice-btn btn-block">
{% trans "Back to indexpage" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -27,6 +27,7 @@
</div>
<hr>
<div class="text-center">
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
<button type="submit" class="btn choicered-btn btn-block">
{% trans "Delete Account" %}
</button>

View File

@ -0,0 +1,37 @@
{% extends "base_short.html" %}
{% load i18n staticfiles bootstrap3 %}
{% block title %}
<title>Options for {{user}}</title>
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="auth-bg"></div>
<div class="auth-center">
<div class="auth-content">
<div class="auth-box">
<h1 class="section-heading allcaps">{% trans "Seeds of," %} {{user}}</h1><br><br>
<table class="table table-hover text-center">
<tbody>
{% for i in seed %}
<tr>
<td>{{ i.realm }}</td>
<td>{{ i.seed }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
<hr>
<form class="form" novalidate>
<div class="text-center">
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -16,6 +16,7 @@
<form class="form">
<a class="btn choice-btn btn-block" href="{% url 'change_data' %}" role="button">{% trans "Change your userdata" %}</a><br>
<a class="btn choice-btn btn-block" href="{% url 'change_password' %}" role="button">Change your password</a><br>
<a class="btn choice-btn btn-block" href="{% url 'user_seeds' %}" role="button">Show seeds</a><br>
<a class="btn choicered-btn btn-block" href="{% url 'logout' %}" role="button">{% trans "Logout" %}</a><br><br>
</form>
<br>

View File

@ -91,7 +91,7 @@ class LdapManager:
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
uid = user.encode("utf-8")
uid = user # user.encode("utf-8")
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),

View File

@ -13,19 +13,25 @@ from .views import (
Index,
LogOut,
ResetRequest,
UserCreateAPI
UserCreateAPI,
ActivateAccount,
Seeds,
SeedRetrieveCreate
)
urlpatterns = [
path('register/', Register.as_view(), name="register"),
path('create/', UserCreateAPI.as_view(), name="create"),
path('changedata/', ChangeData.as_view(), name="change_data"),
path('seeds/', Seeds.as_view(), name="user_seeds"),
path('resetpassword/', ResetPassword.as_view(), name="reset_password"),
path('changepassword/', ChangePassword.as_view(), name="change_password"),
path('deleteaccount/', DeleteAccount.as_view(), name="account_delete"),
path('index/', Index.as_view(), name="index"),
path('logout/', LogOut.as_view(), name="logout"),
path('reset/<str:user>/<str:token>/', ResetRequest.as_view()),
path('activate/<str:user>/<str:pwd>/<str:firstname>/<str:lastname>/<str:email>/<str:token>/', ActivateAccount.as_view()),
path('reset/', ResetRequest.as_view(), name="reset"),
path('otp/', SeedRetrieveCreate.as_view(), name="seed"),
path('', Index.as_view(), name="login_index"),
]

View File

@ -14,9 +14,10 @@ from rest_framework.response import Response
from .models import ResetToken
from .forms import LoginForm
from .ungleich_ldap import LdapManager
from decouple import config, Csv
from pyotp import TOTP
import logging
logger = logging.getLogger(__name__)
# Imports for the extra stuff not in django
@ -26,20 +27,70 @@ from datetime import datetime
from random import choice, randint
import string
import requests
import json
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
admin_seed = config('ADMIN_SEED')
admin_name = config('ADMIN_NAME')
admin_realm = config('ADMIN_REALM')
user_realm = config('USER_REALM')
otp_url = config('OTPSERVER')
def activate_account_link(base_url, user, pwd, firstname, lastname, email, epochutc):
tokengen = PasswordResetTokenGenerator()
pseudouser = PseudoUser()
token = tokengen.make_token(pseudouser)
buser = bytes(user, 'utf-8')
bpwd = bytes(pwd, 'utf-8')
bfirstname = bytes(firstname, 'utf-8')
blasttname = bytes(lastname, 'utf-8')
bemail = bytes(email, 'utf-8')
userpart = b64encode(buser)
pwdpart = b64encode(bpwd)
fnpart = b64encode(bfirstname)
lnpart = b64encode(blasttname)
mailpart = b64encode(bemail)
# create entry into the database
newdbentry = ResetToken(user=user, token=token, creation=epochutc)
newdbentry.save()
# set up the link
link = "{base_url}/activate/{user}/{pwd}/{fn}/{ln}/{mail}/{token}/".format(
base_url=base_url, user=userpart.decode('utf-8'),
pwd=pwdpart.decode('utf-8'),
fn=fnpart.decode('utf-8'),
ln=lnpart.decode('utf-8'),
mail=mailpart.decode('utf-8'),
token=token
)
return link
def clean_db():
"""Revoves outdated tokens"""
# 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
class Index(FormView):
template_name = "landing.html"
form_class = LoginForm
success_url = 'useroptions.html'
def form_valid(self, form):
email = form.cleaned_data.get('email')
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=email, password=password)
user = authenticate(username=username, password=password)
if user is not None:
login(self.request, user)
return render(self.request, 'useroptions.html', { 'user': user } )
@ -92,34 +143,30 @@ class Register(View):
pwd = r'%s' % password1
try:
ldap_manager = LdapManager()
ldap_manager.create_user(
username, pwd, firstname, lastname, email
creationtime = int(datetime.utcnow().timestamp())
base_url = "{0}://{1}".format(self.request.scheme,
self.request.get_host())
link = activate_account_link(base_url, username, pwd, firstname, lastname, email, creationtime)
email_from = settings.EMAIL_FROM_ADDRESS
to = ['%s <%s>' % (username, email)]
subject = 'Activate your ungleich account'.format(firstname)
body = 'You can activate your ungleich account account by clicking <a href="{link}">here</a>.' \
' You can also copy and paste the following link into the address bar of your browser and follow' \
' the link in order to activate your account.\n\n{link}'.format(link=link)
# Build the email
mail = EmailMessage(
subject=subject,
body=body,
from_email=email_from,
to=to
)
mail.send()
except Exception as e:
return render(request, 'error.html', { 'urlname': urlname,
'service': service,
'error': e } )
# Finally, we send the send user credentials via email
creationtime = int(datetime.utcnow().timestamp())
# Construct the data for the email
email_from = settings.EMAIL_FROM_ADDRESS
to = ['%s <%s>' % (username, email)]
subject = '{}, Welcome to datacenterlight'.format(firstname)
body = 'The username {} was successfully created.\n'.format(username)
# Build the email
mail = EmailMessage(
subject=subject,
body=body,
from_email=email_from,
to=to
)
try:
mail.send()
except Exception as e:
print(e)
pass
return render(request, 'usercreated.html', { 'user': username } )
return render(request, 'confirm_email.html')
class ChangeData(LoginRequiredMixin, View):
login_url = reverse_lazy('login_index')
@ -297,7 +344,7 @@ class ResetRequest(View):
# 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()
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:
@ -336,10 +383,10 @@ class ResetRequest(View):
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.' } )
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
return render(request, 'error.html', {'service': service,
'error': 'The password is too short, please use a longer one. At least 8 characters.'})
ldap_manager = LdapManager()
result = ldap_manager.change_password(
@ -353,17 +400,7 @@ class ResetRequest(View):
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
@ -488,6 +525,58 @@ class PseudoUser():
pk = ''.join(choice(string.ascii_letters + string.digits) for _ in range(20))
password = ''.join(choice(string.ascii_letters + string.digits) for _ in range(30))
class ActivateAccount(View):
def get(self, request, user=None, pwd=None, firstname=None, lastname=None, email=None, token=None):
clean_db()
if token is None:
return HttpResponse('Invalid URL', status=404)
elem_list = [user, pwd, firstname, lastname, email]
clean_list = []
for value in elem_list:
try:
value_temp = bytes(value, 'utf-8')
value_decode = b64decode(value_temp)
value_clean = value_decode.decode('utf-8')
clean_list.append(value_clean)
except Exception as e:
return HttpResponse('Invalid URL', status=404)
checks_out = False
dbentries = ResetToken.objects.all().filter(user=clean_list[0])
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, create user
try:
ldap_manager = LdapManager()
ldap_manager.create_user(
clean_list[0], clean_list[1], clean_list[2], clean_list[3], clean_list[4]
)
req = requests.post(otp_url, data=json.dumps(
{
'auth_token': TOTP(admin_seed).now(),
'auth_name': admin_name,
'auth_realm': admin_realm,
'name': clean_list[0],
'realm': user_realm
}), headers={'Content-Type': 'application/json'})
if req.status_code != 201:
logger.error("User {} failed to create its otp seed".format(clean_list[0]))
#Send welcome email
except Exception as e:
return render(request, 'error.html', {'urlname': 'register',
'service': 'register an user',
'error': e})
return render(request, 'usercreated.html', { 'user': clean_list[0] } )
class UserCreateAPI(APIView):
def post(self, request):
@ -508,25 +597,24 @@ class UserCreateAPI(APIView):
pwd = r'%s' % User.objects.make_random_password()
try:
ldap_manager = LdapManager()
ldap_manager.create_user(
username, pwd, firstname, lastname, email
)
except Exception as e:
return Response('While trying to create the user, an error was encountered: %s' % e, 400)
# send user credentials via email
base_url = "{0}://{1}".format(self.request.scheme,
self.request.get_host())
creationtime = int(datetime.utcnow().timestamp())
link = activate_account_link(base_url, username, pwd, firstname, lastname, email, creationtime)
# Construct the data for the email
email_from = settings.EMAIL_FROM_ADDRESS
to = ['%s <%s>' % (username, email)]
subject = 'Your datacenterlight credentials'
body = 'Your user was successfully created.\n'
subject = 'Ungleich account creation.'
body = 'A request has been sent to our servers to register you as a ungleich user.\n'
body += 'In order to complete the registration process you must ' \
'click <a href="{link}">here</a> or copy & paste the following link into the address bar of ' \
'your browser.\n{link}\n'.format(link=link)
body += 'Your credentials are:\n'
body += 'Username: %s\n\n' % username
body += 'Password: %s\n\n' % pwd
body += 'We strongly recommend you to after log in change your password.\n'
body += 'We strongly recommend after the activation to log in and change your password.\n'
body += 'This link will remain active for 24 hours.\n'
# Build the email
mail = EmailMessage(
subject=subject,
@ -536,6 +624,71 @@ class UserCreateAPI(APIView):
)
try:
mail.send()
except:
return Response('User was created, but failed to send the email', 201)
return Response('User successfully created', 200)
except Exception as e:
return Response('Failed to send the email, please try again', 400)
return Response('An email with activation link has been sent in order to complete your registration. Please check your inbox.', 200)
class SeedRetrieveCreate(APIView):
def post(self, request):
try:
username = request.data['username']
password = request.data[r'password']
realm = request.data['realm']
print(password)
except KeyError:
return Response('You need to specify username, password, and realm values', 400)
# authenticate the user against ldap
user = authenticate(username=username, password=password)
if user is not None:
req = requests.get(otp_url, data=json.dumps(
{
'auth_token': TOTP(admin_seed).now(),
'auth_name': admin_name,
'auth_realm': admin_realm}), headers={'Content-Type': 'application/json'})
response_data = json.loads(req.text)
for elem in response_data:
if elem['name'] == username and elem['realm'] == realm:
return Response(elem, 200)
# If doesn't find a match then check if the realm is allowed and create the user
allowed_realms = config('ALLOWED_REALMS', cast=Csv())
if realm not in allowed_realms:
return Response('Not allowed to perform this action.', 403)
else:
req = requests.post(otp_url, data=json.dumps(
{
'auth_token': TOTP(admin_seed).now(),
'auth_name': admin_name,
'auth_realm': admin_realm,
'name': username,
'realm': realm
}), headers={'Content-Type': 'application/json'})
if req.status_code == 201:
msg = json.loads(req.text)
return Response(msg, 201)
else:
return Response(json.loads(req.text), req.status_code)
else:
return Response('Invalid Credentials', 400)
class Seeds(LoginRequiredMixin, View):
login_url = reverse_lazy('login_index')
def get(self, request):
seedlist = []
response = requests.get(
otp_url,
headers={'Content-Type': 'application/json'},
data=json.dumps(
{'auth_name': admin_name, 'auth_realm': admin_realm, 'auth_token': TOTP(admin_seed).now()}))
response_data = json.loads(response.text)
for i in range(len(response_data)):
if response_data[i]['name'] == request.user.username:
value = {'realm': response_data[i]['realm'], 'seed': response_data[i]['seed']}
seedlist.append(value)
return render(request, 'seed_list.html', {'seed': seedlist})

View File

@ -5,4 +5,6 @@ django-bootstrap3
django-filter==2.1.0
python-decouple
ldap3
djangorestframework
djangorestframework
pyotp
requests