Skip to content

Commit bcb95da

Browse files
committed
Assure that ansible_id is prefered for content_object in all cases
1 parent 4398287 commit bcb95da

File tree

3 files changed

+119
-2
lines changed

3 files changed

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

0 commit comments

Comments
 (0)