Skip to content

Commit 44e4ae8

Browse files
AAP-50626 AAP-50627 Use ansible_id references in access lists (#783)
This is needed to complete the UI connection of the access lists. Background: Due to technical issues, we have asked the UI to use the component-based access list for resources owned by a certain component. So if there's an AWX resource, get the access list served from AWX for that item, like an inventory or job template. However, our endpoints were still using the local team or user id. This is a problem, because all other RBAC management is centralized around the resource server. So a translation is needed, and right now there's no easy way to do that translation. So the solution (this) is to formulate all the access list related endpoints around the ansible_id of the actor, that being the user or the team. Corresponding to this, we need a fix for the bug #534, so that the client has the ansible_id as a starting point when navigating to the component endpoints --------- Co-authored-by: John Westcott IV <[email protected]>
1 parent a0f4038 commit 44e4ae8

File tree

7 files changed

+404
-39
lines changed

7 files changed

+404
-39
lines changed

ansible_base/rbac/api/fields.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django.apps import apps
2+
from django.core.exceptions import ObjectDoesNotExist
3+
from rest_framework import serializers
4+
5+
6+
class ActorAnsibleIdField(serializers.UUIDField):
7+
"""
8+
UUID field that serializes actor objects to their ansible_id and accepts ansible_id for deserialization.
9+
10+
Always resolves ansible_id input to the corresponding actor object.
11+
Uses the source parameter to determine which field to populate.
12+
"""
13+
14+
def to_representation(self, actor):
15+
"""Convert actor object to its ansible_id UUID"""
16+
if actor is None:
17+
return None
18+
try:
19+
if hasattr(actor, 'resource') and actor.resource:
20+
return super().to_representation(actor.resource.ansible_id)
21+
except ObjectDoesNotExist:
22+
# Resource doesn't exist, return None
23+
pass
24+
return None
25+
26+
def to_internal_value(self, data):
27+
"""Convert ansible_id UUID to actor object"""
28+
if data is None:
29+
return None
30+
31+
# Let UUIDField handle validation and conversion
32+
uuid_value = super().to_internal_value(data)
33+
34+
# Always resolve to actor object
35+
resource_cls = apps.get_model('dab_resource_registry', 'Resource')
36+
try:
37+
resource = resource_cls.objects.get(ansible_id=uuid_value)
38+
except resource_cls.DoesNotExist:
39+
source_name = getattr(self, 'source', 'actor')
40+
raise serializers.ValidationError(f"No {source_name} found with ansible_id={uuid_value}")
41+
return resource.content_object

ansible_base/rbac/api/serializers.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from django.apps import apps
24
from django.core.exceptions import ObjectDoesNotExist
35
from django.db import transaction
@@ -18,8 +20,11 @@
1820

1921
from ..models import DABContentType, DABPermission
2022
from ..remote import RemoteObject
23+
from .fields import ActorAnsibleIdField
2124
from .queries import assignment_qs_user_to_obj, assignment_qs_user_to_obj_perm
2225

26+
logger = logging.getLogger(__name__)
27+
2328

2429
class RoleDefinitionSerializer(CommonModelSerializer):
2530
permissions = serializers.SlugRelatedField(
@@ -112,17 +117,19 @@ def get_by_ansible_id(self, ansible_id, requesting_user, for_field):
112117
raise ValidationError({for_field: msg.format(pk_value=ansible_id)})
113118
return resource.content_object
114119

115-
def get_actor_from_data(self, validated_data, requesting_user):
120+
def validate(self, attrs):
121+
"""Validate that exactly one of actor or actor_ansible_id is provided"""
116122
actor_aid_field = f'{self.actor_field}_ansible_id'
117-
if validated_data.get(self.actor_field) and validated_data.get(actor_aid_field):
123+
124+
# Check what was actually provided in the request
125+
has_actor_in_request = self.actor_field in self.initial_data
126+
has_actor_aid_in_request = actor_aid_field in self.initial_data
127+
128+
# If both actor and actor_ansible_id are present or both not present than we error out
129+
if has_actor_in_request == has_actor_aid_in_request:
118130
self.raise_id_fields_error(self.actor_field, actor_aid_field)
119-
elif validated_data.get(self.actor_field):
120-
actor = validated_data[self.actor_field]
121-
elif ansible_id := validated_data.get(actor_aid_field):
122-
actor = self.get_by_ansible_id(ansible_id, requesting_user, for_field=actor_aid_field)
123-
else:
124-
self.raise_id_fields_error(self.actor_field, f'{self.actor_field}_ansible_id')
125-
return actor
131+
132+
return super().validate(attrs)
126133

127134
def get_object_from_data(self, validated_data, role_definition, requesting_user):
128135
obj = None
@@ -145,10 +152,11 @@ def get_object_from_data(self, validated_data, role_definition, requesting_user)
145152
elif validated_data.get('object_ansible_id'):
146153
obj = self.get_by_ansible_id(validated_data.get('object_ansible_id'), requesting_user, for_field='object_ansible_id')
147154
if permission_registry.content_type_model.objects.get_for_model(obj) != role_definition.content_type:
155+
model_name = getattr(role_definition.content_type, 'model', 'global')
148156
raise ValidationError(
149157
{
150158
'object_ansible_id': _('Object type of %(model_name)s does not match role type of %(role_definition)s')
151-
% {'model_name': obj._meta.model_name, 'role_definition': role_definition.content_type.model}
159+
% {'model_name': obj._meta.model_name, 'role_definition': model_name}
152160
}
153161
)
154162
return obj
@@ -158,7 +166,7 @@ def create(self, validated_data):
158166
requesting_user = self.context['view'].request.user
159167

160168
# Resolve actor - team or user
161-
actor = self.get_actor_from_data(validated_data, requesting_user)
169+
actor = validated_data[self.actor_field]
162170

163171
# Resolve object
164172
obj = self.get_object_from_data(validated_data, rd, requesting_user)
@@ -216,7 +224,8 @@ def _get_summary_fields(self, obj) -> dict[str, dict]:
216224

217225
class RoleUserAssignmentSerializer(BaseAssignmentSerializer):
218226
actor_field = 'user'
219-
user_ansible_id = serializers.UUIDField(
227+
user_ansible_id = ActorAnsibleIdField(
228+
source='user',
220229
required=False,
221230
help_text=_('The resource ID of the user who will receive permissions from this assignment. An alternative to user field.'),
222231
allow_null=True, # for ease of use of the browseable API
@@ -232,7 +241,8 @@ def get_actor_queryset(self, requesting_user):
232241

233242
class RoleTeamAssignmentSerializer(BaseAssignmentSerializer):
234243
actor_field = 'team'
235-
team_ansible_id = serializers.UUIDField(
244+
team_ansible_id = ActorAnsibleIdField(
245+
source='team',
236246
required=False,
237247
help_text=_('The resource ID of the team who will receive permissions from this assignment. An alternative to team field.'),
238248
allow_null=True,
@@ -257,9 +267,21 @@ def _get_related(self, obj) -> dict[str, str]:
257267
return {}
258268
related_fields = {}
259269
actor_cls = self.Meta.model
270+
271+
# Use ansible_id if available, otherwise fall back to pk
272+
actor_identifier = obj.pk
273+
try:
274+
if hasattr(obj, 'resource') and obj.resource:
275+
actor_identifier = str(obj.resource.ansible_id)
276+
except ObjectDoesNotExist:
277+
# Resource doesn't exist, stick with pk
278+
logger.warning(
279+
f"No resource for {self.Meta.model} {obj.pk} due to internal error. Linking role-{actor_cls._meta.model_name}-access-assignments as pk."
280+
)
281+
260282
related_fields['details'] = get_relative_url(
261283
f'role-{actor_cls._meta.model_name}-access-assignments',
262-
kwargs={'model_name': self.context.get("content_type").api_slug, 'pk': self.context.get("related_object").pk, 'actor_pk': obj.pk},
284+
kwargs={'model_name': self.context.get("content_type").api_slug, 'pk': self.context.get("related_object").pk, 'actor_pk': actor_identifier},
263285
)
264286
return related_fields
265287

ansible_base/rbac/api/views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import uuid
12
from collections import OrderedDict
23
from typing import Type
34

5+
from django.apps import apps
6+
from django.core.exceptions import ObjectDoesNotExist
47
from django.db import transaction
58
from django.db.models import Model
69
from django.utils.translation import gettext_lazy as _
@@ -379,6 +382,28 @@ class UserAccessAssignmentViewSet(
379382
def get_url_actor(self):
380383
actor_pk = self.kwargs.get("actor_pk")
381384
actor_cls = self.get_actor_model()
385+
386+
# First, try to parse as UUID for ansible_id lookup
387+
try:
388+
parsed_uuid = uuid.UUID(actor_pk)
389+
# It's a valid UUID, try resource lookup first
390+
try:
391+
resource_cls = apps.get_model('dab_resource_registry', 'Resource')
392+
resource = resource_cls.objects.get(ansible_id=parsed_uuid)
393+
actor = resource.content_object
394+
# Verify the content object is the correct type
395+
if isinstance(actor, actor_cls):
396+
return actor
397+
else:
398+
raise NotFound(f'Resource with ansible_id {parsed_uuid} is not a {actor_cls._meta.model_name}')
399+
except (LookupError, ObjectDoesNotExist):
400+
# Resource registry not available or resource not found with this UUID
401+
raise NotFound(f'The {actor_cls._meta.model_name} with ansible_id={actor_pk} can not be found')
402+
except ValueError:
403+
# Not a valid UUID, continue with primary key lookup
404+
pass
405+
406+
# Fallback to primary key lookup (only for non-UUID values)
382407
try:
383408
return actor_cls.objects.get(pk=actor_pk)
384409
except actor_cls.DoesNotExist:

ansible_base/rbac/service_api/serializers.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.utils.translation import gettext_lazy as _
55
from rest_framework import serializers
66

7+
from ..api.fields import ActorAnsibleIdField
78
from ..models import DABContentType, DABPermission, RoleDefinition, RoleTeamAssignment, RoleUserAssignment
89
from ..remote import RemoteObject
910

@@ -24,25 +25,6 @@ class Meta:
2425
fields = ['api_slug', 'codename', 'content_type', 'name']
2526

2627

27-
class ActorAnsibleIDField(serializers.Field):
28-
def to_representation(self, actor):
29-
if actor is None:
30-
return None
31-
resource = actor.resource
32-
if resource is None:
33-
return None
34-
return str(resource.ansible_id)
35-
36-
def to_internal_value(self, data):
37-
resource_cls = apps.get_model('dab_resource_registry', 'Resource')
38-
try:
39-
resource = resource_cls.objects.get(ansible_id=data)
40-
except resource_cls.DoesNotExist:
41-
raise serializers.ValidationError(f"No {self.source} found with ansible_id={data}")
42-
43-
return resource.content_object
44-
45-
4628
class ObjectIDAnsibleIDField(serializers.Field):
4729
"This is an ansible_id field intended to be used with source pointing to object_id, so, does conversion"
4830

@@ -71,7 +53,7 @@ def to_internal_value(self, value):
7153
class BaseAssignmentSerializer(serializers.ModelSerializer):
7254
content_type = serializers.SlugRelatedField(read_only=True, slug_field='api_slug')
7355
role_definition = serializers.SlugRelatedField(slug_field='name', queryset=RoleDefinition.objects.all())
74-
created_by_ansible_id = ActorAnsibleIDField(source='created_by', required=False)
56+
created_by_ansible_id = ActorAnsibleIdField(source='created_by', required=False)
7557
object_ansible_id = ObjectIDAnsibleIDField(source='object_id', required=False, allow_null=True)
7658
object_id = serializers.CharField(allow_blank=True, required=False, allow_null=True)
7759
from_service = serializers.CharField(write_only=True)
@@ -158,7 +140,7 @@ def create(self, validated_data):
158140

159141

160142
class ServiceRoleUserAssignmentSerializer(BaseAssignmentSerializer):
161-
user_ansible_id = ActorAnsibleIDField(source='user', required=True)
143+
user_ansible_id = ActorAnsibleIdField(source='user', required=True)
162144
actor_field = 'user'
163145

164146
class Meta:
@@ -167,7 +149,7 @@ class Meta:
167149

168150

169151
class ServiceRoleTeamAssignmentSerializer(BaseAssignmentSerializer):
170-
team_ansible_id = ActorAnsibleIDField(source='team', required=True)
152+
team_ansible_id = ActorAnsibleIdField(source='team', required=True)
171153
actor_field = 'team'
172154

173155
class Meta:

test_app/management/commands/create_demo_data.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from ansible_base.authentication.models import Authenticator, AuthenticatorUser
99
from ansible_base.oauth2_provider.models import OAuth2Application
10+
from ansible_base.rbac import permission_registry
1011
from ansible_base.rbac.models import DABContentType, RoleDefinition
1112
from test_app.models import EncryptionModel, InstanceGroup, Inventory, Organization, Team, User
1213

@@ -41,6 +42,7 @@ def handle(self, *args, **kwargs):
4142
(galaxy, _) = Organization.objects.get_or_create(name='Galaxy_community')
4243

4344
(spud, _) = User.objects.get_or_create(username='angry_spud')
45+
(team_member, _) = User.objects.get_or_create(username='team_member')
4446
(bull_bot, _) = User.objects.get_or_create(username='ansibullbot')
4547
(admin, _) = User.objects.get_or_create(username='admin')
4648
spud.set_password('password')
@@ -72,8 +74,8 @@ def handle(self, *args, **kwargs):
7274

7375
# Inventory objects exist inside of an organization
7476
Inventory.objects.create(name='K8S clusters', organization=operator_stuff)
75-
Inventory.objects.create(name='Galaxy Host', organization=galaxy)
76-
Inventory.objects.create(name='AWX deployment', organization=awx)
77+
galaxy_inv = Inventory.objects.create(name='Galaxy Host', organization=galaxy)
78+
awx_inv = Inventory.objects.create(name='AWX deployment', organization=awx)
7779
# Objects that have no associated organization
7880
InstanceGroup.objects.create(name='Default')
7981
isolated_group = InstanceGroup.objects.create(name='Isolated Network')
@@ -95,7 +97,18 @@ def handle(self, *args, **kwargs):
9597
user.set_password('password')
9698
user.save()
9799

98-
RoleDefinition.objects.managed.team_member.give_permission(spud, awx_devs)
100+
# Give some users team member and give that team some inventory object permissions
101+
for user in (spud, team_member):
102+
RoleDefinition.objects.managed.team_member.give_permission(spud, awx_devs)
103+
104+
with impersonate(bull_bot):
105+
inv_admin, _ = RoleDefinition.objects.get_or_create(
106+
name='Inventory Admin',
107+
permissions=['change_inventory', 'view_inventory'],
108+
defaults={'content_type': permission_registry.content_type_model.objects.get_for_model(Inventory)},
109+
)
110+
for inv in (awx_inv, galaxy_inv):
111+
inv_admin.give_permission(awx_devs, inv)
99112

100113
OAuth2Application.objects.get_or_create(
101114
name="Demo OAuth2 Application",

0 commit comments

Comments
 (0)