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 712889073 feat(portal): de-Thrift the auth IAM user-management to gRPC
(Track D, D5) (#180)
712889073 is described below
commit 7128890734fa914cb85e0e906dfb87884e7897de
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 21:46:05 2026 -0400
feat(portal): de-Thrift the auth IAM user-management to gRPC (Track D, D5)
(#180)
Repoint the auth app's user-management path from the legacy Thrift clients
to
the new gRPC `iam` facade, using direct proto types (no Thrift-name
roundtrip).
- `iam_admin_client.py`: the IAM admin operations (username availability,
user
registration, enable, reset password, lookup) ran on the Thrift
`iamadmin_client_pool`. They now build a short-lived
service-account-scoped
`AiravataClient` and call the gRPC `iam` facade. These run in
unauthenticated
contexts (account creation, email verification, password reset), so the
client carries the Keycloak service-account token plus a `gatewayID` claim
(the IAM admin service resolves the Keycloak realm from the gateway
claim).
`update_user`/`update_username` are unchanged — they already talk to the
Keycloak admin REST API directly, not Thrift.
- `serializers.py` / `views.py`: the user-profile updates
(`UserSerializer.update`,
`UserViewSet.verify_email_change`) used the Thrift `profile_service`
user-profile client. They now use `request.airavata.iam` (does_user_exist
/
get_user_profile_by_id / update_user_profile) in the authenticated request
context.
- Consumers read the returned protobuf `UserProfile` directly
(`first_name`/`last_name`/`emails`/`user_id`) instead of the Thrift
attribute
names.
---
.../django_airavata/apps/auth/iam_admin_client.py | 77 ++++++++++++++--------
.../django_airavata/apps/auth/serializers.py | 16 ++---
.../django_airavata/apps/auth/views.py | 26 ++++----
3 files changed, 68 insertions(+), 51 deletions(-)
diff --git
a/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
b/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
index 6106626ae..c080409b6 100644
--- a/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
+++ b/airavata-django-portal/django_airavata/apps/auth/iam_admin_client.py
@@ -1,70 +1,91 @@
-"""
-Wrapper around the IAM Admin Services client.
+"""IAM admin operations via the gRPC ``iam`` facade.
+
+These operations (username availability, user registration, enable/reset) run
in
+unauthenticated contexts — account creation, email verification, password
reset —
+so they use a Keycloak **service-account** token rather than a logged-in user's
+token. Each call builds a short-lived ``AiravataClient`` scoped to that token
and
+talks to the gRPC ``iam`` facade; callers consume the returned protobuf
+``UserProfile`` directly (``user_id``/``first_name``/``last_name``/``emails``).
+
+``update_user``/``update_username`` talk to the Keycloak admin REST API
directly
+(not the gRPC backend) and are unchanged.
"""
import logging
+from contextlib import contextmanager
from urllib.parse import urlparse
import requests
from django.conf import settings
-from django_airavata.utils import iamadmin_client_pool
+from django_airavata.airavata_grpc import build_airavata_client
from . import utils
logger = logging.getLogger(__name__)
+@contextmanager
+def _iam():
+ """Yield the gRPC ``iam`` facade scoped to the Keycloak service account.
+
+ The IAM admin operations resolve the Keycloak realm from the request's
+ gateway claim, so the service-account client carries ``gatewayID`` in its
+ ``x-claims`` metadata (mirroring the legacy service-account
``AuthzToken``).
+ """
+ access_token = utils.get_service_account_authz_token().accessToken
+ client = build_airavata_client(
+ access_token, claims={'gatewayID': settings.GATEWAY_ID})
+ try:
+ yield client.iam
+ finally:
+ client.close()
+
+
def is_username_available(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.isUsernameAvailable(authz_token, username)
+ with _iam() as iam:
+ return iam.is_username_available(username)
def register_user(username, email_address, first_name, last_name, password):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.registerUser(
- authz_token,
- username,
- email_address,
- first_name,
- last_name,
- password)
+ with _iam() as iam:
+ return iam.register_user(
+ username, email_address, first_name, last_name, password)
def is_user_enabled(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.isUserEnabled(authz_token, username)
+ with _iam() as iam:
+ return iam.is_user_enabled(username)
def enable_user(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.enableUser(authz_token, username)
+ with _iam() as iam:
+ return iam.enable_user(username)
def delete_user(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.deleteUser(authz_token, username)
+ with _iam() as iam:
+ return iam.delete_iam_user(username)
def is_user_exist(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.isUserExist(authz_token, username)
+ with _iam() as iam:
+ return iam.is_user_exist(username)
def get_user(username):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.getUser(authz_token, username)
+ with _iam() as iam:
+ return iam.get_iam_user(username)
def get_users(offset, limit, search=None):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.getUsers(authz_token, offset, limit, search)
+ with _iam() as iam:
+ return iam.get_iam_users(offset, limit, search or "")
def reset_user_password(username, new_password):
- authz_token = utils.get_service_account_authz_token()
- return iamadmin_client_pool.resetUserPassword(
- authz_token, username, new_password)
+ with _iam() as iam:
+ return iam.reset_user_password(username, new_password)
def update_username(username, new_username):
diff --git a/airavata-django-portal/django_airavata/apps/auth/serializers.py
b/airavata-django-portal/django_airavata/apps/auth/serializers.py
index 4b35c111f..48a5a80b3 100644
--- a/airavata-django-portal/django_airavata/apps/auth/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/auth/serializers.py
@@ -68,17 +68,15 @@ class UserSerializer(serializers.ModelSerializer):
self._send_email_verification_link(request, pending_email_change)
instance.save()
# save in the user profile service too
- user_profile_client = request.profile_service['user_profile']
+ iam = request.airavata.iam
# update the Airavata profile if it exists
- if user_profile_client.doesUserExist(request.authz_token,
- request.user.username,
- settings.GATEWAY_ID):
- airavata_user_profile = user_profile_client.getUserProfileById(
- request.authz_token, request.user.username,
settings.GATEWAY_ID)
- airavata_user_profile.firstName = instance.first_name
- airavata_user_profile.lastName = instance.last_name
- user_profile_client.updateUserProfile(request.authz_token,
airavata_user_profile)
+ if iam.does_user_exist(request.user.username, settings.GATEWAY_ID):
+ airavata_user_profile = iam.get_user_profile_by_id(
+ request.user.username, settings.GATEWAY_ID)
+ airavata_user_profile.first_name = instance.first_name
+ airavata_user_profile.last_name = instance.last_name
+ iam.update_user_profile(airavata_user_profile)
# otherwise, update in Keycloak user store
else:
iam_admin_client.update_user(request.user.username,
diff --git a/airavata-django-portal/django_airavata/apps/auth/views.py
b/airavata-django-portal/django_airavata/apps/auth/views.py
index a05b2480e..0a27cb40b 100644
--- a/airavata-django-portal/django_airavata/apps/auth/views.py
+++ b/airavata-django-portal/django_airavata/apps/auth/views.py
@@ -260,8 +260,8 @@ def verify_email(request, code):
iam_admin_client.enable_user(username)
user_profile = iam_admin_client.get_user(username)
email_address = user_profile.emails[0]
- first_name = user_profile.firstName
- last_name = user_profile.lastName
+ first_name = user_profile.first_name
+ last_name = user_profile.last_name
utils.send_new_user_email(request,
username,
email_address,
@@ -305,8 +305,8 @@ def resend_email_link(request):
request,
username,
email_address,
- user_profile.firstName,
- user_profile.lastName)
+ user_profile.first_name,
+ user_profile.last_name)
messages.success(
request,
"Email verification link sent successfully. Please "
@@ -414,8 +414,8 @@ def _create_and_send_password_reset_request_link(request,
username):
context = Context({
"username": username,
"email": user.emails[0],
- "first_name": user.firstName,
- "last_name": user.lastName,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
"portal_title": settings.PORTAL_TITLE,
"url": verification_uri,
})
@@ -622,14 +622,12 @@ class UserViewSet(viewsets.ModelViewSet):
try:
# only update the airavata profile if it exists
- user_profile_client = request.profile_service['user_profile']
- if user_profile_client.doesUserExist(request.authz_token,
- request.user.username,
- settings.GATEWAY_ID):
- airavata_user_profile = user_profile_client.getUserProfileById(
- request.authz_token, user.username, settings.GATEWAY_ID)
- airavata_user_profile.emails =
[pending_email_change.email_address]
- user_profile_client.updateUserProfile(request.authz_token,
airavata_user_profile)
+ iam = request.airavata.iam
+ if iam.does_user_exist(request.user.username, settings.GATEWAY_ID):
+ airavata_user_profile = iam.get_user_profile_by_id(
+ user.username, settings.GATEWAY_ID)
+ airavata_user_profile.emails[:] =
[pending_email_change.email_address]
+ iam.update_user_profile(airavata_user_profile)
# otherwise, update the user's email in the Keycloak user store
else:
iam_admin_client.update_user(request.user.username,