Skip to content

Commit b9fce77

Browse files
committed
Save permissions via additional_data
1 parent 3645522 commit b9fce77

File tree

7 files changed

+98
-9
lines changed

7 files changed

+98
-9
lines changed

ansible_base/rbac/models/role.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ansible_base.rbac.permission_registry import permission_registry
2323
from ansible_base.rbac.prefetch import TypesPrefetch
2424
from ansible_base.rbac.validators import validate_assignment, validate_permissions_for_model
25+
from ansible_base.resource_registry.fields import AnsibleResourceField
2526

2627
from ..remote import RemoteObject
2728
from .content_type import DABContentType
@@ -164,6 +165,8 @@ class Meta:
164165
default=None,
165166
on_delete=models.CASCADE,
166167
)
168+
# This is a synchronized field, so add reverse relation for resource
169+
resource = AnsibleResourceField(primary_key_field="id")
167170

168171
objects = RoleDefinitionManager()
169172
router_basename = 'roledefinition'
@@ -323,6 +326,15 @@ def user_global_permissions(cls, user, permission_qs=None):
323326
perm_set.update(perm_qs)
324327
return perm_set
325328

329+
def save_remote_permissions(self, additional_data: list[str]):
330+
"""Save permissions from an external system.
331+
332+
This does very little more than just making the related permissions exactly equal to the set.
333+
If a provided permission is not present in the local system, we pretend we did not get it.
334+
However, we must still allow removing permissions from the role.
335+
"""
336+
self.permissions.set(DABPermission.objects.filter(api_slug__in=additional_data))
337+
326338
def summary_fields(self):
327339
return {'id': self.id, 'name': self.name, 'description': self.description, 'managed': self.managed}
328340

ansible_base/resource_registry/rest_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
ResourceRequestBody = namedtuple(
1212
"ResourceRequestBody",
13-
["ansible_id", "service_id", "is_partially_migrated", "resource_type", "resource_data"],
14-
defaults=(None, None, None, None, None),
13+
["ansible_id", "service_id", "is_partially_migrated", "resource_type", "resource_data", "additional_data"],
14+
defaults=(None, None, None, None, None, None),
1515
)
1616

1717

ansible_base/resource_registry/serializers.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def create(self, validated_data):
9696

9797

9898
class ResourceSerializer(ResourceListSerializer):
99-
additional_data = serializers.SerializerMethodField()
99+
additional_data = serializers.JSONField(write_only=True, required=False)
100100
resource_data = ResourceDataField(source="*")
101101

102102
class Meta:
@@ -106,12 +106,37 @@ class Meta:
106106
"additional_data",
107107
]
108108

109-
def get_additional_data(self, obj):
110-
if serializer := obj.content_type.resource_type.serializer_class:
109+
def to_representation(self, instance):
110+
ret = super().to_representation(instance)
111+
112+
additional_data = None
113+
if serializer := instance.content_type.resource_type.serializer_class:
111114
if serializer.ADDITIONAL_DATA_SERIALIZER is not None:
112-
return serializer.ADDITIONAL_DATA_SERIALIZER(obj.content_object).data
115+
additional_data = serializer.ADDITIONAL_DATA_SERIALIZER(instance.content_object).data
116+
117+
ret['additional_data'] = additional_data
118+
return ret
119+
120+
def _maybe_process_additional_data(self, instance, additional_data):
121+
if not additional_data:
122+
return
113123

114-
return None
124+
if serializer := instance.content_type.resource_type.serializer_class:
125+
if serializer and hasattr(serializer, 'process_additional_data'):
126+
logger.debug(f'Processing additional data for resource {str(instance.ansible_id)}')
127+
serializer(instance.content_object).process_additional_data(instance.content_object, additional_data)
128+
129+
def create(self, validated_data):
130+
additional_data = validated_data.pop('additional_data', None)
131+
instance = super().create(validated_data)
132+
self._maybe_process_additional_data(instance, additional_data)
133+
return instance
134+
135+
def update(self, instance, validated_data):
136+
additional_data = validated_data.pop('additional_data', None)
137+
instance = super().update(instance, validated_data)
138+
self._maybe_process_additional_data(instance, additional_data)
139+
return instance
115140

116141

117142
class ResourceTypeSerializer(serializers.ModelSerializer):

ansible_base/resource_registry/shared_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,6 @@ class RoleDefinitionType(SharedResourceTypeSerializer):
100100
allow_null=True,
101101
default=None,
102102
)
103+
104+
def process_additional_data(self, instance, additional_data):
105+
instance.save_remote_permissions(additional_data.get('permissions'))

test_app/tests/rbac/models/test_role_definition.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,12 @@ def test_change_role_definition_member_permission(organization, inventory, org_t
105105
# Adding it back restores them
106106
member_rd.permissions.add(member_perm)
107107
assert [u.has_obj_perm(inventory, 'change') for u in (team_user, org_team_user)] == [True, True]
108+
109+
110+
@pytest.mark.django_db
111+
def test_save_remote_permissions(inv_rd):
112+
"This is tested as part of resource registry in test_additional_data_write"
113+
assert {perm.api_slug for perm in inv_rd.permissions.all()} == {'aap.view_inventory', 'aap.change_inventory'}
114+
assert len(DABPermission.objects.filter(api_slug__in={'aap.view_inventory', 'fooland.not_here'})) == 1
115+
inv_rd.permissions.set([DABPermission.objects.get(api_slug='aap.view_inventory')])
116+
assert {perm.api_slug for perm in inv_rd.permissions.all()} == {'aap.view_inventory'}

test_app/tests/rbac/test_management_rbac_checks.py renamed to test_app/tests/rbac/test_management.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from io import StringIO
22

33
import pytest
4+
from django.apps import apps
45

6+
from ansible_base.rbac.management import create_dab_permissions
57
from ansible_base.rbac.management.commands.RBAC_checks import Command
6-
from ansible_base.rbac.models import DABContentType, ObjectRole, RoleDefinition
8+
from ansible_base.rbac.models import DABContentType, DABPermission, ObjectRole, RoleDefinition
79
from test_app.models import Inventory
810

911

@@ -25,3 +27,14 @@ def test_role_definition_wrong_model(organization):
2527
rd, _ = RoleDefinition.objects.get_or_create(name='foo-def', permissions=['view_organization'])
2628
orole = ObjectRole.objects.create(object_id=inventory.id, content_type=DABContentType.objects.get_for_model(inventory), role_definition=rd)
2729
assert f"Object role {orole} has permission view_organization for an unlike content type" in run_and_get_output()
30+
31+
32+
@pytest.mark.django_db
33+
def test_recreate_model_permissions():
34+
del_ct = 0
35+
for permission in DABPermission.objects.filter(content_type__model='inventory'):
36+
permission.delete()
37+
del_ct += 1
38+
assert del_ct == 5
39+
create_dab_permissions(app_config=apps.get_app_config('dab_rbac'))
40+
assert DABPermission.objects.filter(content_type__model='inventory').count() == 5

test_app/tests/resource_registry/test_resources_api_rest_client.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from requests.exceptions import HTTPError
66

77
from ansible_base.authentication.models import AuthenticatorUser
8+
from ansible_base.rbac import permission_registry
9+
from ansible_base.rbac.models import RoleDefinition
810
from ansible_base.resource_registry.models import Resource, service_id
911
from ansible_base.resource_registry.resource_server import get_resource_server_config
1012
from ansible_base.resource_registry.rest_client import ResourceAPIClient, ResourceRequestBody
13+
from test_app.models import Inventory
1114

1215

1316
@pytest.fixture
@@ -182,7 +185,7 @@ def test_get_resource_404(resource_client):
182185

183186

184187
@pytest.mark.django_db
185-
def test_additional_data(resource_client, django_user_model, github_authenticator):
188+
def test_additional_data_read(resource_client, django_user_model, github_authenticator):
186189
user = django_user_model.objects.create(username="lisan_al_gaib")
187190

188191
AuthenticatorUser.objects.create(provider=github_authenticator, user=user, uid="different_uid")
@@ -200,6 +203,30 @@ def test_additional_data(resource_client, django_user_model, github_authenticato
200203
assert additional["social_auth"][0]["sso_server"] == "https://github.com/login/oauth/authorize"
201204

202205

206+
@pytest.mark.django_db
207+
@pytest.mark.parametrize('partial', [True, False])
208+
def test_additional_data_write(resource_client, partial):
209+
"Will remove a permission from a role definition using the additional_data field."
210+
rd = RoleDefinition.objects.create_from_permissions(
211+
permissions=['aap.change_inventory', 'aap.view_inventory'],
212+
name='change-inv-for-now',
213+
content_type=permission_registry.content_type_model.objects.get_for_model(Inventory),
214+
)
215+
ansible_id = str(rd.resource.ansible_id)
216+
217+
# Need this to make a coherent PUT
218+
resp = resource_client.get_resource(ansible_id)
219+
assert resp.status_code == 200
220+
ref = resp.json()
221+
222+
data = ResourceRequestBody(additional_data={'permissions': ['aap.view_inventory', 'fooland.action_unicorns']}, resource_data=ref['resource_data'])
223+
resp = resource_client.update_resource(ansible_id, data, partial=partial)
224+
assert resp.status_code == 200, resp.__dict__
225+
226+
# Removed the change permission
227+
assert {perm.api_slug for perm in rd.permissions.all()} == {'aap.view_inventory'}
228+
229+
203230
@pytest.mark.django_db
204231
def test_validate_local_user(resource_client, admin_user, member_rd):
205232
resp = resource_client.validate_local_user(username=admin_user.username, password="password")

0 commit comments

Comments
 (0)