Skip to content

Commit cfce158

Browse files
committed
Test and fix creating assignments through the API
1 parent 98b6da9 commit cfce158

File tree

6 files changed

+97
-16
lines changed

6 files changed

+97
-16
lines changed

ansible_base/rbac/api/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ansible_base.rbac.validators import check_locally_managed, validate_permissions_for_model
1717

1818
from ..models import DABContentType, DABPermission, get_evaluation_model
19+
from ..remote import RemoteObject
1920

2021

2122
class RoleDefinitionSerializer(CommonModelSerializer):
@@ -129,6 +130,8 @@ def get_object_from_data(self, validated_data, role_definition, requesting_user)
129130
if not role_definition.content_type:
130131
raise ValidationError({'object_id': _('System role does not allow for object assignment')})
131132
model = role_definition.content_type.model_class()
133+
if issubclass(model, RemoteObject):
134+
return model(content_type=role_definition.content_type, object_id=validated_data['object_id'])
132135
try:
133136
obj = serializers.PrimaryKeyRelatedField(queryset=model.access_qs(requesting_user)).to_internal_value(validated_data['object_id'])
134137
except ValidationError as exc:

ansible_base/rbac/models/role.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from ansible_base.rbac.validators import validate_assignment, validate_permissions_for_model
2525
from ansible_base.resource_registry.fields import AnsibleResourceField
2626

27-
from ..remote import RemoteObject
27+
from ..remote import RemoteObject, StandInPK
2828
from .content_type import DABContentType
2929
from .fields import FederatedForeignKey
3030
from .permission import DABPermission
@@ -531,7 +531,7 @@ def descendent_roles(self):
531531
descendents.update(set(target_team.has_roles.all()))
532532
return descendents
533533

534-
def expected_direct_permissions(self, types_prefetch=None) -> set[tuple[str, int, Union[int, str, UUID]]]:
534+
def expected_direct_permissions(self, types_prefetch=None) -> set[tuple[str, int, Union[int, UUID]]]:
535535
"""The expected permissions that holding this ObjectRole confers to the holder
536536
537537
This is given in the form of tuples, which represent RoleEvaluation entries.
@@ -546,7 +546,8 @@ def expected_direct_permissions(self, types_prefetch=None) -> set[tuple[str, int
546546
role_content_type = types_prefetch.get_content_type(self.content_type_id)
547547
role_model = role_content_type.model_class()
548548
if role_content_type.is_remote:
549-
object_id = self.object_id
549+
pk_field = StandInPK(role_content_type) # remote, mock, field
550+
object_id = pk_field.to_python(self.object_id)
550551
else:
551552
# ObjectRole.object_id is stored as text, we convert it to the model pk native type
552553
object_id = role_model._meta.pk.to_python(self.object_id)

ansible_base/rbac/remote.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import inspect
2+
import uuid
23
from typing import Type, Union
34

45
from django.apps import apps
@@ -19,26 +20,33 @@
1920
"""
2021

2122

23+
class StandInPK:
24+
def __init__(self, ct: models.Model):
25+
self.pk_field_type = ct.pk_field_type
26+
27+
def get_prep_value(self, value: Union[str, int, uuid.UUID]) -> Union[str, int]:
28+
if self.pk_field_type == "uuid":
29+
if isinstance(value, uuid.UUID):
30+
return str(value)
31+
return str(uuid.UUID(value))
32+
return int(value)
33+
34+
def to_python(self, value: Union[str, int, uuid.UUID]) -> Union[int, uuid.UUID]:
35+
if self.pk_field_type == "uuid":
36+
if isinstance(value, uuid.UUID):
37+
return value
38+
return uuid.UUID(value)
39+
return int(value)
40+
41+
2242
class StandinMeta:
2343
def __init__(self, ct: models.Model, abstract=False):
2444
self.service = ct.service
2545
self.model_name = ct.model
2646
self.app_label = ct.app_label
2747
self.abstract = abstract
2848

29-
class PK:
30-
def __init__(self, pk_field_type):
31-
self.pk_field_type = pk_field_type
32-
33-
def get_prep_value(self, value):
34-
# TODO: handle uuid fields based on
35-
# if self.pk_field_type:
36-
return int(value)
37-
38-
def to_python(self, value):
39-
return int(value)
40-
41-
self.pk = PK(ct.pk_field_type)
49+
self.pk = StandInPK(ct)
4250

4351

4452
class RemoteObject:
@@ -54,6 +62,12 @@ def __init__(self, content_type: models.Model, object_id: Union[int, str]):
5462
if content_type.model != self._meta.model_name:
5563
raise RuntimeError(f'RemoteObject created with type {content_type} but with type for {self._meta.model_name}')
5664

65+
# Raise an early error if the primary key is obviously not valid for the model type
66+
try:
67+
self._meta.pk.to_python(object_id)
68+
except (ValueError, TypeError, AttributeError) as e:
69+
raise ValueError(f"Invalid primary key value {object_id} for type {content_type.pk_field_type}, error: {e}")
70+
5771
def __repr__(self):
5872
return f"<RemoteObject {self.content_type} id={self.object_id}>"
5973

test_app/tests/rbac/remote/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ def foo_rd(foo_type, foo_permission):
1919
return RoleDefinition.objects.create_from_permissions(
2020
name='Foo fooers for the foos in foo service', permissions=[foo_permission.api_slug], content_type=foo_type
2121
)
22+
23+
24+
@pytest.fixture
25+
def foo_type_uuid():
26+
return DABContentType.objects.create(service='foo', model='foo_uuid', app_label='foo', pk_field_type='uuid')
27+
28+
29+
@pytest.fixture
30+
def foo_permission_uuid(foo_type_uuid):
31+
return DABPermission.objects.create(codename='foo_foo_uuid', content_type=foo_type_uuid)
32+
33+
34+
@pytest.fixture
35+
def foo_rd_uuid(foo_type_uuid, foo_permission_uuid):
36+
return RoleDefinition.objects.create_from_permissions(name='UUID foo role thing', permissions=[foo_permission_uuid.api_slug], content_type=foo_type_uuid)

test_app/tests/rbac/remote/test_public_api_compat.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
import pytest
24

35
from ansible_base.lib.utils.response import get_relative_url
@@ -53,3 +55,31 @@ def test_user_role_assignment_remote_and_local(admin_api_client, rando, foo_type
5355
sf = item['summary_fields']
5456
assert 'content_object' in sf
5557
assert sf['content_object'] == {'<remote_object_placeholder>': True, 'model_name': 'foo', 'service': 'foo', 'pk': 42}
58+
59+
60+
@pytest.mark.django_db
61+
def test_give_permission_to_remote_object(admin_api_client, rando, foo_type, foo_rd):
62+
a_foo = RemoteObject(content_type=foo_type, object_id=42)
63+
assert not rando.has_obj_perm(a_foo, 'foo')
64+
65+
url = get_relative_url('roleuserassignment-list')
66+
# NOTE: at this point the object_id is made up, cross-server coordination is not running here
67+
data = {"role_definition": foo_rd.id, "user": rando.pk, "object_id": 42}
68+
response = admin_api_client.post(path=url, data=data)
69+
assert response.status_code == 201, response.data
70+
71+
assert rando.has_obj_perm(a_foo, 'foo')
72+
73+
74+
@pytest.mark.django_db
75+
def test_give_permission_to_remote_object_uuid(admin_api_client, rando, foo_type_uuid, foo_rd_uuid):
76+
pk_value = str(uuid.uuid4())
77+
a_foo = RemoteObject(content_type=foo_type_uuid, object_id=pk_value)
78+
assert not rando.has_obj_perm(a_foo, 'foo')
79+
80+
url = get_relative_url('roleuserassignment-list')
81+
data = {"role_definition": foo_rd_uuid.id, "user": rando.pk, "object_id": pk_value}
82+
response = admin_api_client.post(path=url, data=data)
83+
assert response.status_code == 201, response.data
84+
85+
assert rando.has_obj_perm(a_foo, 'foo')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import uuid
2+
3+
import pytest
4+
5+
from ansible_base.rbac.remote import RemoteObject
6+
7+
8+
@pytest.mark.django_db
9+
def test_validate_object_id_uuid(foo_type_uuid):
10+
"The 42 primary key is invalid for uuid type objects"
11+
with pytest.raises(ValueError):
12+
RemoteObject(content_type=foo_type_uuid, object_id=42)
13+
14+
15+
@pytest.mark.django_db
16+
def test_validate_object_id_int(foo_type):
17+
with pytest.raises(ValueError):
18+
RemoteObject(content_type=foo_type, object_id=str(uuid.uuid4()))

0 commit comments

Comments
 (0)