Skip to content

Commit df0798d

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 df0798d

File tree

3 files changed

+77
-15
lines changed

3 files changed

+77
-15
lines changed

ansible_base/jwt_consumer/common/auth.py

Lines changed: 67 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,43 @@ 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+
def _fetch_jwt_claims_from_gateway(self, user_ansible_id):
165+
"""
166+
Fetch JWT claims for a user from the gateway service-index API.
167+
Returns None if claims cannot be retrieved.
168+
"""
169+
try:
170+
client = get_resource_server_client("service-index")
171+
response = client.get_jwt_claims(user_ansible_id)
172+
173+
if response.status_code == 200:
174+
claims = response.json()
175+
logger.debug(f"Retrieved JWT claims from gateway for user {user_ansible_id}")
176+
return claims
177+
else:
178+
logger.warning(f"Failed to retrieve JWT claims from gateway for user {user_ansible_id}: " f"{response.status_code}")
179+
return None
180+
except Exception as e:
181+
logger.error(f"Error fetching JWT claims from gateway for user {user_ansible_id}: {e}")
182+
return None
183+
149184
def log_and_raise(self, conditional_translate_object, expand_values={}, error_code=None):
150185
logger.error(conditional_translate_object.not_translated() % expand_values)
151186
translated_error_message = conditional_translate_object.translated() % expand_values
@@ -226,7 +261,9 @@ def validate_token(self, unencrypted_token, decryption_key, request_id=None):
226261
return validated_body
227262

228263
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"]
264+
# Core required fields - objects, object_roles, global_roles are now deprecated
265+
# and will be loaded from the gateway jwt_claims endpoint instead
266+
local_required_field = ["sub", "user_data", "exp", "version"]
230267
options = {"require": local_required_field}
231268
options.update(additional_options)
232269
return jwt.decode(
@@ -259,16 +296,23 @@ def get_role_definition(self, name: str) -> Optional[Model]:
259296
def process_rbac_permissions(self):
260297
"""
261298
This is a default process_permissions which should be usable if you are using RBAC from DAB
299+
Uses gateway claims data exclusively - no fallback to JWT token fields
262300
"""
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")
301+
if self.user is None:
302+
logger.error("Unable to process rbac permissions because user is not defined, please call authenticate first")
303+
return
304+
305+
if self.gateway_claims is None:
306+
logger.error("Unable to process rbac permissions because gateway claims are not available. Ensure gateway jwt_claims endpoint is accessible.")
265307
return
266308

267309
from ansible_base.rbac.models import RoleUserAssignment
268310

269311
role_diff = RoleUserAssignment.objects.filter(user=self.user, role_definition__name__in=settings.ANSIBLE_BASE_JWT_MANAGED_ROLES)
270312

271-
for system_role_name in self.token.get("global_roles", []):
313+
# Process global roles from gateway claims
314+
global_roles = self.gateway_claims.get("global_roles", [])
315+
for system_role_name in global_roles:
272316
logger.debug(f"Processing system role {system_role_name} for {self.user.username}")
273317
rd = self.get_role_definition(system_role_name)
274318
if rd:
@@ -282,7 +326,11 @@ def process_rbac_permissions(self):
282326
logger.error(f"Unable to grant {self.user.username} system level role {system_role_name} because it does not exist")
283327
continue
284328

285-
for object_role_name in self.token.get('object_roles', {}).keys():
329+
# Process object roles from gateway claims
330+
object_roles = self.gateway_claims.get('object_roles', {})
331+
objects = self.gateway_claims.get('objects', {})
332+
333+
for object_role_name in object_roles.keys():
286334
rd = self.get_role_definition(object_role_name)
287335
if rd is None:
288336
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it does not exist")
@@ -291,11 +339,11 @@ def process_rbac_permissions(self):
291339
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it is not a JWT managed role")
292340
continue
293341

294-
object_type = self.token['object_roles'][object_role_name]['content_type']
295-
object_indexes = self.token['object_roles'][object_role_name]['objects']
342+
object_type = object_roles[object_role_name]['content_type']
343+
object_indexes = object_roles[object_role_name]['objects']
296344

297345
for index in object_indexes:
298-
object_data = self.token['objects'][object_type][index]
346+
object_data = objects[object_type][index]
299347
try:
300348
resource, obj = self.get_or_create_resource(object_type, object_data)
301349
except IntegrityError as e:
@@ -310,7 +358,7 @@ def process_rbac_permissions(self):
310358
role_diff = role_diff.exclude(pk=assignment.pk)
311359
logger.info(f"Granted user {self.user.username} role {object_role_name} to object {obj.name} with ansible_id {object_data['ansible_id']}")
312360

313-
# Remove all permissions not authorized by the JWT
361+
# Remove all permissions not authorized by the gateway claims
314362
for role_assignment in role_diff:
315363
rd = role_assignment.role_definition
316364
content_object = role_assignment.content_object
@@ -322,9 +370,17 @@ def process_rbac_permissions(self):
322370
def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optional[Resource], Optional[Model]]:
323371
"""
324372
Gets or creates a resource from a content type and its default data
373+
Uses gateway claims exclusively - no fallback to JWT token fields
325374
326375
This can only build or get organizations or teams
376+
Args:
377+
content_type: Type of content ('team', 'organization')
378+
data: Resource data dictionary
327379
"""
380+
if self.gateway_claims is None:
381+
logger.error("Unable to create resource because gateway claims are not available")
382+
return None, None
383+
328384
object_ansible_id = data['ansible_id']
329385
try:
330386
resource = Resource.objects.get(ansible_id=object_ansible_id)
@@ -337,7 +393,7 @@ def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optiona
337393
if content_type == 'team':
338394
# For a team we first have to make sure the org is there
339395
org_id = data['org']
340-
organization_data = self.token['objects']["organization"][org_id]
396+
organization_data = self.gateway_claims['objects']["organization"][org_id]
341397

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

360416
return resource, resource.content_object
361417
else:
362-
logger.error(f"build_resource_stub does not know how to build an object of type {type}")
418+
logger.error(f"build_resource_stub does not know how to build an object of type {content_type}")
363419
return None, None
364420

365421

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)