Skip to content

Commit 011133f

Browse files
author
Juan Puerto
committed
feat: Add group membership checks to authentication.
1 parent c837622 commit 011133f

File tree

2 files changed

+135
-7
lines changed

2 files changed

+135
-7
lines changed

src/user_workspaces_server/config_schemas/schemas/authentication/GlobusUserAuthentication.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@
3737
"health_check_url": {
3838
"type": "string",
3939
"description": "URL for health check endpoint"
40+
},
41+
"allowed_globus_groups": {
42+
"type": "array",
43+
"items": {
44+
"type": "string",
45+
"description": "Globus group UUID"
46+
},
47+
"description": "List of Globus group UUIDs. Users must be members of at least one group to authenticate. If empty, group checking is disabled.",
48+
"default": []
4049
}
4150
}
4251
}

src/user_workspaces_server/controllers/userauthenticationmethods/globus_user_authentication.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import globus_sdk
55
from flask.wrappers import Response as flask_response
66
from hubmap_commons.hm_auth import AuthHelper
7-
from rest_framework.exceptions import ParseError
7+
from rest_framework.exceptions import ParseError, PermissionDenied
88
from rest_framework.response import Response
99

1010
from user_workspaces_server.controllers.userauthenticationmethods.abstract_user_authentication import (
@@ -21,13 +21,108 @@ def __init__(self, config):
2121
client_secret = self.connection_details["client_secret"]
2222
self.authentication_type = self.connection_details["authentication_type"]
2323
self.oauth = globus_sdk.ConfidentialAppAuthClient(client_id, client_secret)
24+
self.allowed_globus_groups = self.connection_details.get("allowed_globus_groups", [])
2425
if not AuthHelper.isInitialized():
2526
self.auth_helper = AuthHelper.create(clientId=client_id, clientSecret=client_secret)
2627
else:
2728
self.auth_helper = AuthHelper.instance()
2829

2930
def has_permission(self, internal_user):
30-
pass
31+
"""
32+
Verify user has permission by checking external user mapping exists
33+
and optionally validating Globus group membership.
34+
35+
Returns:
36+
ExternalUserMapping on success, False on failure
37+
"""
38+
external_user_mapping = self.get_external_user_mapping(
39+
{"user_id": internal_user, "user_authentication_name": type(self).__name__}
40+
)
41+
42+
if not external_user_mapping:
43+
# No mapping exists - user needs to authenticate first
44+
return False
45+
46+
# If group checking is enabled, validate membership
47+
if self.allowed_globus_groups:
48+
try:
49+
# Extract groups token from stored external_user_details
50+
external_user_details = external_user_mapping.external_user_details or {}
51+
groups_token = external_user_details.get("globus_groups_token")
52+
53+
if not groups_token:
54+
logger.error(
55+
f"Groups token not found for user {internal_user.username}. "
56+
"User may need to re-authenticate."
57+
)
58+
return False
59+
60+
# Check if user is still a member of allowed groups
61+
if not self._check_group_membership(
62+
groups_token, external_user_mapping.external_user_id
63+
):
64+
logger.warning(
65+
f"User {internal_user.username} is no longer a member of allowed Globus groups."
66+
)
67+
return False
68+
69+
except Exception as e:
70+
logger.error(
71+
f"Error checking group membership for {internal_user.username}: {repr(e)}"
72+
)
73+
return False
74+
75+
# User has valid mapping and (if required) is in allowed groups
76+
return external_user_mapping
77+
78+
def _check_group_membership(self, groups_token, user_id):
79+
"""
80+
Check if user is a member of any allowed Globus groups.
81+
82+
Args:
83+
groups_token: Access token for Globus Groups API
84+
user_id: Globus user ID (sub)
85+
86+
Returns:
87+
True if user is in at least one allowed group or if no groups configured, False otherwise
88+
"""
89+
if not self.allowed_globus_groups:
90+
# No groups configured - skip check
91+
return True
92+
93+
try:
94+
# Create GroupsClient with access token
95+
authorizer = globus_sdk.AccessTokenAuthorizer(groups_token)
96+
groups_client = globus_sdk.GroupsClient(authorizer=authorizer)
97+
98+
# Get user's group memberships
99+
user_groups = groups_client.get_my_groups()
100+
101+
# Extract group IDs from response
102+
user_group_ids = {group["id"] for group in user_groups}
103+
104+
# Check if user is in any allowed group (OR logic)
105+
allowed_groups_set = set(self.allowed_globus_groups)
106+
intersection = user_group_ids.intersection(allowed_groups_set)
107+
108+
if intersection:
109+
logger.info(f"User {user_id} is member of allowed groups: {intersection}")
110+
return True
111+
else:
112+
logger.warning(
113+
f"User {user_id} is not a member of any allowed groups. "
114+
f"User groups: {user_group_ids}, Allowed: {allowed_groups_set}"
115+
)
116+
return False
117+
118+
except globus_sdk.GlobusAPIError as e:
119+
logger.error(f"Globus API error checking groups for {user_id}: {e.code} - {e.message}")
120+
# Fail closed - deny access on API errors
121+
return False
122+
except Exception as e:
123+
logger.error(f"Unexpected error checking groups for {user_id}: {repr(e)}")
124+
# Fail closed - deny access on unexpected errors
125+
return False
31126

32127
def api_authenticate(self, request):
33128
try:
@@ -55,6 +150,19 @@ def api_authenticate(self, request):
55150
}
56151
)
57152

153+
# Check whether the user is part of predefined set of Globus groups
154+
if not external_user_mapping and self.allowed_globus_groups:
155+
# For new users, check group membership before creating account
156+
groups_token = globus_user_info.get("globus_groups_token")
157+
if not groups_token:
158+
raise PermissionDenied("Groups token not available for authentication.")
159+
160+
if not self._check_group_membership(groups_token, globus_user_info["sub"]):
161+
raise PermissionDenied(
162+
"User is not a member of any allowed Globus groups. "
163+
"Please contact your administrator for access."
164+
)
165+
58166
if not external_user_mapping:
59167
# Since its Globus, lets get the username from the email
60168
username = globus_user_info["email"].split("@")[0]
@@ -80,6 +188,7 @@ def api_authenticate(self, request):
80188
"user_authentication_name": type(self).__name__,
81189
"external_user_id": globus_user_info["sub"],
82190
"external_username": globus_user_info["username"],
191+
"external_user_details": globus_user_info,
83192
}
84193
)
85194
return internal_user
@@ -125,13 +234,23 @@ def globus_oauth_get_user_info(self, body):
125234
code = body["code"]
126235
tokens = self.oauth.oauth2_exchange_code_for_tokens(code)
127236

128-
# Need to add call here to grab user profile info
129-
return self.introspect_globus_user(
130-
tokens.by_resource_server["groups.api.globus.org"]["access_token"]
131-
)
237+
# Get user profile info using groups token
238+
groups_token = tokens.by_resource_server["groups.api.globus.org"]["access_token"]
239+
user_info = self.introspect_globus_user(groups_token)
240+
241+
# Store the groups token for later group membership checking
242+
user_info["globus_groups_token"] = groups_token
243+
244+
return user_info
132245

133246
def globus_token_get_user_info(self, body):
134247
if "auth_token" not in body:
135248
raise ParseError("Missing auth_token.")
136249

137-
return self.introspect_globus_user(body.get("auth_token"))
250+
auth_token = body.get("auth_token")
251+
user_info = self.introspect_globus_user(auth_token)
252+
253+
# Store the auth token as groups token for group membership checking
254+
user_info["globus_groups_token"] = auth_token
255+
256+
return user_info

0 commit comments

Comments
 (0)