Skip to content

Commit 7af751a

Browse files
authored
AAP-51654 Enforce that references to objects are strictly done in claims data, not token data (#809)
The prior solution partially migrate us from passing claims data inside of the JWT data to a new user "claims" data structure. However, it was still referencing "objects" inside of the JWT data itself. This refactors the methods out of the auth class, which makes this kind of bug likely. Stuff is just passed as method arguments in this new approach. This is what I originally expected out of this work.
1 parent 4c37812 commit 7af751a

File tree

6 files changed

+235
-430
lines changed

6 files changed

+235
-430
lines changed

ansible_base/jwt_consumer/common/auth.py

Lines changed: 3 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import logging
22
import uuid
33
from datetime import datetime
4-
from typing import Optional, Tuple
4+
from typing import Optional
55

66
import jwt
7-
from django.apps import apps
8-
from django.conf import settings
97
from django.contrib.auth import get_user_model
108
from django.core.exceptions import ObjectDoesNotExist
11-
from django.db.models import Model
129
from django.db.utils import IntegrityError
1310
from rest_framework.authentication import BaseAuthentication
1411
from rest_framework.exceptions import AuthenticationFailed
@@ -19,7 +16,7 @@
1916
from ansible_base.lib.logging.runtime import log_excess_runtime
2017
from ansible_base.lib.utils.auth import get_user_by_ansible_id
2118
from ansible_base.lib.utils.translations import translatableConditionally as _
22-
from ansible_base.rbac.claims import get_claims_hash, get_user_claims, get_user_claims_hashable_form
19+
from ansible_base.rbac.claims import get_claims_hash, get_user_claims, get_user_claims_hashable_form, save_user_claims
2320
from ansible_base.resource_registry.models import Resource, ResourceType
2421
from ansible_base.resource_registry.rest_client import get_resource_server_client
2522
from ansible_base.resource_registry.signals.handlers import no_reverse_sync
@@ -36,18 +33,6 @@
3633
"is_superuser",
3734
]
3835

39-
_permission_registry = None
40-
41-
42-
def permission_registry():
43-
global _permission_registry
44-
45-
if not _permission_registry:
46-
from ansible_base.rbac.permission_registry import permission_registry as permission_registry_singleton
47-
48-
_permission_registry = permission_registry_singleton
49-
return _permission_registry
50-
5136

5237
class JWTCommonAuth:
5338
def __init__(self, user_fields=default_mapped_user_fields) -> None:
@@ -241,24 +226,6 @@ def decode_jwt_token(self, unencrypted_token, decryption_key, additional_options
241226
algorithms=["RS256"],
242227
)
243228

244-
def get_role_definition(self, name: str) -> Optional[Model]:
245-
"""Simply get the RoleDefinition from the database if it exists and handler corner cases
246-
247-
If this is the name of a managed role for which we have a corresponding definition in code,
248-
and that role can not be found in the database, it may be created here
249-
"""
250-
from ansible_base.rbac.models import RoleDefinition
251-
252-
try:
253-
return RoleDefinition.objects.get(name=name)
254-
except RoleDefinition.DoesNotExist:
255-
256-
constructor = permission_registry().get_managed_role_constructor_by_name(name)
257-
if constructor:
258-
rd, _ = constructor.get_or_create(apps)
259-
return rd
260-
return None
261-
262229
def process_rbac_permissions(self):
263230
"""
264231
Process RBAC permissions using claims hash logic
@@ -313,7 +280,7 @@ def process_rbac_permissions(self):
313280
global_roles = gateway_claims.get('global_roles', [])
314281

315282
# Process the RBAC permissions with the gateway claims
316-
self._apply_rbac_permissions(objects, object_roles, global_roles)
283+
save_user_claims(self.user, objects, object_roles, global_roles)
317284

318285
# Update cache with the new hash
319286
self.cache.cache_claims_hash(user_ansible_id, jwt_claims_hash)
@@ -344,108 +311,6 @@ def _fetch_jwt_claims_from_gateway(self, user_ansible_id: str) -> Optional[dict]
344311
logger.error(f"Error fetching claims from gateway: {e}")
345312
return None
346313

347-
def _apply_rbac_permissions(self, objects, object_roles, global_roles):
348-
"""
349-
Apply RBAC permissions from claims data
350-
"""
351-
from ansible_base.rbac.models import RoleUserAssignment
352-
353-
role_diff = RoleUserAssignment.objects.filter(user=self.user, role_definition__name__in=settings.ANSIBLE_BASE_JWT_MANAGED_ROLES)
354-
355-
for system_role_name in global_roles:
356-
logger.debug(f"Processing system role {system_role_name} for {self.user.username}")
357-
rd = self.get_role_definition(system_role_name)
358-
if rd:
359-
if rd.name in settings.ANSIBLE_BASE_JWT_MANAGED_ROLES:
360-
assignment = rd.give_global_permission(self.user)
361-
role_diff = role_diff.exclude(pk=assignment.pk)
362-
logger.info(f"Granted user {self.user.username} global role {system_role_name}")
363-
else:
364-
logger.error(f"Unable to grant {self.user.username} system level role {system_role_name} because it is not a JWT managed role")
365-
else:
366-
logger.error(f"Unable to grant {self.user.username} system level role {system_role_name} because it does not exist")
367-
continue
368-
369-
for object_role_name in object_roles.keys():
370-
rd = self.get_role_definition(object_role_name)
371-
if rd is None:
372-
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it does not exist")
373-
continue
374-
elif rd.name not in settings.ANSIBLE_BASE_JWT_MANAGED_ROLES:
375-
logger.error(f"Unable to grant {self.user.username} object role {object_role_name} because it is not a JWT managed role")
376-
continue
377-
378-
object_type = object_roles[object_role_name]['content_type']
379-
object_indexes = object_roles[object_role_name]['objects']
380-
381-
for index in object_indexes:
382-
object_data = objects[object_type][index]
383-
try:
384-
resource, obj = self.get_or_create_resource(object_type, object_data)
385-
except IntegrityError as e:
386-
logger.warning(
387-
f"Got integrity error ({e}) on {object_data}. Skipping {object_type} assignment. "
388-
"Please make sure the sync task is running to prevent this warning in the future."
389-
)
390-
continue
391-
392-
if resource is not None:
393-
assignment = rd.give_permission(self.user, obj)
394-
role_diff = role_diff.exclude(pk=assignment.pk)
395-
logger.info(f"Granted user {self.user.username} role {object_role_name} to object {obj.name} with ansible_id {object_data['ansible_id']}")
396-
397-
# Remove all permissions not authorized by the JWT
398-
for role_assignment in role_diff:
399-
rd = role_assignment.role_definition
400-
content_object = role_assignment.content_object
401-
if content_object:
402-
rd.remove_permission(self.user, content_object)
403-
else:
404-
rd.remove_global_permission(self.user)
405-
406-
def get_or_create_resource(self, content_type: str, data: dict) -> Tuple[Optional[Resource], Optional[Model]]:
407-
"""
408-
Gets or creates a resource from a content type and its default data
409-
410-
This can only build or get organizations or teams
411-
"""
412-
object_ansible_id = data['ansible_id']
413-
try:
414-
resource = Resource.objects.get(ansible_id=object_ansible_id)
415-
logger.debug(f"Resource {object_ansible_id} already exists")
416-
return resource, resource.content_object
417-
except Resource.DoesNotExist:
418-
pass
419-
420-
# The resource was missing so we need to create its stub
421-
if content_type == 'team':
422-
# For a team we first have to make sure the org is there
423-
org_id = data['org']
424-
organization_data = self.token['objects']["organization"][org_id]
425-
426-
# Now that we have the org we can build a team
427-
org_resource, _ = self.get_or_create_resource("organization", organization_data)
428-
429-
resource = Resource.create_resource(
430-
ResourceType.objects.get(name="shared.team"),
431-
{"name": data["name"], "organization": org_resource.ansible_id},
432-
ansible_id=data["ansible_id"],
433-
)
434-
435-
return resource, resource.content_object
436-
437-
elif content_type == 'organization':
438-
resource = Resource.create_resource(
439-
ResourceType.objects.get(name="shared.organization"),
440-
{"name": data["name"]},
441-
ansible_id=data["ansible_id"],
442-
)
443-
444-
return resource, resource.content_object
445-
else:
446-
logger.error(f"build_resource_stub does not know how to build an object of type {type}")
447-
return None, None
448-
449314

450315
class JWTAuthentication(BaseAuthentication):
451316
map_fields = default_mapped_user_fields
Lines changed: 1 addition & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,9 @@
11
import logging
22

3-
from django.contrib.contenttypes.models import ContentType
4-
from django.db.models.query import IntegrityError
5-
63
from ansible_base.jwt_consumer.common.auth import JWTAuthentication
7-
from ansible_base.jwt_consumer.common.exceptions import InvalidService
8-
from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment
9-
from ansible_base.resource_registry.models import Resource
104

115
logger = logging.getLogger('ansible_base.jwt_consumer.hub.auth')
126

137

148
class HubJWTAuth(JWTAuthentication):
15-
16-
def get_galaxy_models(self):
17-
'''This is separate from process_permissions purely for testability.'''
18-
try:
19-
from galaxy_ng.app.models import Organization, Team
20-
except ImportError:
21-
raise InvalidService("automation-hub")
22-
23-
return Organization, Team
24-
25-
def _apply_rbac_permissions(self, objects, object_roles, global_roles):
26-
# Map teams in the JWT to Automation Hub groups.
27-
Organization, Team = self.get_galaxy_models()
28-
self.team_content_type = ContentType.objects.get_for_model(Team)
29-
self.org_content_type = ContentType.objects.get_for_model(Organization)
30-
31-
# TODO - galaxy does not have an org admin roledef yet
32-
# admin_orgs = []
33-
34-
# TODO - galaxy does not have an org member roledef yet
35-
# member_orgs = []
36-
37-
# The "shared" [!local] teams this user admins
38-
admin_teams = []
39-
40-
# the teams this user should have a "shared" [!local] assignment to
41-
member_teams = []
42-
43-
for role_name in object_roles.keys():
44-
if role_name.startswith('Team'):
45-
for object_index in object_roles[role_name]['objects']:
46-
team_data = objects['team'][object_index]
47-
ansible_id = team_data['ansible_id']
48-
try:
49-
team = Resource.objects.get(ansible_id=ansible_id).content_object
50-
except Resource.DoesNotExist:
51-
try:
52-
team = self.common_auth.get_or_create_resource('team', team_data)[1]
53-
except IntegrityError as e:
54-
logger.warning(
55-
f"Got integrity error ({e}) on {team_data}. Skipping team assignment. "
56-
"Please make sure the sync task is running to prevent this warning in the future."
57-
)
58-
continue
59-
60-
if role_name == 'Team Admin':
61-
admin_teams.append(team)
62-
elif role_name == 'Team Member':
63-
member_teams.append(team)
64-
65-
for roledef_name, teams in [('Team Admin', admin_teams), ('Team Member', member_teams)]:
66-
67-
# the "shared" "non-local" definition ...
68-
try:
69-
roledef = RoleDefinition.objects.get(name=roledef_name)
70-
except RoleDefinition.DoesNotExist:
71-
raise RoleDefinition.DoesNotExist(f'Expected JWT role {roledef_name} does not exist locally')
72-
73-
# pks for filtering ...
74-
team_pks = [team.pk for team in teams]
75-
76-
# delete all assignments not defined by this jwt ...
77-
for assignment in RoleUserAssignment.objects.filter(user=self.common_auth.user, role_definition=roledef).exclude(object_id__in=team_pks):
78-
team = Team.objects.get(pk=assignment.object_id)
79-
roledef.remove_permission(self.common_auth.user, team)
80-
81-
# assign "non-local" for each team ...
82-
for team in teams:
83-
roledef.give_permission(self.common_auth.user, team)
84-
85-
auditor_roledef = RoleDefinition.objects.get(name='Platform Auditor')
86-
if "Platform Auditor" in global_roles:
87-
auditor_roledef.give_global_permission(self.common_auth.user)
88-
else:
89-
auditor_roledef.remove_global_permission(self.common_auth.user)
9+
use_rbac_permissions = True

0 commit comments

Comments
 (0)