You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2021/04/29 21:29:39 UTC
[airavata-django-portal] 01/03: AIRAVATA-3455 Verify email change
before updating user profile
This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit 91cb9c49aadf40837569b2db4c07cd097cfc3437
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Apr 29 15:35:23 2021 -0400
AIRAVATA-3455 Verify email change before updating user profile
---
.../auth/migrations/0008_auto_20210422_1838.py | 59 ++++++++++++++++++++++
django_airavata/apps/auth/models.py | 11 ++++
django_airavata/apps/auth/serializers.py | 48 ++++++++++++++++++
.../js/components/UserProfileEditor.vue | 2 +-
django_airavata/apps/auth/urls.py | 4 +-
django_airavata/apps/auth/views.py | 40 +++++++++++++++
6 files changed, 162 insertions(+), 2 deletions(-)
diff --git a/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py b/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
new file mode 100644
index 0000000..9a327b0
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
@@ -0,0 +1,59 @@
+# Generated by Django 2.2.17 on 2021-04-22 18:38
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+from django_airavata.apps.auth.models import VERIFY_EMAIL_CHANGE_TEMPLATE
+
+
+def default_templates(apps, schema_editor):
+ EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+ verify_email_template = EmailTemplate(
+ template_type=VERIFY_EMAIL_CHANGE_TEMPLATE,
+ subject="{{first_name}} {{last_name}} ({{username}}), "
+ "Please Verify Your New Email Address in {{portal_title}}",
+ body="""
+ <p>
+ Dear {{first_name}} {{last_name}},
+ </p>
+
+ <p>
+ Before your email address change can be processed, you need to verify
+ your new email address ({{email}}). Click the link below to verify your email
+ address:
+ </p>
+
+ <p><a href="{{url}}">{{url}}</a></p>
+ """.strip())
+ verify_email_template.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('django_airavata_auth', '0007_auto_20200917_1610'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='emailtemplate',
+ name='template_type',
+ field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template'), (5, 'Verify Email Change Template')], primary_key=True, serialize=False),
+ ),
+ migrations.CreateModel(
+ name='PendingEmailChange',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email_address', models.EmailField(max_length=254)),
+ ('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)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.RunPython(default_templates, migrations.RunPython.noop),
+ ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 082a4a1..c8c0da3 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -7,6 +7,7 @@ VERIFY_EMAIL_TEMPLATE = 1
NEW_USER_EMAIL_TEMPLATE = 2
PASSWORD_RESET_EMAIL_TEMPLATE = 3
USER_ADDED_TO_GROUP_TEMPLATE = 4
+VERIFY_EMAIL_CHANGE_TEMPLATE = 5
class EmailVerification(models.Model):
@@ -24,6 +25,7 @@ class EmailTemplate(models.Model):
(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'),
+ (VERIFY_EMAIL_CHANGE_TEMPLATE, 'Verify Email Change Template'),
)
template_type = models.IntegerField(
primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
@@ -73,3 +75,12 @@ class UserInfo(models.Model):
def __str__(self):
return f"{self.claim}={self.value}"
+
+
+class PendingEmailChange(models.Model):
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ email_address = models.EmailField()
+ 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)
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 6b51cef..310b4a0 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -1,14 +1,62 @@
+import logging
+
+from django.conf import settings
from django.contrib.auth import get_user_model
+from django.db.transaction import atomic
+from django.template import Context
+from django.urls import reverse
from rest_framework import serializers
+from . import models, utils
+
+logger = logging.getLogger(__name__)
+
class UserSerializer(serializers.ModelSerializer):
+
+ # TODO: add a lookup of most recent PendingEmailChange if any
class Meta:
model = get_user_model()
fields = ['id', 'username', 'first_name', 'last_name', 'email']
+ @atomic
def update(self, instance, validated_data):
+ request = self.context['request']
instance.first_name = validated_data['first_name']
instance.last_name = validated_data['last_name']
+ if instance.email != validated_data['email']:
+ # Delete any unverified pending email changes
+ models.PendingEmailChange.objects.filter(user=request.user, verified=False).delete()
+ # Email doesn't get updated until it is verified. Create a pending
+ # email change record in the meantime
+ pending_email_change = models.PendingEmailChange.objects.create(user=request.user, email_address=validated_data['email'])
+ self._send_email_verification_link(pending_email_change)
instance.save()
+ # save in the user profile service too
+ user_profile_client = request.profile_service['user_profile']
+ airavata_user_profile = user_profile_client.getUserProfileById(
+ request.authz_token, request.user.username, settings.GATEWAY_ID)
+ airavata_user_profile.firstName = instance.first_name
+ airavata_user_profile.lastName = instance.last_name
+ user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
return instance
+
+ def _send_email_verification_link(self, pending_email_change):
+
+ request = self.context['request']
+ verification_uri = request.build_absolute_uri(
+ reverse(
+ 'django_airavata_auth:verify_email_change', kwargs={
+ 'code': pending_email_change.verification_code}))
+ logger.debug(
+ "verification_uri={}".format(verification_uri))
+
+ context = Context({
+ "username": pending_email_change.user.username,
+ "email": pending_email_change.email_address,
+ "first_name": pending_email_change.user.first_name,
+ "last_name": pending_email_change.user.last_name,
+ "portal_title": settings.PORTAL_TITLE,
+ "url": verification_uri,
+ })
+ utils.send_email_to_user(models.VERIFY_EMAIL_CHANGE_TEMPLATE, context)
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index aecfc97..844b3a0 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -10,7 +10,7 @@
<b-form-input v-model="user.last_name" />
</b-form-group>
<b-form-group label="Email">
- <b-form-input disabled :value="user.email" />
+ <b-form-input v-model="user.email" />
</b-form-group>
<b-button variant="primary" @click="$emit('save', user)">Save</b-button>
<b-button>Cancel</b-button>
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index 9f193c3..d8de4a2 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -32,5 +32,7 @@ urlpatterns = [
views.login_desktop_success, name="login_desktop_success"),
url(r'^refreshed-token-desktop$', views.refreshed_token_desktop,
name="refreshed_token_desktop"),
- url(r'^user-profile/', views.user_profile),
+ url(r'^user-profile/', views.user_profile, name="user_profile"),
+ url(r'^verify-email-change/(?P<code>[\w-]+)/$', views.verify_email_change,
+ name="verify_email_change"),
]
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index e599abe..3fc0c40 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -8,6 +8,7 @@ from django.contrib import messages
from django.contrib.auth import authenticate, get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
+from django.db.transaction import atomic
from django.forms import ValidationError
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import redirect, render, resolve_url
@@ -543,3 +544,42 @@ class UserViewSet(viewsets.ModelViewSet):
@action(detail=False)
def current(self, request):
return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
+
+
+@login_required
+@atomic
+def verify_email_change(request, code):
+ try:
+ pending_email_change = models.PendingEmailChange.objects.get(user=request.user, verification_code=code)
+ pending_email_change.verified = True
+ pending_email_change.save()
+ request.user.email = pending_email_change.email_address
+ request.user.save()
+
+ user_profile_client = request.profile_service['user_profile']
+ airavata_user_profile = user_profile_client.getUserProfileById(
+ request.authz_token, request.user.username, settings.GATEWAY_ID)
+ airavata_user_profile.emails = [pending_email_change.email_address]
+ user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
+
+ # TODO: add success message
+ return redirect(reverse('django_airavata_auth:user_profile'))
+ except ObjectDoesNotExist:
+ # if doesn't exist, give user a form where they can enter their
+ # username to resend verification code
+ logger.exception("PendingEmailChange object doesn't exist for "
+ "code {}".format(code))
+ # TODO: add error message
+ # 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:user_profile'))
+ except Exception:
+ logger.exception("Email change verification processing failed!")
+ # TODO: add error message
+ # messages.error(
+ # request,
+ # "Email verification failed. Please try clicking the email "
+ # "verification link again later.")
+ return redirect(reverse('django_airavata_auth:user_profile'))