This is an automated email from the ASF dual-hosted git repository. smarru pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/airavata-custos-portal.git
commit 12dbbe27c72721c026db88b25750dc1a24e3291e Author: Shivam Rastogi <shivam_r...@yahoo.com> AuthorDate: Fri Mar 27 12:38:02 2020 -0400 Added email verification --- custos_portal/custos_portal/apps/admin/models.py | 3 - .../apps/auth/migrations/0001_initial.py | 45 +++++++++++ .../auth/migrations/0002_default_email_template.py | 55 +++++++++++++ custos_portal/custos_portal/apps/auth/models.py | 41 ++++++++++ custos_portal/custos_portal/apps/auth/urls.py | 2 + custos_portal/custos_portal/apps/auth/utils.py | 25 ++++++ custos_portal/custos_portal/apps/auth/views.py | 94 ++++++++++++++++++++-- custos_portal/custos_portal/settings_local.py | 21 ++++- 8 files changed, 275 insertions(+), 11 deletions(-) diff --git a/custos_portal/custos_portal/apps/admin/models.py b/custos_portal/custos_portal/apps/admin/models.py index 71a8362..e69de29 100644 --- a/custos_portal/custos_portal/apps/admin/models.py +++ b/custos_portal/custos_portal/apps/admin/models.py @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py b/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py new file mode 100644 index 0000000..bfe59d5 --- /dev/null +++ b/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 3.0.4 on 2020-03-27 16:05 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='EmailTemplate', + fields=[ + ('template_type', models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template')], primary_key=True, serialize=False)), + ('subject', models.CharField(max_length=255)), + ('body', models.TextField()), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='EmailVerification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=64)), + ('verification_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('verified', models.BooleanField(default=False)), + ('next', models.CharField(blank=True, max_length=255)), + ], + ), + migrations.CreateModel( + name='PasswordResetRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=64)), + ('reset_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py b/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py new file mode 100644 index 0000000..d0db295 --- /dev/null +++ b/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py @@ -0,0 +1,55 @@ +from __future__ import unicode_literals + +from django.db import migrations + +from custos_portal.apps.auth.models import ( + NEW_USER_EMAIL_TEMPLATE, + VERIFY_EMAIL_TEMPLATE +) + + +def default_templates(apps, schema_editor): + + EmailTemplate = apps.get_model("custos_portal_auth", "EmailTemplate") + verify_email_template = EmailTemplate( + template_type=VERIFY_EMAIL_TEMPLATE, + subject="{{first_name}} {{last_name}} ({{username}}), " + "Please Verify Your Email Account in {{portal_title}}", + body=""" + <p> + Dear {{first_name}} {{last_name}}, + </p> + + <p> + Someone has created an account with this email address. If this was + you, click the link below to verify your email address: + </p> + + <p><a href="{{url}}">{{url}}</a></p> + + <p>If you didn't create this account, just ignore this message.</p> + """.strip()) + verify_email_template.save() + new_user_email_template = EmailTemplate( + template_type=NEW_USER_EMAIL_TEMPLATE, + subject="New User Account Was Created Successfully", + body=""" + <p>Gateway Portal: {{http_host}}</p> + <p>Tenant: {{gateway_id}}</p> + <p>Username: {{username}}</p> + <p>Name: {{first_name}} {{last_name}}</p> + <p>Email: {{email}}</p> + """.strip() + ) + new_user_email_template.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('custos_portal_auth', '0001_initial'), + ] + + operations = [ + migrations.RunPython(default_templates) + ] diff --git a/custos_portal/custos_portal/apps/auth/models.py b/custos_portal/custos_portal/apps/auth/models.py index 71a8362..c50df65 100644 --- a/custos_portal/custos_portal/apps/auth/models.py +++ b/custos_portal/custos_portal/apps/auth/models.py @@ -1,3 +1,44 @@ +import uuid + from django.db import models +VERIFY_EMAIL_TEMPLATE = 1 +NEW_USER_EMAIL_TEMPLATE = 2 +PASSWORD_RESET_EMAIL_TEMPLATE = 3 +USER_ADDED_TO_GROUP_TEMPLATE = 4 + + # Create your models here. +class EmailVerification(models.Model): + username = models.CharField(max_length=64) + verification_code = models.CharField(max_length=36, unique=True, default=uuid.uuid4) + created_date = models.DateTimeField(auto_now_add=True) + verified = models.BooleanField(default=False) + next = models.CharField(max_length=255, blank=True) + + +class EmailTemplate(models.Model): + TEMPLATE_TYPE_CHOICES = ( + (VERIFY_EMAIL_TEMPLATE, 'Verify Email Template'), + (NEW_USER_EMAIL_TEMPLATE, 'New User Email Template'), + (PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'), + (USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'), + ) + template_type = models.IntegerField(primary_key=True, choices=TEMPLATE_TYPE_CHOICES) + subject = models.CharField(max_length=255) + body = models.TextField() + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + + def __str__(self): + for choice in self.TEMPLATE_TYPE_CHOICES: + if self.template_type == choice[0]: + return choice[1] + return "Unknown" + + +class PasswordResetRequest(models.Model): + username = models.CharField(max_length=64) + reset_code = models.CharField( + max_length=36, unique=True, default=uuid.uuid4) + created_date = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/custos_portal/custos_portal/apps/auth/urls.py b/custos_portal/custos_portal/apps/auth/urls.py index ef1de64..623799e 100644 --- a/custos_portal/custos_portal/apps/auth/urls.py +++ b/custos_portal/custos_portal/apps/auth/urls.py @@ -14,5 +14,7 @@ urlpatterns = [ name='callback-error'), url(r'handle-login', views.handle_login, name="handle_login"), url(r'^logout$', views.start_logout, name='logout'), + url(r'^verify-email/(?P<code>[\w-]+)/$', views.verify_email, + name="verify_email"), ] diff --git a/custos_portal/custos_portal/apps/auth/utils.py b/custos_portal/custos_portal/apps/auth/utils.py new file mode 100644 index 0000000..a1f5598 --- /dev/null +++ b/custos_portal/custos_portal/apps/auth/utils.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.core.mail import EmailMessage + +from . import models +from django.template import Context, Template + + +def send_email_to_user(template_id, context): + email_template = models.EmailTemplate.objects.get(pk=template_id) + subject = Template(email_template.subject).render(context) + body = Template(email_template.body).render(context) + msg = EmailMessage( + subject=subject, + body=body, + from_email="\"{}\" <{}>".format(settings.PORTAL_TITLE, + settings.SERVER_EMAIL), + to=["\"{} {}\" <{}>".format(context['first_name'], + context['last_name'], + context['email'])], + reply_to=[f"\"{a[0]}\" <{a[1]}>" for a in getattr(settings, + 'PORTAL_ADMINS', + settings.ADMINS)] + ) + msg.content_subtype = 'html' + msg.send() diff --git a/custos_portal/custos_portal/apps/auth/views.py b/custos_portal/custos_portal/apps/auth/views.py index dc2e688..c806245 100644 --- a/custos_portal/custos_portal/apps/auth/views.py +++ b/custos_portal/custos_portal/apps/auth/views.py @@ -8,11 +8,15 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import authenticate, login, logout from django.core.exceptions import ValidationError -from django.forms import formset_factory +from django.forms import formset_factory, models from django.shortcuts import render, redirect, resolve_url +from django.template import Context from django.urls import reverse +from django.utils.http import urlencode from requests_oauthlib import OAuth2Session +from . import utils +from . import models from . import forms from ... import identity_management_client from ... import user_management_client @@ -132,7 +136,6 @@ def start_logout(request): def create_account(request): - print("Create account is called") if request.method == 'POST': form = forms.CreateAccountForm(request.POST) if form.is_valid(): @@ -143,10 +146,11 @@ def create_account(request): last_name = form.cleaned_data['last_name'] password = form.cleaned_data['password'] is_temp_password = True - result = user_management_client.register_user(settings.CUSTOS_TOKEN, - username, first_name, last_name, password, email, - is_temp_password) + result = user_management_client.register_user(settings.CUSTOS_TOKEN, username, first_name, last_name, + password, email, is_temp_password) if result.is_registered: + logger.debug("User account successfully created for : {}".format(username)) + _create_and_send_email_verification_link(request, username, email, first_name, last_name, next) messages.success( request, "Account request processed successfully. Before you " @@ -154,8 +158,7 @@ def create_account(request): "We've sent you an email with a link that you should " "click on to complete the account creation process.") else: - form.add_error(None, ValidationError( - "Failed to register the user with IAM service")) + form.add_error(None, ValidationError("Failed to register the user with IAM service")) except TypeError as e: logger.exception( "Failed to create account for user", exc_info=e) @@ -170,3 +173,80 @@ def create_account(request): 'options': settings.AUTHENTICATION_OPTIONS, 'form': form }) + + +def verify_email(request, code): + + try: + email_verification = models.EmailVerification.objects.get(verification_code=code) + email_verification.verified = True + email_verification.save() + # Check if user is enabled, if so redirect to login page + username = email_verification.username + logger.debug("Email address verified for {}".format(username)) + login_url = reverse('custos_portal_auth:login') + if email_verification.next: + login_url += "?" + urlencode({'next': email_verification.next}) + if iam_admin_client.is_user_enabled(username): + logger.debug("User {} is already enabled".format(username)) + messages.success( + request, + "Your account has already been successfully created. " + "Please log in now.") + return redirect(login_url) + else: + logger.debug("Enabling user {}".format(username)) + # enable user and inform admins + iam_admin_client.enable_user(username) + user_profile = iam_admin_client.get_user(username) + email_address = user_profile.emails[0] + first_name = user_profile.firstName + last_name = user_profile.lastName + utils.send_new_user_email(request, + username, + email_address, + first_name, + last_name) + messages.success( + request, + "Your account has been successfully created. " + "Please log in now.") + return redirect(login_url) + except ObjectDoesNotExist as e: + # if doesn't exist, give user a form where they can enter their + # username to resend verification code + logger.exception("EmailVerification object doesn't exist for " + "code {}".format(code)) + messages.error( + request, + "Email verification failed. Please enter your username and we " + "will send you another email verification link.") + return redirect(reverse('django_airavata_auth:resend_email_link')) + except Exception as e: + logger.exception("Email verification processing failed!") + messages.error( + request, + "Email verification failed. Please try clicking the email " + "verification link again later.") + return redirect(reverse('django_airavata_auth:create_account')) + + +def _create_and_send_email_verification_link(request, username, email, first_name, last_name, next_url): + + email_verification = models.EmailVerification(username=username, next=next_url) + email_verification.save() + + verification_uri = request.build_absolute_uri( + reverse('custos_portal_auth:verify_email', kwargs={'code': email_verification.verification_code})) + logger.debug( + "verification_uri={}".format(verification_uri)) + + context = Context({ + "username": username, + "email": email, + "first_name": first_name, + "last_name": last_name, + "portal_title": settings.PORTAL_TITLE, + "url": verification_uri, + }) + utils.send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context) diff --git a/custos_portal/custos_portal/settings_local.py b/custos_portal/custos_portal/settings_local.py index b4e3175..383a3d5 100644 --- a/custos_portal/custos_portal/settings_local.py +++ b/custos_portal/custos_portal/settings_local.py @@ -22,4 +22,23 @@ KEYCLOAK_LOGOUT_URL = 'https://keycloak.custos.scigap.org:31000/auth/realms/1000 KEYCLOAK_VERIFY_SSL = False -SESSION_COOKIE_SECURE = False \ No newline at end of file +SESSION_COOKIE_SECURE = False + +# Default email backend (for local development) +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Django - Email settings +# Uncomment and specify the following for sending emails (default email backend +# just prints to the console) +# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = '...' +# EMAIL_PORT = '...' +# EMAIL_HOST_USER = '...' +# EMAIL_HOST_PASSWORD = '...' +# EMAIL_USE_TLS = True +ADMINS = [('Admin Name', 'ad...@example.com')] +# SERVER_EMAIL = 'por...@example.com' + + +# Portal settings +PORTAL_TITLE = 'Custos Admin Portal'