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/21 18:46:16 UTC

[airavata-django-portal] 01/03: 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 dd587b3e49733b63c757cf10930d85dada6cb60b
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              | 71 +++++++++++++++++-----
 .../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(+), 18 deletions(-)

diff --git a/django_airavata/apps/auth/backends.py b/django_airavata/apps/auth/backends.py
index b21afc8..349d4d2 100644
--- a/django_airavata/apps/auth/backends.py
+++ b/django_airavata/apps/auth/backends.py
@@ -12,7 +12,7 @@ from requests_oauthlib import OAuth2Session
 
 from django_airavata.apps.auth.utils import get_authz_token
 
-from . import utils
+from . import models, utils
 
 logger = logging.getLogger(__name__)
 
@@ -200,23 +200,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()
-            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 8400f84..23c6794 100644
--- a/django_airavata/templates/base.html
+++ b/django_airavata/templates/base.html
@@ -203,8 +203,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 %}