Skip to content

Commit 6d162c1

Browse files
authored
AAP-58622 Assure that ansible_id is prefered for content_object in all cases (#882)
We had some bugs due to confusion of `object_id` and `object_ansible_id` in internal syncing. The only reason to use object_id is if the ansible_id doesn't exist, and this hopes to make that more consistent and fix the bugs we have seen.
1 parent 9db7237 commit 6d162c1

File tree

3 files changed

+121
-2
lines changed

3 files changed

+121
-2
lines changed

ansible_base/rbac/service_api/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ def validate(self, attrs):
9191
if not self.partial and not has_object_id and not has_object_ansible_id:
9292
raise serializers.ValidationError("You must provide either 'object_id' or 'object_ansible_id'.")
9393
# If object_ansible_id was provided and converted, use that for object_id
94-
if has_object_ansible_id and not has_object_id:
94+
# Prioritize object_ansible_id when both are provided (defensive behavior)
95+
if has_object_ansible_id:
9596
attrs['object_id'] = attrs['object_ansible_id']
9697
else:
9798
if has_object_id or has_object_ansible_id:

ansible_base/resource_registry/rest_client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,15 @@ def sync_assignment(self, assignment):
202202
else:
203203
serializer = ServiceRoleTeamAssignmentSerializer(assignment)
204204

205-
return self._sync_assignment(serializer.data)
205+
data = serializer.data
206+
207+
# Remove object_id if object_ansible_id is present to avoid sending both
208+
# For registered objects: send only object_ansible_id
209+
# For non-registered objects: send only object_id
210+
if data.get('object_ansible_id') is not None:
211+
data.pop('object_id', None)
212+
213+
return self._sync_assignment(data)
206214

207215
def sync_unassignment(self, role_definition, actor, content_object):
208216
_check_rbac_installed()

test_app/tests/rbac/remote/test_service_api.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,116 @@ def test_serializer_allows_null_values_in_validation(self, admin_api_client, ran
493493
assert 'created_by' not in validated_data or validated_data.get('created_by') is None
494494

495495

496+
@pytest.mark.django_db
497+
class TestRestClientSyncAssignment:
498+
"""
499+
Test that rest_client.sync_assignment only sends the appropriate ID field.
500+
501+
For objects registered with resource registry: send only object_ansible_id
502+
For objects NOT registered: send only object_id
503+
504+
Generated by Claude Sonnet 4.5
505+
"""
506+
507+
def test_sync_assignment_sends_only_object_ansible_id_for_registered_objects(self, rando, organization, org_admin_rd):
508+
"""Test that sync_assignment removes object_id when object_ansible_id is present"""
509+
from unittest.mock import MagicMock, patch
510+
511+
from ansible_base.resource_registry.rest_client import ResourceAPIClient
512+
513+
# Create an assignment to an organization (which has a resource)
514+
assignment = org_admin_rd.give_permission(rando, organization)
515+
516+
# Create a mock client
517+
client = ResourceAPIClient(service_url='http://example.com', service_path='/api/v1/service-index/')
518+
519+
# Mock the _sync_assignment method to capture what data is sent
520+
with patch.object(client, '_sync_assignment', return_value=MagicMock()) as mock_sync:
521+
# Call sync_assignment
522+
client.sync_assignment(assignment)
523+
524+
# Verify _sync_assignment was called
525+
assert mock_sync.called
526+
sent_data = mock_sync.call_args[0][0]
527+
528+
# Should have object_ansible_id
529+
assert 'object_ansible_id' in sent_data
530+
assert sent_data['object_ansible_id'] == str(organization.resource.ansible_id)
531+
532+
# Should NOT have object_id (removed by sync_assignment)
533+
assert 'object_id' not in sent_data, "object_id should not be sent for registered objects"
534+
535+
def test_sync_assignment_sends_only_object_id_for_non_registered_objects(self, rando, inventory, inv_rd):
536+
"""Test that sync_assignment keeps object_id when object_ansible_id is None"""
537+
from unittest.mock import MagicMock, patch
538+
539+
from ansible_base.resource_registry.rest_client import ResourceAPIClient
540+
541+
# Create an assignment to an inventory (which doesn't have a resource)
542+
assignment = inv_rd.give_permission(rando, inventory)
543+
544+
# Create a mock client
545+
client = ResourceAPIClient(service_url='http://example.com', service_path='/api/v1/service-index/')
546+
547+
# Mock the _sync_assignment method to capture what data is sent
548+
with patch.object(client, '_sync_assignment', return_value=MagicMock()) as mock_sync:
549+
# Call sync_assignment
550+
client.sync_assignment(assignment)
551+
552+
# Verify _sync_assignment was called
553+
assert mock_sync.called
554+
sent_data = mock_sync.call_args[0][0]
555+
556+
# Should have object_id
557+
assert 'object_id' in sent_data
558+
assert sent_data['object_id'] == str(inventory.id)
559+
560+
# object_ansible_id should either be absent or None
561+
assert sent_data.get('object_ansible_id') is None
562+
563+
564+
@pytest.mark.django_db
565+
class TestObjectIdVsAnsibleId:
566+
"""
567+
Test server-side defensive behavior: object_ansible_id takes precedence when both are provided.
568+
569+
This is a defensive measure for the edge case where a client incorrectly sends both fields
570+
that point to different objects. In this case, object_ansible_id should win.
571+
572+
Generated by Claude Sonnet 4.5
573+
"""
574+
575+
def test_object_ansible_id_takes_precedence_when_both_differ(self, admin_api_client, rando, org_admin_rd, organization):
576+
"""
577+
Test DESIRED defensive server-side behavior: object_ansible_id wins when both are provided but differ.
578+
579+
This should not happen in normal operation (the client should only send one), but if it does,
580+
object_ansible_id should take precedence since it's the canonical identifier for cross-service sync.
581+
"""
582+
from test_app.models import Organization
583+
584+
# Create a second organization
585+
org2 = Organization.objects.create(name='Other Organization')
586+
587+
url = get_relative_url('serviceuserassignment-assign')
588+
589+
# Provide both with valid but different values (simulating a buggy client)
590+
# object_id points to org2, object_ansible_id points to organization
591+
data = {
592+
"role_definition": org_admin_rd.name,
593+
"user_ansible_id": str(rando.resource.ansible_id),
594+
"object_id": str(org2.id), # Wrong object
595+
"object_ansible_id": str(organization.resource.ansible_id), # Should take precedence
596+
}
597+
598+
response = admin_api_client.post(url, data=data)
599+
assert response.status_code == 201, f"Expected 201 but got {response.status_code}: {response.data}"
600+
601+
# Verify the assignment was made to organization (from object_ansible_id), not org2
602+
assert rando.has_obj_perm(organization, 'change'), "object_ansible_id should take precedence"
603+
assert not rando.has_obj_perm(org2, 'change'), "object_id should be ignored when both provided"
604+
605+
496606
@pytest.mark.django_db
497607
class TestValidationErrors:
498608
"""Test validation error cases in service API serializers"""

0 commit comments

Comments
 (0)