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 2020/09/17 22:00:24 UTC
[airavata-django-portal] 01/02: AIRAVATA-3319 Model for storing
userinfo claims and evaluating user profile completeness
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 3bb887154f8c81471b67731b5593fc54730bb423
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Sep 17 12:14:55 2020 -0400
AIRAVATA-3319 Model for storing userinfo claims and evaluating user profile completeness
---
django_airavata/apps/auth/backends.py | 73 ++++++++++++++++------
.../auth/migrations/0007_auto_20200917_1610.py | 43 +++++++++++++
django_airavata/apps/auth/models.py | 30 +++++++++
django_airavata/templates/base.html | 3 +-
4 files changed, 129 insertions(+), 20 deletions(-)
diff --git a/django_airavata/apps/auth/backends.py b/django_airavata/apps/auth/backends.py
index 89827a5..79507f0 100644
--- a/django_airavata/apps/auth/backends.py
+++ b/django_airavata/apps/auth/backends.py
@@ -9,7 +9,7 @@ from django.views.decorators.debug import sensitive_variables
from oauthlib.oauth2 import InvalidGrantError, LegacyApplicationClient
from requests_oauthlib import OAuth2Session
-from . import utils
+from . import models, utils
logger = logging.getLogger(__name__)
@@ -150,25 +150,62 @@ class KeycloakBackend(object):
def _process_userinfo(self, request, userinfo):
logger.debug("userinfo: {}".format(userinfo))
+ sub = userinfo['sub']
username = userinfo['preferred_username']
email = userinfo['email']
first_name = userinfo['given_name']
last_name = userinfo['family_name']
- request.session['USERINFO'] = userinfo
+
+ user = self._get_or_create_user(sub, username)
+ user_profile = user.user_profile
+
+ # Save the user info claims
+ for (claim, value) in userinfo.items():
+ if user_profile.userinfo_set.filter(claim=claim).exists():
+ userinfo_claim = user_profile.userinfo_set.get(claim=claim)
+ userinfo_claim.value = value
+ userinfo_claim.save()
+ else:
+ user_profile.userinfo_set.create(claim=claim, value=value)
+
+ # Update User model fields
+ user = user_profile.user
+ user.email = email
+ user.first_name = first_name
+ user.last_name = last_name
+ user.save()
+ return user
+
+ def _get_or_create_user(self, sub, username):
+
try:
- user = User.objects.get(username=username)
- # Update these fields each time, in case they have changed
- user.email = email
- user.first_name = first_name
- user.last_name = last_name
- user.save()
- return user
- except User.DoesNotExist:
- user = User(username=username,
- first_name=first_name,
- last_name=last_name,
- email=email)
- user.save()
- utils.send_new_user_email(
- request, username, email, first_name, last_name)
- return user
+ user_profile = models.UserProfile.objects.get(
+ userinfo__claim='sub', userinfo__value=sub)
+ return user_profile.user
+ except models.UserProfile.DoesNotExist:
+ try:
+ # For backwards compatibility, lookup by username
+ user = User.objects.get(username=username)
+ # Make sure there is a user_profile with the sub claim, which
+ # will be used to do the lookup next time
+ if not hasattr(user, 'user_profile'):
+ user_profile = models.UserProfile(user=user)
+ user_profile.save()
+ user_profile.userinfo_set.create(
+ claim='sub', value=sub)
+ else:
+ userinfo = user.user_profile.userinfo_set.get(claim='sub')
+ logger.warning(
+ f"User {username} exists but sub claims don't match: "
+ f"old={userinfo.value}, new={sub}. Updating to new "
+ "sub claim.")
+ userinfo.value = sub
+ userinfo.save()
+ return user
+ except User.DoesNotExist:
+ user = User(username=username)
+ user.save()
+ user_profile = models.UserProfile(user=user)
+ user_profile.save()
+ user_profile.userinfo_set.create(claim='sub', value=sub)
+ return user
diff --git a/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py b/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py
new file mode 100644
index 0000000..4938ebf
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0007_auto_20200917_1610.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-09-17 16:10
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('django_airavata_auth', '0006_emailverification_next'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserInfo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('claim', models.CharField(max_length=64)),
+ ('value', models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UserProfile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('username_locked', models.BooleanField(default=False)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='userinfo',
+ name='user_profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_airavata_auth.UserProfile'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='userinfo',
+ unique_together=set([('user_profile', 'claim')]),
+ ),
+ ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 2e2f212..082a4a1 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -1,5 +1,6 @@
import uuid
+from django.conf import settings
from django.db import models
VERIFY_EMAIL_TEMPLATE = 1
@@ -43,3 +44,32 @@ class PasswordResetRequest(models.Model):
reset_code = models.CharField(
max_length=36, unique=True, default=uuid.uuid4)
created_date = models.DateTimeField(auto_now_add=True)
+
+
+class UserProfile(models.Model):
+ user = models.OneToOneField(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE, related_name="user_profile")
+ # TODO: maybe this can be derived from whether there exists an Airavata
+ # User Profile for the user's username
+ username_locked = models.BooleanField(default=False)
+
+ @property
+ def is_complete(self):
+ # TODO: implement this to check if there are any missing fields on the
+ # User model (email, first_name, last_name) or if the username was is
+ # invalid (for example if defaulted to the IdP's 'sub' claim) or if
+ # there are any extra profile fields that are not valid
+ return False
+
+
+class UserInfo(models.Model):
+ claim = models.CharField(max_length=64)
+ value = models.CharField(max_length=255)
+ user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
+
+ class Meta:
+ unique_together = ['user_profile', 'claim']
+
+ def __str__(self):
+ return f"{self.claim}={self.value}"
diff --git a/django_airavata/templates/base.html b/django_airavata/templates/base.html
index 09778e1..e2e8ba5 100644
--- a/django_airavata/templates/base.html
+++ b/django_airavata/templates/base.html
@@ -202,8 +202,7 @@
<a href=# class="dropdown-toggle text-dark" id=dropdownMenuButton data-toggle=dropdown aria-haspopup=true
aria-expanded=false>
<i class="fa fa-user mr-2"></i>
- {{ request.session.USERINFO.given_name }}
- {{ request.session.USERINFO.family_name }}
+ {{ request.user.first_name }} {{ request.user.last_name }}
</a>
<div class=dropdown-menu aria-labelledby=dropdownMenuButton>
{% comment %} <a class=dropdown-item href=#>User settings</a> {% endcomment %}