Skip to content

Commit 287a4a7

Browse files
committed
Unified solution to serialization of ansible_id
1 parent bcd92f2 commit 287a4a7

File tree

4 files changed

+167
-30
lines changed

4 files changed

+167
-30
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: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from ..models import DABContentType, DABPermission
2222
from ..remote import RemoteObject
23+
from .fields import ActorAnsibleIdField
2324
from .queries import assignment_qs_user_to_obj, assignment_qs_user_to_obj_perm
2425

2526
logger = logging.getLogger(__name__)
@@ -116,16 +117,28 @@ def get_by_ansible_id(self, ansible_id, requesting_user, for_field):
116117
raise ValidationError({for_field: msg.format(pk_value=ansible_id)})
117118
return resource.content_object
118119

119-
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"""
120122
actor_aid_field = f'{self.actor_field}_ansible_id'
121-
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 has_actor_in_request and has_actor_aid_in_request:
122129
self.raise_id_fields_error(self.actor_field, actor_aid_field)
123-
elif validated_data.get(self.actor_field):
130+
elif not has_actor_in_request and not has_actor_aid_in_request:
131+
self.raise_id_fields_error(self.actor_field, actor_aid_field)
132+
133+
return super().validate(attrs)
134+
135+
def get_actor_from_data(self, validated_data, requesting_user):
136+
actor_aid_field = f'{self.actor_field}_ansible_id'
137+
if validated_data.get(self.actor_field):
124138
actor = validated_data[self.actor_field]
125-
elif ansible_id := validated_data.get(actor_aid_field):
126-
actor = self.get_by_ansible_id(ansible_id, requesting_user, for_field=actor_aid_field)
127139
else:
128-
self.raise_id_fields_error(self.actor_field, f'{self.actor_field}_ansible_id')
140+
# Actor is already resolved by ActorAnsibleIdField and validated
141+
actor = validated_data[self.actor_field]
129142
return actor
130143

131144
def get_object_from_data(self, validated_data, role_definition, requesting_user):
@@ -220,7 +233,8 @@ def _get_summary_fields(self, obj) -> dict[str, dict]:
220233

221234
class RoleUserAssignmentSerializer(BaseAssignmentSerializer):
222235
actor_field = 'user'
223-
user_ansible_id = serializers.UUIDField(
236+
user_ansible_id = ActorAnsibleIdField(
237+
source='user',
224238
required=False,
225239
help_text=_('The resource ID of the user who will receive permissions from this assignment. An alternative to user field.'),
226240
allow_null=True, # for ease of use of the browseable API
@@ -236,7 +250,8 @@ def get_actor_queryset(self, requesting_user):
236250

237251
class RoleTeamAssignmentSerializer(BaseAssignmentSerializer):
238252
actor_field = 'team'
239-
team_ansible_id = serializers.UUIDField(
253+
team_ansible_id = ActorAnsibleIdField(
254+
source='team',
240255
required=False,
241256
help_text=_('The resource ID of the team who will receive permissions from this assignment. An alternative to team field.'),
242257
allow_null=True,

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:
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pytest
2+
3+
from ansible_base.lib.utils.response import get_relative_url
4+
from test_app.models import Team, User
5+
6+
7+
@pytest.mark.django_db
8+
class TestRoleAssignmentAnsibleIdSerialization:
9+
"""Test serialization of ansible_id fields in role assignment endpoints"""
10+
11+
def test_role_user_assignment_serializes_user_ansible_id(self, admin_api_client, inventory, inv_rd):
12+
"""Test that RoleUserAssignment API returns user_ansible_id instead of null"""
13+
user = User.objects.create(username='test-user')
14+
assignment = inv_rd.give_permission(user, inventory)
15+
16+
# Verify user has a resource and ansible_id
17+
assert hasattr(user, 'resource')
18+
assert user.resource is not None
19+
assert user.resource.ansible_id is not None
20+
expected_ansible_id = str(user.resource.ansible_id)
21+
22+
# Get the assignment via API
23+
url = get_relative_url('roleuserassignment-detail', kwargs={'pk': assignment.pk})
24+
response = admin_api_client.get(url)
25+
assert response.status_code == 200
26+
27+
# The user_ansible_id field should be populated, not null
28+
assert response.data['user_ansible_id'] is not None
29+
assert response.data['user_ansible_id'] == expected_ansible_id
30+
31+
# Also verify the user field contains the primary key for backward compatibility
32+
assert response.data['user'] == user.pk
33+
34+
def test_role_team_assignment_serializes_team_ansible_id(self, admin_api_client, inventory, inv_rd, organization):
35+
"""Test that RoleTeamAssignment API returns team_ansible_id instead of null"""
36+
team = Team.objects.create(name='test-team', organization=organization)
37+
assignment = inv_rd.give_permission(team, inventory)
38+
39+
# Verify team has a resource and ansible_id
40+
assert hasattr(team, 'resource')
41+
assert team.resource is not None
42+
assert team.resource.ansible_id is not None
43+
expected_ansible_id = str(team.resource.ansible_id)
44+
45+
# Get the assignment via API
46+
url = get_relative_url('roleteamassignment-detail', kwargs={'pk': assignment.pk})
47+
response = admin_api_client.get(url)
48+
assert response.status_code == 200
49+
50+
# The team_ansible_id field should be populated, not null
51+
assert response.data['team_ansible_id'] is not None
52+
assert response.data['team_ansible_id'] == expected_ansible_id
53+
54+
# Also verify the team field contains the primary key for backward compatibility
55+
assert response.data['team'] == team.pk
56+
57+
def test_role_user_assignment_list_includes_ansible_id(self, admin_api_client, inventory, inv_rd):
58+
"""Test that RoleUserAssignment list endpoint includes user_ansible_id"""
59+
user = User.objects.create(username='test-user-list')
60+
assignment = inv_rd.give_permission(user, inventory)
61+
expected_ansible_id = str(user.resource.ansible_id)
62+
63+
# Get assignments list via API
64+
url = get_relative_url('roleuserassignment-list')
65+
response = admin_api_client.get(url)
66+
assert response.status_code == 200
67+
68+
# Find our assignment in the results
69+
assignment_data = None
70+
for item in response.data['results']:
71+
if item['id'] == assignment.pk:
72+
assignment_data = item
73+
break
74+
75+
assert assignment_data is not None
76+
assert assignment_data['user_ansible_id'] is not None
77+
assert assignment_data['user_ansible_id'] == expected_ansible_id
78+
79+
def test_role_team_assignment_list_includes_ansible_id(self, admin_api_client, inventory, inv_rd, organization):
80+
"""Test that RoleTeamAssignment list endpoint includes team_ansible_id"""
81+
team = Team.objects.create(name='test-team-list', organization=organization)
82+
assignment = inv_rd.give_permission(team, inventory)
83+
expected_ansible_id = str(team.resource.ansible_id)
84+
85+
# Get assignments list via API
86+
url = get_relative_url('roleteamassignment-list')
87+
response = admin_api_client.get(url)
88+
assert response.status_code == 200
89+
90+
# Find our assignment in the results
91+
assignment_data = None
92+
for item in response.data['results']:
93+
if item['id'] == assignment.pk:
94+
assignment_data = item
95+
break
96+
97+
assert assignment_data is not None
98+
assert assignment_data['team_ansible_id'] is not None
99+
assert assignment_data['team_ansible_id'] == expected_ansible_id

0 commit comments

Comments
 (0)