diff --git a/README.md b/README.md index 79100d4..5329c4d 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,6 @@ from .rpc import ``` till it's fixed\. Config options are in nameko\.conf and of course the dal/dal/settings\.py +Don't forget to do python manage\.py makemigrations dal and then migrate The standard settings there work fine with a LDAP server set up with the information on our wiki\. Except the manager password, set that to what you choose\. diff --git a/dal/dal/models.py b/dal/dal/models.py new file mode 100644 index 0000000..e197640 --- /dev/null +++ b/dal/dal/models.py @@ -0,0 +1,14 @@ +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) + # Just so we are save for the next few decades ;) + creation = models.BigIntegerField() diff --git a/dal/dal/views.py b/dal/dal/views.py index 175db04..6af0c32 100644 --- a/dal/dal/views.py +++ b/dal/dal/views.py @@ -9,6 +9,7 @@ from django_nameko import get_pool from django.contrib.auth.tokens import PasswordResetTokenGenerator from base64 import b64encode, b64decode from datetime import datetime +from .models import ResetToken # Check to see if the username is already taken # Helper function, not to be set up as a view @@ -157,9 +158,7 @@ class ChangeData(View): # Resets the password for a user -# Will need to send a confirmation email to the user and we will need a backend -# to confirm the request came from someone who has access to the email -# Out of scope except for creating the workflow +# Sends email to the user with a link to reset the password class ResetPassword(View): @@ -168,7 +167,7 @@ class ResetPassword(View): 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 + # (by having the user type in his username) def post(self, request): urlname = 'reset_password' service = 'send a password reset request' @@ -191,52 +190,69 @@ class ResetPassword(View): 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): - #TODO figure out how to send email - email_from = 'Userservice at ungleich ' + # getting epoch for the time now in UTC to spare us headache with timezones + creationtime = int(datetime.utcnow().timestamp()) + #TODO figure out how to send email + email_from = 'Userservice at ungleich ' to = '%s <%s>' % (user, email) subject = 'Password reset request for %s' % user - no-reply = True - link = self.build_reset_link(user) + noreply = True + link = self.build_reset_link(user, creationtime) body = 'This is an automated email which was triggered by a reset request for the user %s.\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' - # For debug + body += 'The link will remain active for 24 hours.\n' + # For debug return link - def build_reset_link(self, user): + # 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 = 'localhost:8000' - x = PasswordResetTokenGenerator() - token = x.make_token(user) - buser = bytes(user, 'utf-8') - userpart = b64encode(buser) - d = datetime.now() - # TODO Make Model und put it into the database + tokengen = PasswordResetTokenGenerator() + token = tokengen.make_token(user) + 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 and check it +# 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') - d = datetime.now() - #TODO write the model and check if token is still active and belongs to the user - # set checks_out = True if yes + # 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 } ) @@ -244,26 +260,40 @@ class ResetRequest(View): # 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.' } ) + # 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): - # TODO write the model and use this to clean tokens > 24h old - - + # 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 @@ -346,7 +376,7 @@ class DeleteAccount(View): else: return render(request, 'error.html', { 'urlname': urlname, 'service': service, 'error': result } ) - +# Log out the session class LogOut(View): def get(self, request):