This is an automated email from the ASF dual-hosted git repository.
yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
The following commit(s) were added to refs/heads/main by this push:
new 5da3e569c feat(portal): de-Thrift the auth login path + browser
session-token bridge (Track D, D5) (#179)
5da3e569c is described below
commit 5da3e569c8b1bbf368ecdfdc32d902a27ab1cf5d
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 21:26:59 2026 -0400
feat(portal): de-Thrift the auth login path + browser session-token bridge
(Track D, D5) (#179)
The browser session login authenticated against Keycloak fine but then
hung: login(request, user) -> user_logged_in -> signals.initialize_user_
profile made a Thrift user_profile_client_pool.doesUserExist call, and
gateway_groups_middleware made Thrift getGatewayGroups + group_manager
calls -- both time out because the legacy Thrift server read-times-out.
- signals.initialize_user_profile: use request.airavata.iam.does_user_
exist; drop the Thrift initializeUserProfile (the gRPC backend
provisions the profile server-side; admins are emailed for new complete
profiles). Wrapped so a failure never blocks login.
- middleware.set_admin_group_attributes / gateway_groups_middleware: use
request.airavata.compute.get_gateway_groups() (admins_group_id /
read_only_admins_group_id) and request.airavata.sharing.gm_get_all_
groups_user_belongs() instead of the Thrift client + profile_service.
- KeycloakTokenAuthentication: when there is no Bearer header, fall back to
the session-stored ACCESS_TOKEN, so the existing browser session
authenticates against the token-only API without a frontend change
(interim until the frontend sends the token as a Bearer header).
Verified end to end: cookie-jar login POST -> 302 to the dashboard in
~0.08s (was hanging), and a subsequent /api/projects/ call carrying only
the session cookie -> 200 via the session-token bridge.
---
.../django_airavata/apps/auth/middleware.py | 21 +++++++------
.../django_airavata/apps/auth/signals.py | 36 +++++++++++++---------
.../apps/auth/token_authentication.py | 15 +++++++--
3 files changed, 44 insertions(+), 28 deletions(-)
diff --git a/airavata-django-portal/django_airavata/apps/auth/middleware.py
b/airavata-django-portal/django_airavata/apps/auth/middleware.py
index 53569325a..319e5fa63 100644
--- a/airavata-django-portal/django_airavata/apps/auth/middleware.py
+++ b/airavata-django-portal/django_airavata/apps/auth/middleware.py
@@ -1,5 +1,4 @@
"""Django Airavata Auth Middleware."""
-import copy
import logging
from django.conf import settings
@@ -31,16 +30,21 @@ def authz_token_middleware(get_response):
return middleware
+def _gateway_groups_dict(request):
+ """Fetch the gateway's admin group ids via the gRPC compute facade."""
+ gg = request.airavata.compute.get_gateway_groups()
+ return {'adminsGroupId': gg.admins_group_id,
+ 'readOnlyAdminsGroupId': gg.read_only_admins_group_id}
+
+
def set_admin_group_attributes(request, gateway_groups=None):
"""Set is_gateway_admin and is_read_only_gateway_admin request attrs."""
if gateway_groups is None:
- gateway_groups =
request.airavata_client.getGatewayGroups(request.authz_token)
- gateway_groups = copy.deepcopy(gateway_groups.__dict__)
+ gateway_groups = _gateway_groups_dict(request)
admins_group_id = gateway_groups['adminsGroupId']
read_only_admins_group_id = gateway_groups['readOnlyAdminsGroupId']
- group_manager_client = request.profile_service['group_manager']
- group_memberships = group_manager_client.getAllGroupsUserBelongs(
- request.authz_token, request.user.username + "@" + settings.GATEWAY_ID)
+ group_memberships =
request.airavata.sharing.gm_get_all_groups_user_belongs(
+ request.user.username + "@" + settings.GATEWAY_ID)
group_ids = [group.id for group in group_memberships]
request.is_gateway_admin = admins_group_id in group_ids
request.is_read_only_gateway_admin = read_only_admins_group_id in group_ids
@@ -63,10 +67,7 @@ def gateway_groups_middleware(get_response):
# Load the GatewayGroups and check if user is in the Admins and/or
# Read Only Admins groups
if not request.session.get('GATEWAY_GROUPS'):
- gateway_groups = request.airavata_client.getGatewayGroups(
- request.authz_token)
- gateway_groups_dict = copy.deepcopy(gateway_groups.__dict__)
- request.session['GATEWAY_GROUPS'] = gateway_groups_dict
+ request.session['GATEWAY_GROUPS'] =
_gateway_groups_dict(request)
set_admin_group_attributes(request,
gateway_groups=request.session.get("GATEWAY_GROUPS"))
# Gateway Admins are made 'superuser' in Django so they can edit
# pages in the CMS
diff --git a/airavata-django-portal/django_airavata/apps/auth/signals.py
b/airavata-django-portal/django_airavata/apps/auth/signals.py
index 466e20bff..96d180d69 100644
--- a/airavata-django-portal/django_airavata/apps/auth/signals.py
+++ b/airavata-django-portal/django_airavata/apps/auth/signals.py
@@ -7,7 +7,6 @@ from django.shortcuts import reverse
from django.template import Context
from django_airavata.apps.api.signals import user_added_to_group
-from django_airavata.utils import user_profile_client_pool
from . import models, utils
@@ -38,20 +37,27 @@ def initialize_user_profile(sender, request, user,
**kwargs):
# have an Airavata user profile (See IAMAdminServices.enableUser). The
# following is necessary for users coming from federated login who don't
# need to verify their email.
- if request.authz_token is not None:
- if not user_profile_client_pool.doesUserExist(request.authz_token,
- user.username,
- settings.GATEWAY_ID):
- if user.user_profile.is_complete:
-
user_profile_client_pool.initializeUserProfile(request.authz_token)
- log.info("initialized user profile for
{}".format(user.username))
- # Since user profile created, inform admins of new user
- utils.send_new_user_email(
- request, user.username, user.email, user.first_name,
user.last_name)
- log.info("sent new user email for user
{}".format(user.username))
- else:
- log.info(f"user profile not complete for {user.username}, "
- "skipping initializing Airavata user profile")
+ if request.authz_token is None:
+ return
+ try:
+ exists = request.airavata.iam.does_user_exist(
+ user.username, settings.GATEWAY_ID)
+ except Exception:
+ log.warning("Could not check Airavata user existence for %s",
+ user.username, exc_info=True)
+ return
+ if not exists:
+ if user.user_profile.is_complete:
+ # New federated-login user with a complete profile. Inform admins;
+ # the Airavata user profile is provisioned server-side on first
+ # authenticated request.
+ utils.send_new_user_email(
+ request, user.username, user.email, user.first_name,
+ user.last_name)
+ log.info("sent new user email for user {}".format(user.username))
+ else:
+ log.info(f"user profile not complete for {user.username}, "
+ "skipping initializing Airavata user profile")
else:
log.warning(f"Logged in user {user.username} has no access token")
diff --git
a/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
b/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
index cb5811be7..a78313d7d 100644
--- a/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
+++ b/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
@@ -65,9 +65,18 @@ class
KeycloakTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
header = request.META.get('HTTP_AUTHORIZATION', '')
- if not header.startswith('Bearer '):
- return None
- token = header[len('Bearer '):].strip()
+ if header.startswith('Bearer '):
+ token = header[len('Bearer '):].strip()
+ else:
+ # Browser bridge: the session login flow stores the Keycloak access
+ # token in the session; use it when no Authorization header is sent
+ # so the existing browser session authenticates against the
+ # token-only API. (Final state: the frontend sends the token as a
+ # Bearer header and the session login is removed.)
+ session = getattr(request, 'session', None)
+ token = session.get('ACCESS_TOKEN') if session is not None else
None
+ if not token:
+ return None
try:
signing_key = _jwks().get_signing_key_from_jwt(token)
claims = jwt.decode(