19
19
from ansible_base .lib .utils .auth import get_user_by_ansible_id
20
20
from ansible_base .lib .utils .translations import translatableConditionally as _
21
21
from ansible_base .resource_registry .models import Resource , ResourceType
22
+ from ansible_base .resource_registry .rest_client import get_resource_server_client
22
23
from ansible_base .resource_registry .signals .handlers import no_reverse_sync
23
24
24
25
logger = logging .getLogger ("ansible_base.jwt_consumer.common.auth" )
@@ -52,6 +53,7 @@ def __init__(self, user_fields=default_mapped_user_fields) -> None:
52
53
self .cache = JWTCache ()
53
54
self .user = None
54
55
self .token = None
56
+ self .gateway_claims = None # Store claims from gateway
55
57
56
58
@log_excess_runtime (logger , debug_cutoff = 0.01 )
57
59
def parse_jwt_token (self , request ):
@@ -142,10 +144,77 @@ def parse_jwt_token(self, request):
142
144
resource .service_id = self .token ['service_id' ]
143
145
resource .save (update_fields = ['ansible_id' , 'service_id' ])
144
146
147
+ # Check if claims need to be refreshed from gateway based on claims_hash
148
+ user_ansible_id = self .token ['sub' ]
149
+ current_claims_hash = self .token .get ('claims_hash' )
150
+
151
+ if self ._should_fetch_claims_from_gateway (user_ansible_id , current_claims_hash ):
152
+ logger .debug (f"Claims hash changed or not cached, fetching claims from gateway for user { user_ansible_id } " )
153
+ jwt_claims = self ._fetch_jwt_claims_from_gateway (user_ansible_id )
154
+
155
+ if jwt_claims :
156
+ self .gateway_claims = jwt_claims
157
+ self ._cache_claims_hash (user_ansible_id , current_claims_hash )
158
+ logger .debug (f"Successfully loaded and cached gateway claims for user { user_ansible_id } " )
159
+ else :
160
+ logger .error (f"Failed to fetch claims from gateway for user { user_ansible_id } . RBAC processing will not be available." )
161
+ # Note: We don't raise an exception here to allow basic authentication to succeed
162
+ # RBAC processing will fail gracefully with appropriate error messages
163
+ else :
164
+ logger .debug (f"Using cached claims for user { user_ansible_id } (claims_hash unchanged)" )
145
165
setattr (self .user , "resource_api_actions" , self .token .get ("resource_api_actions" , None ))
146
166
147
167
logger .info (f"User { self .user .username } authenticated from JWT auth" )
148
168
169
+ def _should_fetch_claims_from_gateway (self , user_ansible_id , current_claims_hash ):
170
+ """
171
+ Determine if claims should be fetched from gateway based on claims_hash comparison.
172
+ Returns True if claims need to be fetched (hash changed or not cached).
173
+ """
174
+ if not current_claims_hash :
175
+ logger .debug (f"No claims_hash in token for user { user_ansible_id } , will fetch claims" )
176
+ return True
177
+
178
+ cached_hash = self .cache .get_claims_hash (user_ansible_id )
179
+ if cached_hash != current_claims_hash :
180
+ logger .debug (f"Claims hash changed for user { user_ansible_id } : cached={ cached_hash } , current={ current_claims_hash } " )
181
+ return True
182
+
183
+ # Hash matches cached value, try to get cached claims
184
+ cached_claims = self .cache .get_cached_claims (user_ansible_id )
185
+ if cached_claims :
186
+ self .gateway_claims = cached_claims
187
+ return False
188
+ else :
189
+ logger .debug (f"Claims hash matches but no cached claims found for user { user_ansible_id } " )
190
+ return True
191
+
192
+ def _cache_claims_hash (self , user_ansible_id , claims_hash ):
193
+ """Cache the claims hash and gateway claims for future comparisons."""
194
+ if claims_hash and self .gateway_claims :
195
+ self .cache .set_claims_hash (user_ansible_id , claims_hash )
196
+ self .cache .set_cached_claims (user_ansible_id , self .gateway_claims )
197
+
198
+ def _fetch_jwt_claims_from_gateway (self , user_ansible_id ):
199
+ """
200
+ Fetch JWT claims for a user from the gateway service-index API.
201
+ Returns None if claims cannot be retrieved.
202
+ """
203
+ try :
204
+ client = get_resource_server_client ("service-index" )
205
+ response = client .get_jwt_claims (user_ansible_id )
206
+
207
+ if response .status_code == 200 :
208
+ claims = response .json ()
209
+ logger .debug (f"Retrieved JWT claims from gateway for user { user_ansible_id } " )
210
+ return claims
211
+ else :
212
+ logger .warning (f"Failed to retrieve JWT claims from gateway for user { user_ansible_id } : " f"{ response .status_code } " )
213
+ return None
214
+ except Exception as e :
215
+ logger .error (f"Error fetching JWT claims from gateway for user { user_ansible_id } : { e } " )
216
+ return None
217
+
149
218
def log_and_raise (self , conditional_translate_object , expand_values = {}, error_code = None ):
150
219
logger .error (conditional_translate_object .not_translated () % expand_values )
151
220
translated_error_message = conditional_translate_object .translated () % expand_values
@@ -226,7 +295,8 @@ def validate_token(self, unencrypted_token, decryption_key, request_id=None):
226
295
return validated_body
227
296
228
297
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" ]
298
+ # Core required fields - claims_hash is now required to track permission changes
299
+ local_required_field = ["sub" , "user_data" , "exp" , "version" , "claims_hash" ]
230
300
options = {"require" : local_required_field }
231
301
options .update (additional_options )
232
302
return jwt .decode (
@@ -259,16 +329,23 @@ def get_role_definition(self, name: str) -> Optional[Model]:
259
329
def process_rbac_permissions (self ):
260
330
"""
261
331
This is a default process_permissions which should be usable if you are using RBAC from DAB
332
+ Uses gateway claims data exclusively - no fallback to JWT token fields
262
333
"""
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" )
334
+ if self .user is None :
335
+ logger .error ("Unable to process rbac permissions because user is not defined, please call authenticate first" )
336
+ return
337
+
338
+ if self .gateway_claims is None :
339
+ logger .error ("Unable to process rbac permissions because gateway claims are not available. Ensure gateway jwt_claims endpoint is accessible." )
265
340
return
266
341
267
342
from ansible_base .rbac .models import RoleUserAssignment
268
343
269
344
role_diff = RoleUserAssignment .objects .filter (user = self .user , role_definition__name__in = settings .ANSIBLE_BASE_JWT_MANAGED_ROLES )
270
345
271
- for system_role_name in self .token .get ("global_roles" , []):
346
+ # Process global roles from gateway claims
347
+ global_roles = self .gateway_claims .get ("global_roles" , [])
348
+ for system_role_name in global_roles :
272
349
logger .debug (f"Processing system role { system_role_name } for { self .user .username } " )
273
350
rd = self .get_role_definition (system_role_name )
274
351
if rd :
@@ -282,7 +359,11 @@ def process_rbac_permissions(self):
282
359
logger .error (f"Unable to grant { self .user .username } system level role { system_role_name } because it does not exist" )
283
360
continue
284
361
285
- for object_role_name in self .token .get ('object_roles' , {}).keys ():
362
+ # Process object roles from gateway claims
363
+ object_roles = self .gateway_claims .get ('object_roles' , {})
364
+ objects = self .gateway_claims .get ('objects' , {})
365
+
366
+ for object_role_name in object_roles .keys ():
286
367
rd = self .get_role_definition (object_role_name )
287
368
if rd is None :
288
369
logger .error (f"Unable to grant { self .user .username } object role { object_role_name } because it does not exist" )
@@ -291,11 +372,11 @@ def process_rbac_permissions(self):
291
372
logger .error (f"Unable to grant { self .user .username } object role { object_role_name } because it is not a JWT managed role" )
292
373
continue
293
374
294
- object_type = self . token [ ' object_roles' ] [object_role_name ]['content_type' ]
295
- object_indexes = self . token [ ' object_roles' ] [object_role_name ]['objects' ]
375
+ object_type = object_roles [object_role_name ]['content_type' ]
376
+ object_indexes = object_roles [object_role_name ]['objects' ]
296
377
297
378
for index in object_indexes :
298
- object_data = self . token [ ' objects' ] [object_type ][index ]
379
+ object_data = objects [object_type ][index ]
299
380
try :
300
381
resource , obj = self .get_or_create_resource (object_type , object_data )
301
382
except IntegrityError as e :
@@ -310,7 +391,7 @@ def process_rbac_permissions(self):
310
391
role_diff = role_diff .exclude (pk = assignment .pk )
311
392
logger .info (f"Granted user { self .user .username } role { object_role_name } to object { obj .name } with ansible_id { object_data ['ansible_id' ]} " )
312
393
313
- # Remove all permissions not authorized by the JWT
394
+ # Remove all permissions not authorized by the gateway claims
314
395
for role_assignment in role_diff :
315
396
rd = role_assignment .role_definition
316
397
content_object = role_assignment .content_object
@@ -322,9 +403,17 @@ def process_rbac_permissions(self):
322
403
def get_or_create_resource (self , content_type : str , data : dict ) -> Tuple [Optional [Resource ], Optional [Model ]]:
323
404
"""
324
405
Gets or creates a resource from a content type and its default data
406
+ Uses gateway claims exclusively - no fallback to JWT token fields
325
407
326
408
This can only build or get organizations or teams
409
+ Args:
410
+ content_type: Type of content ('team', 'organization')
411
+ data: Resource data dictionary
327
412
"""
413
+ if self .gateway_claims is None :
414
+ logger .error ("Unable to create resource because gateway claims are not available" )
415
+ return None , None
416
+
328
417
object_ansible_id = data ['ansible_id' ]
329
418
try :
330
419
resource = Resource .objects .get (ansible_id = object_ansible_id )
@@ -337,7 +426,7 @@ def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optiona
337
426
if content_type == 'team' :
338
427
# For a team we first have to make sure the org is there
339
428
org_id = data ['org' ]
340
- organization_data = self .token ['objects' ]["organization" ][org_id ]
429
+ organization_data = self .gateway_claims ['objects' ]["organization" ][org_id ]
341
430
342
431
# Now that we have the org we can build a team
343
432
org_resource , _ = self .get_or_create_resource ("organization" , organization_data )
@@ -359,7 +448,7 @@ def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optiona
359
448
360
449
return resource , resource .content_object
361
450
else :
362
- logger .error (f"build_resource_stub does not know how to build an object of type { type } " )
451
+ logger .error (f"build_resource_stub does not know how to build an object of type { content_type } " )
363
452
return None , None
364
453
365
454
0 commit comments