Skip to content

Commit 30d4e41

Browse files
committed
Refactor JWT claims handling to use gateway endpoint
JWT claims are now exclusively fetched from the gateway service-index API instead of being included in the JWT token. Deprecated fields (objects, object_roles, global_roles) are removed from token processing and all RBAC logic now relies on gateway claims. Added helper to ResourceAPIClient for fetching claims, and updated tests to reflect the new claims source.
1 parent 28ef3a6 commit 30d4e41

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

ansible_base/jwt_consumer/common/auth.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ansible_base.lib.utils.auth import get_user_by_ansible_id
2020
from ansible_base.lib.utils.translations import translatableConditionally as _
2121
from ansible_base.resource_registry.models import Resource, ResourceType
22+
from ansible_base.resource_registry.rest_client import get_resource_server_client
2223
from ansible_base.resource_registry.signals.handlers import no_reverse_sync
2324

2425
logger = logging.getLogger("ansible_base.jwt_consumer.common.auth")
@@ -52,6 +53,7 @@ def __init__(self, user_fields=default_mapped_user_fields) -> None:
5253
self.cache = JWTCache()
5354
self.user = None
5455
self.token = None
56+
self.gateway_claims = None # Store claims from gateway
5557

5658
@log_excess_runtime(logger, debug_cutoff=0.01)
5759
def parse_jwt_token(self, request):
@@ -142,10 +144,47 @@ def parse_jwt_token(self, request):
142144
resource.service_id = self.token['service_id']
143145
resource.save(update_fields=['ansible_id', 'service_id'])
144146

147+
# Fetch JWT claims from gateway service-index (required for RBAC processing)
148+
user_ansible_id = self.token['sub']
149+
logger.debug(f"Fetching claims from gateway for user {user_ansible_id}")
150+
151+
jwt_claims = self._fetch_jwt_claims_from_gateway(user_ansible_id)
152+
153+
if jwt_claims:
154+
self.gateway_claims = jwt_claims
155+
logger.debug(f"Successfully loaded gateway claims for user {user_ansible_id}")
156+
else:
157+
logger.error(f"Failed to fetch claims from gateway for user {user_ansible_id}. RBAC processing will not be available.")
158+
# Note: We don't raise an exception here to allow basic authentication to succeed
159+
# RBAC processing will fail gracefully with appropriate error messages
145160
setattr(self.user, "resource_api_actions", self.token.get("resource_api_actions", None))
146161

147162
logger.info(f"User {self.user.username} authenticated from JWT auth")
148163

164+
165+
def _fetch_jwt_claims_from_gateway(self, user_ansible_id):
166+
"""
167+
Fetch JWT claims for a user from the gateway service-index API.
168+
Returns None if claims cannot be retrieved.
169+
"""
170+
try:
171+
client = get_resource_server_client("service-index")
172+
response = client.get_jwt_claims(user_ansible_id)
173+
174+
if response.status_code == 200:
175+
claims = response.json()
176+
logger.debug(f"Retrieved JWT claims from gateway for user {user_ansible_id}")
177+
return claims
178+
else:
179+
logger.warning(
180+
f"Failed to retrieve JWT claims from gateway for user {user_ansible_id}: "
181+
f"{response.status_code}"
182+
)
183+
return None
184+
except Exception as e:
185+
logger.error(f"Error fetching JWT claims from gateway for user {user_ansible_id}: {e}")
186+
return None
187+
149188
def log_and_raise(self, conditional_translate_object, expand_values={}, error_code=None):
150189
logger.error(conditional_translate_object.not_translated() % expand_values)
151190
translated_error_message = conditional_translate_object.translated() % expand_values
@@ -226,7 +265,9 @@ def validate_token(self, unencrypted_token, decryption_key, request_id=None):
226265
return validated_body
227266

228267
def decode_jwt_token(self, unencrypted_token, decryption_key, additional_options={}):
229-
local_required_field = ["sub", "user_data", "exp", "objects", "object_roles", "global_roles", "version"]
268+
# Core required fields - objects, object_roles, global_roles are now deprecated
269+
# and will be loaded from the gateway jwt_claims endpoint instead
270+
local_required_field = ["sub", "user_data", "exp", "version"]
230271
options = {"require": local_required_field}
231272
options.update(additional_options)
232273
return jwt.decode(
@@ -259,16 +300,23 @@ def get_role_definition(self, name: str) -> Optional[Model]:
259300
def process_rbac_permissions(self):
260301
"""
261302
This is a default process_permissions which should be usable if you are using RBAC from DAB
303+
Uses gateway claims data exclusively - no fallback to JWT token fields
262304
"""
263-
if self.token is None or self.user is None:
264-
logger.error("Unable to process rbac permissions because user or token is not defined, please call authenticate first")
305+
if self.user is None:
306+
logger.error("Unable to process rbac permissions because user is not defined, please call authenticate first")
307+
return
308+
309+
if self.gateway_claims is None:
310+
logger.error("Unable to process rbac permissions because gateway claims are not available. Ensure gateway jwt_claims endpoint is accessible.")
265311
return
266312

267313
from ansible_base.rbac.models import RoleUserAssignment
268314

269315
role_diff = RoleUserAssignment.objects.filter(user=self.user, role_definition__name__in=settings.ANSIBLE_BASE_JWT_MANAGED_ROLES)
270316

271-
for system_role_name in self.token.get("global_roles", []):
317+
# Process global roles from gateway claims
318+
global_roles = self.gateway_claims.get("global_roles", [])
319+
for system_role_name in global_roles:
272320
logger.debug(f"Processing system role {system_role_name} for {self.user.username}")
273321
rd = self.get_role_definition(system_role_name)
274322
if rd:
@@ -282,7 +330,11 @@ def process_rbac_permissions(self):
282330
logger.error(f"Unable to grant {self.user.username} system level role {system_role_name} because it does not exist")
283331
continue
284332

285-
for object_role_name in self.token.get('object_roles', {}).keys():
333+
# Process object roles from gateway claims
334+
object_roles = self.gateway_claims.get('object_roles', {})
335+
objects = self.gateway_claims.get('objects', {})
336+
337+
for object_role_name in object_roles.keys():
286338
rd = self.get_role_definition(object_role_name)
287339
if rd is None:
288340
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it does not exist")
@@ -291,11 +343,11 @@ def process_rbac_permissions(self):
291343
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it is not a JWT managed role")
292344
continue
293345

294-
object_type = self.token['object_roles'][object_role_name]['content_type']
295-
object_indexes = self.token['object_roles'][object_role_name]['objects']
346+
object_type = object_roles[object_role_name]['content_type']
347+
object_indexes = object_roles[object_role_name]['objects']
296348

297349
for index in object_indexes:
298-
object_data = self.token['objects'][object_type][index]
350+
object_data = objects[object_type][index]
299351
try:
300352
resource, obj = self.get_or_create_resource(object_type, object_data)
301353
except IntegrityError as e:
@@ -310,7 +362,7 @@ def process_rbac_permissions(self):
310362
role_diff = role_diff.exclude(pk=assignment.pk)
311363
logger.info(f"Granted user {self.user.username} role {object_role_name} to object {obj.name} with ansible_id {object_data['ansible_id']}")
312364

313-
# Remove all permissions not authorized by the JWT
365+
# Remove all permissions not authorized by the gateway claims
314366
for role_assignment in role_diff:
315367
rd = role_assignment.role_definition
316368
content_object = role_assignment.content_object
@@ -322,9 +374,17 @@ def process_rbac_permissions(self):
322374
def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optional[Resource], Optional[Model]]:
323375
"""
324376
Gets or creates a resource from a content type and its default data
377+
Uses gateway claims exclusively - no fallback to JWT token fields
325378
326379
This can only build or get organizations or teams
380+
Args:
381+
content_type: Type of content ('team', 'organization')
382+
data: Resource data dictionary
327383
"""
384+
if self.gateway_claims is None:
385+
logger.error("Unable to create resource because gateway claims are not available")
386+
return None, None
387+
328388
object_ansible_id = data['ansible_id']
329389
try:
330390
resource = Resource.objects.get(ansible_id=object_ansible_id)
@@ -337,7 +397,7 @@ def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optiona
337397
if content_type == 'team':
338398
# For a team we first have to make sure the org is there
339399
org_id = data['org']
340-
organization_data = self.token['objects']["organization"][org_id]
400+
organization_data = self.gateway_claims['objects']["organization"][org_id]
341401

342402
# Now that we have the org we can build a team
343403
org_resource, _ = self.get_or_create_resource("organization", organization_data)
@@ -359,7 +419,7 @@ def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optiona
359419

360420
return resource, resource.content_object
361421
else:
362-
logger.error(f"build_resource_stub does not know how to build an object of type {type}")
422+
logger.error(f"build_resource_stub does not know how to build an object of type {content_type}")
363423
return None, None
364424

365425

ansible_base/jwt_consumer/common/cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ def get_key_from_cache(self) -> Optional[str]:
4646

4747
def set_key_in_cache(self, key: str) -> None:
4848
cache.set(cache_key, key, timeout=self.get_cache_timeout())
49+

ansible_base/resource_registry/rest_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,10 @@ def list_team_assignments(self, team_ansible_id: Optional[str] = None, filters:
189189
return self._make_request("get", "role-team-assignments/", params=params)
190190

191191
def sync_assignment(self, assignment):
192-
from ansible_base.rbac.service_api.serializers import ServiceRoleTeamAssignmentSerializer, ServiceRoleUserAssignmentSerializer
192+
from ansible_base.rbac.service_api.serializers import (
193+
ServiceRoleTeamAssignmentSerializer,
194+
ServiceRoleUserAssignmentSerializer,
195+
)
193196

194197
if assignment._meta.model_name == 'roleuserassignment':
195198
serializer = ServiceRoleUserAssignmentSerializer(assignment)
@@ -227,3 +230,7 @@ def _sync_assignment(self, data, giving=True):
227230
url = f'role-{actor_type}-assignments/{sub_url}/'
228231

229232
return self._make_request(method="post", path=url, data=data)
233+
234+
def get_jwt_claims(self, user_ansible_id):
235+
"""Get JWT claims for a user from the gateway service-index."""
236+
return self._make_request("get", f"jwt_claims/{user_ansible_id}/")

test_app/tests/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,8 @@ def __init__(self):
542542
"email": "[email protected]",
543543
"is_superuser": False,
544544
},
545-
"objects": {},
546-
"object_roles": {},
547-
"global_roles": [],
545+
# NOTE: objects, object_roles, global_roles are deprecated
546+
# These fields are no longer used - data comes from gateway jwt_claims endpoint
548547
}
549548

550549
def encrypt_token(self):

0 commit comments

Comments
 (0)