Skip to content

Commit 673a1e9

Browse files
committed
First pass at drill-down endpoint
1 parent 58620c1 commit 673a1e9

File tree

5 files changed

+221
-68
lines changed

5 files changed

+221
-68
lines changed

ansible_base/rbac/api/queries.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.db.models import Model
2+
3+
from ..models import DABContentType, get_evaluation_model
4+
5+
6+
def assignment_qs_user_to_obj(actor: Model, obj: Model):
7+
"""Querset of assignments (team or user) that grants the actor any form of permission to obj"""
8+
evaluation_cls = get_evaluation_model(obj)
9+
ct = DABContentType.objects.get_for_model(obj)
10+
reverse_name = evaluation_cls._meta.get_field('role').remote_field.name
11+
12+
# All relevant assignments for the object
13+
obj_eval_qs = evaluation_cls.objects.filter(object_id=obj.pk, content_type_id=ct.id)
14+
obj_assignment_qs = actor.role_assignments.filter(**{f'object_role__{reverse_name}__in': obj_eval_qs})
15+
16+
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions__content_type=ct)
17+
18+
return (global_assignment_qs | obj_assignment_qs).distinct()
19+
20+
21+
def assignment_qs_user_to_obj_perm(actor: Model, obj: Model, permission: Model):
22+
"""Queryset of assignments that grants this specific permission to this specific object"""
23+
evaluation_cls = get_evaluation_model(obj)
24+
ct = DABContentType.objects.get_for_model(obj)
25+
reverse_name = evaluation_cls._meta.get_field('role').remote_field.name
26+
27+
obj_eval_qs = evaluation_cls.objects.filter(codename=permission.codename, object_id=obj.pk, content_type_id=ct.id)
28+
obj_assignment_qs = actor.role_assignments.filter(**{f'object_role__{reverse_name}__in': obj_eval_qs})
29+
30+
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions=permission)
31+
32+
return (global_assignment_qs | obj_assignment_qs).distinct()

ansible_base/rbac/api/serializers.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
from ansible_base.lib.abstract_models.common import get_url_for_object
1111
from ansible_base.lib.serializers.common import AbstractCommonModelSerializer, CommonModelSerializer, ImmutableCommonModelSerializer
1212
from ansible_base.lib.utils.auth import get_team_model
13+
from ansible_base.lib.utils.response import get_relative_url
1314
from ansible_base.rbac.models import RoleDefinition, RoleTeamAssignment, RoleUserAssignment
1415
from ansible_base.rbac.permission_registry import permission_registry # careful for circular imports
1516
from ansible_base.rbac.policies import check_content_obj_permission, visible_users
1617
from ansible_base.rbac.validators import check_locally_managed, validate_permissions_for_model
1718

18-
from ..models import DABContentType, DABPermission, get_evaluation_model
19+
from ..models import DABContentType, DABPermission
1920
from ..remote import RemoteObject
21+
from .queries import assignment_qs_user_to_obj, assignment_qs_user_to_obj_perm
2022

2123

2224
class RoleDefinitionSerializer(CommonModelSerializer):
@@ -249,6 +251,17 @@ class RoleMetadataSerializer(serializers.Serializer):
249251

250252
class AccessListMixin:
251253

254+
def _get_related(self, obj) -> dict[str, str]:
255+
if obj is None:
256+
return {}
257+
related_fields = {}
258+
actor_cls = self.Meta.model
259+
related_fields['details'] = get_relative_url(
260+
f'role-{actor_cls._meta.model_name}-access-assignments',
261+
kwargs={'model_name': self.context.get("content_type").api_slug, 'pk': self.context.get("related_object").pk, 'actor_pk': obj.pk},
262+
)
263+
return related_fields
264+
252265
@staticmethod
253266
def summarize_role_definition(role_definition):
254267
return {"name": role_definition.name, "url": get_url_for_object(role_definition)}
@@ -258,36 +271,25 @@ def get_object_role_assignments(self, actor):
258271
permission = self.context.get("permission")
259272
ct = self.context.get("content_type")
260273

261-
assignment_list = []
262-
263-
evaluation_cls = get_evaluation_model(obj)
264-
reverse_name = evaluation_cls._meta.get_field('role').remote_field.name
265274
if permission:
266-
obj_eval_qs = evaluation_cls.objects.filter(codename=permission.codename, object_id=obj.pk, content_type_id=ct.id)
275+
assignment_qs = assignment_qs_user_to_obj_perm(actor, obj, permission)
267276
else:
268-
# All relevant assignments for the object
269-
obj_eval_qs = evaluation_cls.objects.filter(object_id=obj.pk, content_type_id=ct.id)
270-
obj_assignment_qs = actor.role_assignments.filter(**{f'object_role__{reverse_name}__in': obj_eval_qs})
277+
assignment_qs = assignment_qs_user_to_obj(actor, obj)
271278

272279
team_ct = DABContentType.objects.get_for_model(get_team_model())
273280

274-
for assignment in obj_assignment_qs.distinct():
275-
if assignment.content_type_id == team_ct.pk:
281+
assignment_list = []
282+
for assignment in assignment_qs.distinct():
283+
if assignment.content_type_id is None:
284+
perm_type = "global"
285+
elif assignment.content_type_id == team_ct.pk:
276286
perm_type = "team"
277287
elif assignment.content_type_id == ct.pk:
278288
perm_type = "direct"
279289
else:
280290
perm_type = "indirect"
281291
assignment_list.append({"type": perm_type, "role_definition": self.summarize_role_definition(assignment.role_definition)})
282292

283-
if permission:
284-
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions=permission)
285-
else:
286-
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions__content_type=ct)
287-
288-
for assignment in global_assignment_qs.distinct():
289-
assignment_list.append({"type": "global", "role_definition": self.summarize_role_definition(assignment.role_definition)})
290-
291293
return assignment_list
292294

293295
def get_url(self, obj) -> str:
@@ -298,9 +300,19 @@ class UserAccessListMixin(AccessListMixin, serializers.ModelSerializer):
298300
"controller uses auth.User model so this needs to be as compatible as possible, thus ModelSerializer"
299301

300302
object_role_assignments = serializers.SerializerMethodField()
301-
_expected_fields = ['id', 'url', 'username', 'is_superuser', 'object_role_assignments']
303+
related = serializers.SerializerMethodField('_get_related')
304+
_expected_fields = ['id', 'url', 'related', 'username', 'is_superuser', 'object_role_assignments']
302305

303306

304307
class TeamAccessListMixin(AccessListMixin, AbstractCommonModelSerializer):
305308
object_role_assignments = serializers.SerializerMethodField()
306-
_expected_fields = ['id', 'url', 'name', 'organization', 'object_role_assignments']
309+
related = serializers.SerializerMethodField('_get_related')
310+
_expected_fields = ['id', 'url', 'related', 'name', 'organization', 'object_role_assignments']
311+
312+
313+
class UserAccessAssignmentSerializer(RoleUserAssignmentSerializer):
314+
pass
315+
316+
317+
class TeamAccessAssignmentSerializer(RoleTeamAssignmentSerializer):
318+
pass

ansible_base/rbac/api/views.py

Lines changed: 107 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
RoleMetadataSerializer,
2121
RoleTeamAssignmentSerializer,
2222
RoleUserAssignmentSerializer,
23+
TeamAccessAssignmentSerializer,
2324
TeamAccessListMixin,
25+
UserAccessAssignmentSerializer,
2426
UserAccessListMixin,
2527
)
2628
from ansible_base.rbac.evaluations import has_super_permission
@@ -34,6 +36,7 @@
3436
from ..policies import check_content_obj_permission
3537
from ..remote import RemoteObject, get_resource_prefix
3638
from ..sync import maybe_reverse_sync_assignment, maybe_reverse_sync_unassignment
39+
from .queries import assignment_qs_user_to_obj, assignment_qs_user_to_obj_perm
3740

3841

3942
def list_combine_values(data: dict[Type[Model], list[str]]) -> list[str]:
@@ -220,7 +223,61 @@ class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
220223
]
221224

222225

226+
class AccessURLMixin:
227+
def get_actor_model(self):
228+
return get_user_model()
229+
230+
def get_url_permission(self):
231+
model_name = self.kwargs.get("model_name")
232+
# Prefer treating the URL as requesting for some permission
233+
return DABPermission.objects.filter(api_slug=model_name).first()
234+
235+
def get_url_content_type(self):
236+
if getattr(self, 'permission', None):
237+
# Access list will be all permissions for the given object
238+
return self.permission.content_type
239+
240+
model_name = self.kwargs.get("model_name")
241+
content_type = DABContentType.objects.filter(api_slug=model_name).first()
242+
if not content_type:
243+
raise NotFound(f'The slug {model_name} is not a valid permission or type identifier')
244+
245+
return content_type
246+
247+
def get_url_obj(self):
248+
model_cls = self.content_type.model_class()
249+
object_id = self.kwargs.get("pk")
250+
if not issubclass(model_cls, RemoteObject):
251+
try:
252+
return model_cls.objects.get(pk=object_id)
253+
except model_cls.DoesNotExist:
254+
raise NotFound(f'The primary key {object_id} was not found for model {model_cls}')
255+
else:
256+
return model_cls(content_type=self.content_type, object_id=object_id)
257+
258+
def check_permission_to_object(self, obj):
259+
try:
260+
if not self.request.user.has_obj_perm(obj, 'view'):
261+
raise NotFound
262+
except RuntimeError:
263+
check_content_obj_permission(self.request.user, obj)
264+
265+
def get_serializer_context(self):
266+
ctx = super().get_serializer_context()
267+
self.get_data_from_url()
268+
269+
ctx.update(
270+
{
271+
"permission": self.permission,
272+
"related_object": self.related_object,
273+
"content_type": self.content_type,
274+
}
275+
)
276+
return ctx
277+
278+
223279
class UserAccessViewSet(
280+
AccessURLMixin,
224281
AnsibleBaseDjangoAppApiView,
225282
mixins.ListModelMixin,
226283
GenericViewSet,
@@ -232,40 +289,12 @@ class UserAccessViewSet(
232289

233290
serializer_mixin = UserAccessListMixin
234291

235-
def get_actor_model(self):
236-
return get_user_model()
237-
238292
def get_data_from_url(self):
239293
if not hasattr(self, 'related_object'):
240-
model_name = self.kwargs.get("model_name")
241-
object_id = self.kwargs.get("pk")
242-
243-
# Prefer treating the URL as requesting for some permission
244-
self.permission = DABPermission.objects.filter(api_slug=model_name).first()
245-
246-
if not self.permission:
247-
self.content_type = DABContentType.objects.filter(api_slug=model_name).first()
248-
if not self.content_type:
249-
raise NotFound(f'The slug {model_name} is not a valid permission or type identifier')
250-
else:
251-
# Access list will be all permissions for the given object
252-
self.content_type = self.permission.content_type
253-
254-
model_cls = self.content_type.model_class()
255-
if not issubclass(model_cls, RemoteObject):
256-
try:
257-
self.related_object = model_cls.objects.get(pk=object_id)
258-
except model_cls.DoesNotExist:
259-
raise NotFound
260-
else:
261-
self.related_object = model_cls(content_type=self.content_type, object_id=object_id)
262-
263-
try:
264-
if not self.request.user.has_obj_perm(self.related_object, 'view'):
265-
raise NotFound
266-
except RuntimeError:
267-
check_content_obj_permission(self.request.user, self.related_object)
268-
294+
self.permission = self.get_url_permission()
295+
self.content_type = self.get_url_content_type()
296+
self.related_object = self.get_url_obj()
297+
self.check_permission_to_object(self.related_object)
269298
return (self.permission, self.content_type, self.related_object)
270299

271300
def get_queryset(self):
@@ -315,22 +344,55 @@ class Meta:
315344

316345
return DynamicActorSerializer
317346

318-
def get_serializer_context(self):
319-
ctx = super().get_serializer_context()
320-
permission, ct, obj = self.get_data_from_url()
321-
322-
ctx.update(
323-
{
324-
"permission": permission,
325-
"related_object": obj,
326-
"content_type": ct,
327-
}
328-
)
329-
return ctx
330-
331347

332348
class TeamAccessViewSet(UserAccessViewSet):
333349
serializer_mixin = TeamAccessListMixin
334350

335351
def get_actor_model(self):
336352
return get_team_model()
353+
354+
355+
class UserAccessAssignmentViewSet(
356+
AccessURLMixin,
357+
AnsibleBaseDjangoAppApiView,
358+
mixins.ListModelMixin,
359+
GenericViewSet,
360+
):
361+
"""
362+
This gives drill-down information about the means of inheritance
363+
for all the permissions show in the higher-level view of the access list
364+
"""
365+
366+
serializer_class = UserAccessAssignmentSerializer
367+
368+
def get_url_actor(self):
369+
actor_pk = self.kwargs.get("actor_pk")
370+
actor_cls = self.get_actor_model()
371+
try:
372+
return actor_cls.objects.get(pk=actor_pk)
373+
except actor_cls.DoesNotExist:
374+
raise NotFound(f'The {actor_cls._meta.model_name} with pk={actor_pk} can not be found')
375+
376+
def get_data_from_url(self):
377+
if not hasattr(self, 'related_object'):
378+
self.permission = self.get_url_permission()
379+
self.content_type = self.get_url_content_type()
380+
self.related_object = self.get_url_obj()
381+
self.actor = self.get_url_actor()
382+
self.check_permission_to_object(self.related_object)
383+
return (self.permission, self.content_type, self.related_object, self.actor)
384+
385+
def get_queryset(self):
386+
permission, ct, obj, actor = self.get_data_from_url()
387+
388+
if permission:
389+
return assignment_qs_user_to_obj_perm(actor, obj, permission)
390+
else:
391+
return assignment_qs_user_to_obj(actor, obj)
392+
393+
394+
class TeamAccessAssignmentViewSet(UserAccessAssignmentViewSet):
395+
serializer_class = TeamAccessAssignmentSerializer
396+
397+
def get_actor_model(self):
398+
return get_team_model()

ansible_base/rbac/urls.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
from django.urls import include, path
22

33
from ansible_base.rbac.api.router import router
4-
from ansible_base.rbac.api.views import RoleMetadataView, TeamAccessViewSet, UserAccessViewSet
4+
from ansible_base.rbac.api.views import RoleMetadataView, TeamAccessAssignmentViewSet, TeamAccessViewSet, UserAccessAssignmentViewSet, UserAccessViewSet
55
from ansible_base.rbac.apps import AnsibleRBACConfig
66

77
app_name = AnsibleRBACConfig.label
88

99
user_access_view = UserAccessViewSet.as_view({'get': 'list'})
1010
team_access_view = TeamAccessViewSet.as_view({'get': 'list'})
11+
user_access_assignment_view = UserAccessAssignmentViewSet.as_view({'get': 'list'})
12+
team_access_assignment_view = TeamAccessAssignmentViewSet.as_view({'get': 'list'})
1113

1214
api_version_urls = [
1315
path('', include(router.urls)),
1416
path(r'role_metadata/', RoleMetadataView.as_view(), name="role-metadata"),
1517
path('role_user_access/<str:model_name>/<int:pk>/', user_access_view, name="role-user-access"),
1618
path('role_team_access/<str:model_name>/<int:pk>/', team_access_view, name="role-team-access"),
19+
path(
20+
'role_user_access/<str:model_name>/<int:pk>/<str:actor_pk>/',
21+
user_access_assignment_view,
22+
name='role-user-access-assignments',
23+
),
24+
path(
25+
'role_team_access/<str:model_name>/<int:pk>/<str:actor_pk>/',
26+
team_access_assignment_view,
27+
name='role-team-access-assignments',
28+
),
1729
]
1830

1931
root_urls = []

0 commit comments

Comments
 (0)