Skip to content

Commit e7ec1aa

Browse files
committed
Make assignment list fully function with remote permissions
1 parent d495e3d commit e7ec1aa

File tree

5 files changed

+85
-26
lines changed

5 files changed

+85
-26
lines changed

ansible_base/rbac/models/content_type.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ def app_labeled_name(self) -> str:
221221
model = self.model_class()
222222
if not model:
223223
return self.model
224+
if issubclass(model, RemoteObject):
225+
return f'RemoteObject | {self.model}'
224226
return f"{model._meta.app_config.verbose_name} | {model._meta.verbose_name}"
225227

226228
def save(self, *args, **kwargs):

ansible_base/rbac/models/fields.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.db.models.sql import AND
1010
from django.db.models.sql.where import WhereNode
1111

12-
from ..remote import get_local_resource_prefix
12+
from ..remote import RemoteObject, get_local_resource_prefix
1313
from .content_type import DABContentType
1414

1515

@@ -59,6 +59,8 @@ def _check_content_type_field(self):
5959

6060
def get_content_type(self, obj=None, id=None, using=None, model=None):
6161
if obj is not None:
62+
if isinstance(obj, RemoteObject):
63+
return obj.content_type
6264
return DABContentType.objects.db_manager(obj._state.db).get_for_model(
6365
obj.__class__,
6466
)

ansible_base/rbac/remote.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,45 @@ def __init__(self, ct: models.Model, abstract=False):
2626
self.app_label = ct.app_label
2727
self.abstract = abstract
2828

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)
42+
2943

3044
class RemoteObject:
3145
"""Placeholder for objects that live in another project."""
3246

3347
def __init__(self, content_type: models.Model, object_id: Union[int, str]):
3448
self.content_type = content_type
3549
self.object_id = object_id
36-
self._meta = StandinMeta(content_type, abstract=True)
50+
if not hasattr(self, '_meta'):
51+
# If object is created without a type-specific subclass, do the best we can
52+
self._meta = StandinMeta(content_type, abstract=True)
53+
else:
54+
if content_type.model != self._meta.model_name:
55+
raise RuntimeError(f'RemoteObject created with type {content_type} but with type for {self._meta.model_name}')
3756

3857
def __repr__(self):
3958
return f"<RemoteObject {self.content_type} id={self.object_id}>"
4059

60+
def __eq__(self, value):
61+
if isinstance(value, RemoteObject):
62+
return bool(self.content_type.id == value.content_type.id and self.pk == value.pk)
63+
return super().__eq__(value)
64+
65+
def __hash__(self):
66+
return hash((self.content_type.id, self.pk))
67+
4168
@classmethod
4269
def get_ct_from_type(cls):
4370
if not hasattr(cls, '_meta'):
@@ -59,7 +86,16 @@ def access_ids_qs(cls, actor, codename: str = 'view', content_types=None, cast_f
5986

6087
@property
6188
def pk(self):
62-
return self.object_id
89+
"""Alias to :attr:`object_id` for compatibility with Django. Also, handles type."""
90+
return self._meta.pk.to_python(self.object_id)
91+
92+
def summary_fields(self):
93+
"""This gives a placeholder, planned to introduce a summary_fields shared endpoint.
94+
95+
This placeholder should be cleary identifable by a client or by the RBAC resource server.
96+
Then, the idea, is that it can make a request to the remote server to get the summary data.
97+
"""
98+
return {'<remote_object_placeholder>': True, 'model_name': self._meta.model_name, 'service': self._meta.service, 'pk': self.pk}
6399

64100

65101
def get_remote_base_class() -> Type[RemoteObject]:
Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from ansible_base.lib.utils.response import get_relative_url
4+
from ansible_base.rbac.remote import RemoteObject
45

56

67
@pytest.mark.django_db
@@ -30,26 +31,25 @@ def test_create_remote_role_definition(admin_api_client, foo_type, foo_permissio
3031
assert response.data['permissions'] == ['foo.foo_foo']
3132

3233

33-
# TODO: check that assignment endpoint works
34-
35-
# @pytest.mark.django_db
36-
# def test_give_remote_permission(rando, foo_type, foo_permission, foo_rd):
37-
# assert foo_type.service == 'foo' # a place, a domain, a server, known as foo
38-
# assert foo_type.api_slug == 'foo.foo' # there lives a foo in foo
39-
40-
# assert foo_permission.api_slug == 'foo.foo_foo' # expression of the ability that one may foo a foo
41-
42-
# a_foo = RemoteObject(content_type=foo_type, object_id=42)
43-
# assignment = foo_rd.give_permission(rando, a_foo)
44-
45-
# assignment = RoleUserAssignment.objects.get(pk=assignment.pk)
46-
# assert isinstance(assignment.content_object, RemoteObject)
47-
48-
# # We can do evaluation querysets, but these can not return objects, just id values
49-
# assert set(foo_type.model_class().access_ids_qs(actor=rando, codename='foo')) == {(int(assignment.object_id),)}
50-
51-
# # Test that user-attached methods also work
52-
# assert rando.has_obj_perm(a_foo, 'foo')
53-
# with pytest.raises(RuntimeError) as exc:
54-
# assert not rando.has_obj_perm(a_foo, 'bar') # not a valid permission
55-
# assert 'The permission bar_foo is not valid for model foo' in str(exc)
34+
@pytest.mark.django_db
35+
def test_give_remote_permission(admin_api_client, rando, foo_type, foo_rd):
36+
a_foo = RemoteObject(content_type=foo_type, object_id=42)
37+
assignment = foo_rd.give_permission(rando, a_foo)
38+
assignment.content_object
39+
40+
assert isinstance(assignment.content_object, RemoteObject)
41+
42+
# Should show up in the assignments list
43+
url = get_relative_url('roleuserassignment-list')
44+
response = admin_api_client.get(url, format="json")
45+
assert response.status_code == 200, response.data
46+
47+
data_by_rd = {item['role_definition']: item for item in response.data['results']}
48+
assert foo_rd.id in data_by_rd
49+
item = data_by_rd[foo_rd.id]
50+
assert item['user'] == rando.id
51+
assert item['object_id'] == str(a_foo.object_id)
52+
assert 'summary_fields' in item
53+
sf = item['summary_fields']
54+
assert 'content_object' in sf
55+
assert sf['content_object'] == {'<remote_object_placeholder>': True, 'model_name': 'foo', 'service': 'foo', 'pk': 42}

test_app/tests/rbac/remote/test_remote_assignment.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ansible_base.rbac.models import RoleUserAssignment
44
from ansible_base.rbac.remote import RemoteObject
5+
from test_app.models import User
56

67

78
@pytest.mark.django_db
@@ -13,9 +14,12 @@ def test_give_remote_permission(rando, foo_type, foo_permission, foo_rd):
1314

1415
a_foo = RemoteObject(content_type=foo_type, object_id=42)
1516
assignment = foo_rd.give_permission(rando, a_foo)
17+
assert '42' in repr(a_foo)
1618

1719
assignment = RoleUserAssignment.objects.get(pk=assignment.pk)
20+
assert assignment.content_object.pk == 42
1821
assert isinstance(assignment.content_object, RemoteObject)
22+
assert isinstance(assignment.content_object, RemoteObject) # There was a bug where multiple references would error
1923

2024
# We can do evaluation querysets, but these can not return objects, just id values
2125
assert set(foo_type.model_class().access_ids_qs(actor=rando, codename='foo')) == {(int(assignment.object_id),)}
@@ -25,3 +29,18 @@ def test_give_remote_permission(rando, foo_type, foo_permission, foo_rd):
2529
with pytest.raises(RuntimeError) as exc:
2630
assert not rando.has_obj_perm(a_foo, 'bar') # not a valid permission
2731
assert 'The permission bar_foo is not valid for model foo' in str(exc)
32+
33+
34+
@pytest.mark.django_db
35+
def test_prefetch_related_objects(foo_type, foo_rd, inv_rd, inventory):
36+
users = [User.objects.create(username=f'user{i}') for i in range(10)]
37+
38+
a_foo = RemoteObject(content_type=foo_type, object_id=42)
39+
for u in users:
40+
foo_rd.give_permission(u, a_foo)
41+
inv_rd.give_permission(u, inventory)
42+
43+
assert RoleUserAssignment.objects.count() == 20
44+
assert {assignment.content_object for assignment in RoleUserAssignment.objects.all()} == {a_foo, inventory}
45+
assert {assignment.content_object for assignment in RoleUserAssignment.objects.all()} == {a_foo, inventory}
46+
assert {assignment.content_object for assignment in RoleUserAssignment.objects.prefetch_related('content_object')} == {a_foo, inventory}

0 commit comments

Comments
 (0)