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/06/25 18:26:07 UTC

[airavata-django-portal] 02/03: AIRAVATA-3468 Store IDP userinfo

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 a99970faa473696bdce85fefbb1f85da0d679b31
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jun 25 13:39:14 2021 -0400

    AIRAVATA-3468 Store IDP userinfo
    
    This is mostly for debugging purposes, to see what is and isn't included in CILogon userinfo.
---
 django_airavata/apps/auth/backends.py              | 34 +++++++++++++++++-
 .../auth/migrations/0009_auto_20210625_1725.py     | 41 ++++++++++++++++++++++
 django_airavata/apps/auth/models.py                | 17 +++++++++
 django_airavata/apps/auth/views.py                 |  6 ++--
 4 files changed, 95 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/auth/backends.py b/django_airavata/apps/auth/backends.py
index 24f3143..cc0df6b 100644
--- a/django_airavata/apps/auth/backends.py
+++ b/django_airavata/apps/auth/backends.py
@@ -29,7 +29,8 @@ class KeycloakBackend(object):
                      request=None,
                      username=None,
                      password=None,
-                     refresh_token=None):
+                     refresh_token=None,
+                     idp_alias=None):
         try:
             user = None
             access_token = None
@@ -71,6 +72,9 @@ class KeycloakBackend(object):
                     request)
                 self._process_token(request, token)
                 user = self._process_userinfo(request, userinfo)
+                if idp_alias is not None:
+                    self._store_idp_userinfo(user, token, idp_alias)
+                # TODO: if idp_alias, add idp userinfo too
                 access_token = token['access_token']
             # authz_token_middleware has already run, so must manually add
             # the `request.authz_token` attribute
@@ -260,3 +264,31 @@ class KeycloakBackend(object):
                 user_profile.save()
                 user_profile.userinfo_set.create(claim='sub', value=sub)
                 return user
+
+    def _store_idp_userinfo(self, user, token, idp_alias):
+        try:
+            access_token = token['access_token']
+            logger.debug(f"access_token={access_token} for idp_alias={idp_alias}")
+            # fetch the idp's token
+            headers = {'Authorization': f'Bearer {access_token}'}
+            # For the following to work, in Keycloak the IDP should have 'Store
+            # Tokens' and 'Stored Tokens Readable' enabled and the user needs
+            # the broker/read-token role
+            r = requests.get(f"https://iamdev.scigap.org/auth/realms/seagrid/broker/{idp_alias}/token", headers=headers)
+            idp_token = r.json()
+            idp_headers = {'Authorization': f"Bearer {idp_token['access_token']}"}
+            r = requests.get("https://cilogon.org/oauth2/userinfo", headers=idp_headers)
+            userinfo = r.json()
+            logger.debug(f"userinfo={userinfo}")
+
+            # Save the idp user info claims
+            user_profile = user.user_profile
+            for (claim, value) in userinfo.items():
+                if user_profile.idp_userinfo.filter(idp_alias=idp_alias, claim=claim).exists():
+                    userinfo_claim = user_profile.idp_userinfo.get(idp_alias=idp_alias, claim=claim)
+                    userinfo_claim.value = value
+                    userinfo_claim.save()
+                else:
+                    user_profile.idp_userinfo.create(idp_alias=idp_alias, claim=claim, value=value)
+        except Exception:
+            logger.exception(f"Failed to store IDP userinfo for {user.username} from IDP {idp_alias}")
diff --git a/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py b/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py
new file mode 100644
index 0000000..1d38199
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0009_auto_20210625_1725.py
@@ -0,0 +1,41 @@
+# Generated by Django 2.2.23 on 2021-06-25 17:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0008_auto_20210422_1838'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='userinfo',
+            name='created_date',
+            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='userinfo',
+            name='updated_date',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.CreateModel(
+            name='IDPUserInfo',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('idp_alias', models.CharField(max_length=64)),
+                ('claim', models.CharField(max_length=64)),
+                ('value', models.CharField(max_length=255)),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='idp_userinfo', to='django_airavata_auth.UserProfile')),
+            ],
+            options={
+                'unique_together': {('user_profile', 'claim', 'idp_alias')},
+            },
+        ),
+    ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 86ab451..a7480ff 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -99,6 +99,8 @@ 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)
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
 
     class Meta:
         unique_together = ['user_profile', 'claim']
@@ -107,6 +109,21 @@ class UserInfo(models.Model):
         return f"{self.claim}={self.value}"
 
 
+class IDPUserInfo(models.Model):
+    idp_alias = models.CharField(max_length=64)
+    claim = models.CharField(max_length=64)
+    value = models.CharField(max_length=255)
+    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="idp_userinfo")
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
+
+    class Meta:
+        unique_together = ['user_profile', 'claim', 'idp_alias']
+
+    def __str__(self):
+        return f"{self.idp_alias}: {self.claim}={self.value}"
+
+
 class PendingEmailChange(models.Model):
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
     email_address = models.EmailField()
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index fb30761..9e1d2d4 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -3,6 +3,7 @@ import time
 from datetime import datetime, timedelta, timezone
 from urllib.parse import quote, urlencode
 
+import requests
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import authenticate, get_user_model, login, logout
@@ -138,7 +139,9 @@ def start_logout(request):
 def callback(request):
     try:
         login_desktop = request.GET.get('login_desktop', "false") == "true"
-        user = authenticate(request=request)
+        idp_alias = request.GET.get('idp_alias')
+        user = authenticate(request=request, idp_alias=idp_alias)
+
         if user is not None:
             login(request, user)
             if login_desktop:
@@ -153,7 +156,6 @@ def callback(request):
         messages.error(
             request,
             "Failed to process OAuth2 callback: {}".format(str(err)))
-        idp_alias = request.GET.get('idp_alias')
         if login_desktop:
             return _create_login_desktop_failed_response(
                 request, idp_alias=idp_alias)