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 %}