Skip to content

Commit 8901f1e

Browse files
committed
Get to functionally working sync of role assignments
1 parent 9e85840 commit 8901f1e

File tree

6 files changed

+138
-22
lines changed

6 files changed

+138
-22
lines changed

ansible_base/rbac/api/views.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ..models import DABContentType, DABPermission, get_evaluation_model
3434
from ..policies import check_content_obj_permission
3535
from ..remote import RemoteObject, get_resource_prefix
36+
from ..sync import maybe_reverse_sync_assignment, maybe_reverse_sync_unassignment
3637

3738

3839
def list_combine_values(data: dict[Type[Model], list[str]]) -> list[str]:
@@ -148,20 +149,36 @@ def filter_queryset(self, qs):
148149
new_qs = model.visible_items(self.request.user, qs)
149150
return super().filter_queryset(new_qs)
150151

152+
def remote_sync_assignment(self, assignment):
153+
"Intermediary for sync method so that child classes can modify it purely in viewset"
154+
maybe_reverse_sync_assignment(assignment)
155+
156+
def remote_sync_unassignment(self, role_definition, actor, content_object):
157+
maybe_reverse_sync_unassignment(role_definition, actor, content_object)
158+
151159
def perform_create(self, serializer):
152-
return super().perform_create(serializer)
160+
ret = super().perform_create(serializer)
161+
self.remote_sync_assignment(serializer.instance)
162+
return ret
153163

154164
def perform_destroy(self, instance):
155165
check_can_remove_assignment(self.request.user, instance)
156166
check_locally_managed(instance.role_definition)
157167

168+
# Save properties for sync after it is done locally (at which point assignment will not exist)
169+
role_definition = instance.role_definition
170+
actor = instance.actor
171+
content_object = instance.content_object
172+
158173
if instance.content_type_id:
159174
with transaction.atomic():
160175
instance.role_definition.remove_permission(instance.actor, instance.content_object)
161176
else:
162177
with transaction.atomic():
163178
instance.role_definition.remove_global_permission(instance.actor)
164179

180+
self.remote_sync_unassignment(role_definition, actor, content_object)
181+
165182

166183
class RoleTeamAssignmentViewSet(BaseAssignmentViewSet):
167184
"""

ansible_base/rbac/service_api/serializers.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ class BaseAssignmentSerializer(serializers.ModelSerializer):
6767
content_type = serializers.SlugRelatedField(read_only=True, slug_field='api_slug')
6868
role_definition = serializers.SlugRelatedField(slug_field='name', queryset=RoleDefinition.objects.all())
6969
created_by_ansible_id = ActorAnsibleIDField(source='created_by', required=False)
70-
object_ansible_id = ObjectIDAnsibleIDField(source='object_id', required=False)
71-
# TODO: use the from_service to control what we sync back to
70+
object_ansible_id = ObjectIDAnsibleIDField(source='object_id', required=False, allow_null=True)
71+
object_id = serializers.CharField(allow_blank=True)
7272
from_service = serializers.CharField(write_only=True)
7373

7474
def to_representation(self, instance):
@@ -84,15 +84,18 @@ def validate(self, attrs):
8484
8585
So this does the mutual validation to assure we have sufficient data.
8686
"""
87-
has_oid = 'object_id' in self.initial_data
88-
has_oaid = 'object_ansible_id' in self.initial_data
87+
has_oid = bool(self.initial_data.get('object_id'))
88+
has_oaid = bool(self.initial_data.get('object_ansible_id'))
8989

9090
if not self.partial and not has_oid and not has_oaid:
9191
raise serializers.ValidationError("You must provide either 'object_id' or 'object_ansible_id'.")
92+
elif not has_oaid:
93+
# need to remove blank and null fields or else it can overwrite the non-null non-blank field
94+
attrs['object_id'] = self.initial_data['object_id']
9295

9396
# NOTE: right now not enforcing the case you provide both, could check for consistency later
9497

95-
return attrs
98+
return super().validate(attrs)
9699

97100
def find_existing_assignment(self, queryset):
98101
actor = self.validated_data[self.actor_field]
@@ -116,10 +119,14 @@ def create(self, validated_data):
116119
obj = None
117120
if object_id:
118121
model = rd.content_type.model_class()
119-
try:
120-
obj = model.objects.get(pk=object_id)
121-
except model.DoesNotExist as exc:
122-
raise serializers.ValidationError({'object_id': str(exc)})
122+
123+
if issubclass(model, RemoteObject):
124+
obj = model(content_type=rd.content_type, object_id=object_id)
125+
else:
126+
try:
127+
obj = model.objects.get(pk=object_id)
128+
except model.DoesNotExist as exc:
129+
raise serializers.ValidationError({'object_id': str(exc)})
123130

124131
# Validators not ran, because this should be an internal action
125132

ansible_base/rbac/service_api/views.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ class ServiceRoleUserAssignmentViewSet(
5353
ansible_id_backend.RoleAssignmentFilterBackend,
5454
]
5555

56+
def remote_secondary_sync_assignment(self, assignment, from_service=None):
57+
"""To allow service-specific sync when getting assignment from /service-index/ endpoint
58+
59+
Will get a None value for from_service is the superuser is manually testing this endpoint.
60+
"""
61+
pass
62+
63+
def remote_secondary_sync_unassignment(self, role_definition, actor, content_object, from_service=None):
64+
pass
65+
5666
@action(detail=False, methods=['post'], url_path='assign')
5767
def assign(self, request):
5868
serializer = self.get_serializer(data=request.data)
@@ -64,6 +74,7 @@ def assign(self, request):
6474
return Response(output_serializer.data, status=status.HTTP_200_OK)
6575

6676
instance = serializer.save()
77+
self.remote_secondary_sync_assignment(serializer.instance, from_service=serializer.validated_data.get('from_service'))
6778
output_serializer = self.get_serializer(instance)
6879
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
6980

@@ -77,8 +88,14 @@ def unassign(self, request):
7788
output_serializer = self.get_serializer(existing)
7889
return Response(output_serializer.data, status=status.HTTP_200_OK)
7990

91+
# Save properties for sync after it is done locally (at which point assignment will not exist)
92+
role_definition = existing.role_definition
93+
actor = existing.actor
94+
content_object = existing.content_object
95+
8096
# Use standard DRF delete logic
8197
self.perform_destroy(existing)
98+
self.remote_secondary_sync_unassignment(role_definition, actor, content_object, from_service=serializer.validated_data.get('from_service'))
8299
return Response(status=status.HTTP_204_NO_CONTENT)
83100

84101
def perform_destroy(self, instance):

ansible_base/rbac/sync.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from ansible_base.resource_registry.utils.settings import resource_server_defined # safe import
2+
3+
"""
4+
Module is a parallel to resource_registry
5+
6+
ansible_base.resource_registry.utils.sync_to_resource_server.sync_to_resource_server
7+
8+
However, this only deals with role assignments, which have key differences
9+
- totally immutable model
10+
- have very weird way of referencing related objects
11+
- must run various internal RBAC logic for rebuilding RoleEvaluation entries
12+
"""
13+
14+
15+
def reverse_sync_enabled_all_conditions(assignment):
16+
"""This checks for basically all cases we do not reverse sync
17+
1. object level flag for skipping the sync
18+
2. environment variable to skip sync
19+
3. context manager to disable sync
20+
4. RESOURCE_SERVER setting not actually set
21+
"""
22+
from ansible_base.resource_registry.signals.handlers import reverse_sync_enabled
23+
from ansible_base.resource_registry.utils.sync_to_resource_server import should_skip_reverse_sync
24+
25+
if not resource_server_defined():
26+
return False
27+
28+
if not reverse_sync_enabled.enabled:
29+
return False
30+
31+
if should_skip_reverse_sync(assignment):
32+
return
33+
34+
return True
35+
36+
37+
def maybe_reverse_sync_assignment(assignment):
38+
if not reverse_sync_enabled_all_conditions(assignment):
39+
return
40+
41+
from ansible_base.resource_registry.utils.sync_to_resource_server import get_current_user_resource_client
42+
43+
client = get_current_user_resource_client()
44+
client.sync_assignment(assignment)
45+
46+
47+
def maybe_reverse_sync_unassignment(role_definition, actor, content_object):
48+
if not reverse_sync_enabled_all_conditions(role_definition):
49+
return
50+
51+
from ansible_base.resource_registry.utils.sync_to_resource_server import get_current_user_resource_client
52+
53+
client = get_current_user_resource_client()
54+
client.sync_unassignment(role_definition, actor, content_object)

ansible_base/resource_registry/rest_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def sync_unassignment(self, role_definition, actor, content_object):
190190
if ct.service == 'shared':
191191
data['object_ansible_id'] = str(content_object.resource.ansible_id)
192192
else:
193-
data['object_id'] = content_object.object_id
193+
data['object_id'] = content_object.pk
194194

195195
return self._sync_assignment(data, giving=False)
196196

test_app/tests/resource_registry/test_resources_api_rest_client.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,40 +183,61 @@ def test_list_role_permissions_all_pages(resource_client):
183183
assert resp.json()["count"] > 25
184184

185185

186-
def _assert_assignment_matches_data(assignment, data, organization, user):
186+
def _assert_assignment_matches_data(assignment, data, obj, user):
187187
assert 'created' in data, data
188188
# assert DateTimeField().to_representation(assignment.created) == data['created'] # TODO
189189
assert str(assignment.created_by.resource.ansible_id) == data['created_by_ansible_id']
190-
assert assignment.object_id == organization.id
190+
assert assignment.object_id == obj.id
191191
assert str(assignment.object_id) == str(data['object_id'])
192-
assert str(organization.resource.ansible_id) == data['object_ansible_id']
193-
assert 'shared.organization' == data['content_type']
194-
assert 'Organization Admin' == data['role_definition']
192+
if hasattr(obj, 'resource'):
193+
assert str(obj.resource.ansible_id) == data['object_ansible_id']
194+
assert 'shared.organization' == data['content_type']
195+
assert 'Organization Admin' == data['role_definition']
196+
else:
197+
assert 'aap.inventory' == data['content_type']
198+
assert 'change-inv' == data['role_definition']
195199
assert str(user.resource.ansible_id) == data['user_ansible_id']
196200

197201

198202
@pytest.mark.django_db
199-
def test_sync_assignment(resource_client, org_admin_rd, user, organization):
203+
def test_sync_org_assignment(resource_client, org_admin_rd, user, organization):
200204
assignment = org_admin_rd.give_permission(user, organization)
201205
resp = resource_client.sync_assignment(assignment)
202-
203206
assert resp.status_code == 200, resp.text
204-
205207
data = resp.json()
206208
# Existing assignment should be this current assignment
207209
_assert_assignment_matches_data(assignment, data, organization, user)
208210

209211
org_admin_rd.remove_permission(user, organization)
210-
211212
resp = resource_client.sync_assignment(assignment) # assignment not actually here locally
212-
213213
assert resp.status_code == 201, resp.text # created
214-
215214
data = resp.json()
216215
# All the data, on the remote system, should match our original assignment
217216
_assert_assignment_matches_data(assignment, data, organization, user)
218217

219218

219+
@pytest.mark.django_db
220+
def test_sync_obj_assignment(resource_client, user, inventory):
221+
inv_rd = RoleDefinition.objects.create_from_permissions(
222+
permissions=['change_inventory', 'view_inventory'],
223+
name='change-inv',
224+
content_type=permission_registry.content_type_model.objects.get_for_model(Inventory),
225+
)
226+
assignment = inv_rd.give_permission(user, inventory)
227+
resp = resource_client.sync_assignment(assignment)
228+
assert resp.status_code == 200, resp.text
229+
data = resp.json()
230+
# Existing assignment should be this current assignment
231+
_assert_assignment_matches_data(assignment, data, inventory, user)
232+
233+
inv_rd.remove_permission(user, inventory)
234+
resp = resource_client.sync_assignment(assignment) # assignment not actually here locally
235+
assert resp.status_code == 201, resp.text # created
236+
data = resp.json()
237+
# All the data, on the remote system, should match our original assignment
238+
_assert_assignment_matches_data(assignment, data, inventory, user)
239+
240+
220241
@pytest.mark.django_db
221242
def test_get_resource_404(resource_client):
222243
resource_client.raise_if_bad_request = True

0 commit comments

Comments
 (0)