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)