Skip to content

Commit 495ef82

Browse files
committed
First pass at rest of views
1 parent 9c5cdcc commit 495ef82

File tree

7 files changed

+291
-10
lines changed

7 files changed

+291
-10
lines changed

ansible_base/lib/dynamic_config/settings_logic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def get_mergeable_dab_settings(settings: dict) -> dict: # NOSONAR
108108
'user_ansible_id',
109109
'team_ansible_id',
110110
'object_ansible_id',
111+
'assignment', # for RoleAssignmentFilterBackend, assignment filtering
111112
)
112113

113114
# SPECTACULAR SETTINGS

ansible_base/rbac/api/serializers.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from rest_framework.serializers import ValidationError
99

1010
from ansible_base.lib.abstract_models.common import get_url_for_object
11-
from ansible_base.lib.serializers.common import CommonModelSerializer, ImmutableCommonModelSerializer
11+
from ansible_base.lib.serializers.common import AbstractCommonModelSerializer, CommonModelSerializer, ImmutableCommonModelSerializer
12+
from ansible_base.lib.utils.auth import get_team_model
1213
from ansible_base.rbac.models import RoleDefinition, RoleTeamAssignment, RoleUserAssignment
1314
from ansible_base.rbac.permission_registry import permission_registry # careful for circular imports
1415
from ansible_base.rbac.policies import check_content_obj_permission, visible_users
1516
from ansible_base.rbac.validators import check_locally_managed, validate_permissions_for_model
1617

17-
from ..models import DABContentType, DABPermission
18+
from ..models import DABContentType, DABPermission, get_evaluation_model
1819

1920

2021
class RoleDefinitionSerializer(CommonModelSerializer):
@@ -241,3 +242,56 @@ def get_actor_queryset(self, requesting_user):
241242

242243
class RoleMetadataSerializer(serializers.Serializer):
243244
allowed_permissions = serializers.DictField(help_text=_('A List of permissions allowed for a role definition, given its content type.'))
245+
246+
247+
class AccessListMixin(AbstractCommonModelSerializer):
248+
role_assignments = serializers.SerializerMethodField()
249+
250+
@staticmethod
251+
def summarize_role_definition(role_definition):
252+
return {"name": role_definition.name, "url": get_url_for_object(role_definition)}
253+
254+
def get_role_assignments(self, actor):
255+
obj = self.context.get("related_object")
256+
permission = self.context.get("permission")
257+
ct = self.context.get("content_type")
258+
259+
assignment_list = []
260+
261+
evaluation_cls = get_evaluation_model(obj)
262+
reverse_name = evaluation_cls._meta.get_field('role').remote_field.name
263+
if permission:
264+
obj_eval_qs = evaluation_cls.objects.filter(codename=permission.codename, object_id=obj.pk, content_type_id=ct.id)
265+
else:
266+
# All relevant assignments for the object
267+
obj_eval_qs = evaluation_cls.objects.filter(object_id=obj.pk, content_type_id=ct.id)
268+
obj_assignment_qs = actor.role_assignments.filter(**{f'object_role__{reverse_name}__in': obj_eval_qs})
269+
270+
team_ct = DABContentType.objects.get_for_model(get_team_model())
271+
272+
for assignment in obj_assignment_qs.distinct():
273+
if assignment.content_type_id == team_ct.pk:
274+
perm_type = "team"
275+
elif assignment.content_type_id == ct.pk:
276+
perm_type = "direct"
277+
else:
278+
perm_type = "indirect"
279+
assignment_list.append({"type": perm_type, "role_definition": self.summarize_role_definition(assignment.role_definition)})
280+
281+
if permission:
282+
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions=permission)
283+
else:
284+
global_assignment_qs = actor.role_assignments.filter(content_type=None, role_definition__permissions__content_type=ct)
285+
286+
for assignment in global_assignment_qs.distinct():
287+
assignment_list.append({"type": "global", "role_definition": self.summarize_role_definition(assignment.role_definition)})
288+
289+
return assignment_list
290+
291+
292+
class UserAccessListMixin(AccessListMixin):
293+
_expected_fields = ['username', 'role_assignments']
294+
295+
296+
class TeamAccessListMixin(AccessListMixin):
297+
_expected_fields = ['name', 'organization', 'role_assignments']

ansible_base/rbac/api/views.py

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from django.db.models import Model
66
from django.utils.translation import gettext_lazy as _
77
from rest_framework import permissions
8-
from rest_framework.exceptions import ValidationError
8+
from rest_framework.exceptions import NotFound, ValidationError
99
from rest_framework.generics import GenericAPIView
1010
from rest_framework.response import Response
11-
from rest_framework.viewsets import ModelViewSet
11+
from rest_framework.viewsets import GenericViewSet, ModelViewSet, mixins
1212

13+
from ansible_base.lib.utils.auth import get_team_model, get_user_model
1314
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
1415
from ansible_base.lib.utils.views.permissions import try_add_oauth2_scope_permission
1516
from ansible_base.rbac.api.permissions import RoleDefinitionPermissions
@@ -19,16 +20,18 @@
1920
RoleMetadataSerializer,
2021
RoleTeamAssignmentSerializer,
2122
RoleUserAssignmentSerializer,
23+
TeamAccessListMixin,
24+
UserAccessListMixin,
2225
)
2326
from ansible_base.rbac.evaluations import has_super_permission
2427
from ansible_base.rbac.models import RoleDefinition
2528
from ansible_base.rbac.permission_registry import permission_registry
2629
from ansible_base.rbac.policies import check_can_remove_assignment
2730
from ansible_base.rbac.validators import check_locally_managed, permissions_allowed_for_role, system_roles_enabled
28-
from ansible_base.rest_filters.rest_framework.ansible_id_backend import TeamAnsibleIdAliasFilterBackend, UserAnsibleIdAliasFilterBackend
31+
from ansible_base.rest_filters.rest_framework import ansible_id_backend
2932

30-
from ..models.content_type import DABContentType
31-
from ..remote import get_resource_prefix
33+
from ..models import DABContentType, DABPermission, get_evaluation_model
34+
from ..remote import RemoteObject, get_resource_prefix
3235

3336

3437
def list_combine_values(data: dict[Type[Model], list[str]]) -> list[str]:
@@ -174,7 +177,8 @@ class RoleTeamAssignmentViewSet(BaseAssignmentViewSet):
174177
serializer_class = RoleTeamAssignmentSerializer
175178
prefetch_related = ('team',)
176179
filter_backends = BaseAssignmentViewSet.filter_backends + [
177-
TeamAnsibleIdAliasFilterBackend,
180+
ansible_id_backend.TeamAnsibleIdAliasFilterBackend,
181+
ansible_id_backend.RoleAssignmentFilterBackend,
178182
]
179183

180184

@@ -193,5 +197,108 @@ class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
193197
serializer_class = RoleUserAssignmentSerializer
194198
prefetch_related = ('user',)
195199
filter_backends = BaseAssignmentViewSet.filter_backends + [
196-
UserAnsibleIdAliasFilterBackend,
200+
ansible_id_backend.UserAnsibleIdAliasFilterBackend,
201+
ansible_id_backend.RoleAssignmentFilterBackend,
197202
]
203+
204+
205+
class UserAccessViewSet(
206+
AnsibleBaseDjangoAppApiView,
207+
mixins.ListModelMixin,
208+
GenericViewSet,
209+
):
210+
"""
211+
Use this endpoint to get a list of users who have access to a resource.
212+
This is a list-only view that provides a list of users, plus extra data.
213+
"""
214+
215+
serializer_mixin = UserAccessListMixin
216+
217+
def get_actor_model(self):
218+
return get_user_model()
219+
220+
def get_data_from_url(self):
221+
if not hasattr(self, 'related_object'):
222+
model_name = self.kwargs.get("model_name")
223+
object_id = self.kwargs.get("pk")
224+
225+
# Prefer treating the URL as requesting for some permission
226+
self.permission = DABPermission.objects.filter(api_slug=model_name).first()
227+
228+
if not self.permission:
229+
self.content_type = DABContentType.objects.filter(api_slug=model_name).first()
230+
if not self.content_type:
231+
raise NotFound(f'The slug {model_name} is not a valid permission or type identifier')
232+
else:
233+
# Access list will be all permissions for the given object
234+
self.content_type = self.permission.content_type
235+
236+
model_cls = self.content_type.model_class()
237+
if not issubclass(model_cls, RemoteObject):
238+
try:
239+
self.related_object = model_cls.objects.get(pk=object_id)
240+
except model_cls.DoesNotExist:
241+
raise NotFound
242+
else:
243+
self.related_object = model_cls(content_type=self.content_type, object_id=object_id)
244+
245+
if not self.request.user.has_obj_perm(self.related_object, 'view'):
246+
raise NotFound
247+
248+
return (self.permission, self.content_type, self.related_object)
249+
250+
def get_queryset(self):
251+
permission, ct, obj = self.get_data_from_url()
252+
253+
evaluation_cls = get_evaluation_model(obj)
254+
reverse_name = evaluation_cls._meta.get_field('role').remote_field.name
255+
actor_cls = self.get_actor_model()
256+
assignment_cls = actor_cls._meta.get_field('role_assignments').related_model
257+
258+
if permission:
259+
obj_eval_qs = evaluation_cls.objects.filter(codename=permission.codename, object_id=obj.pk, content_type_id=ct.id)
260+
else:
261+
# All relevant evaluations for the object
262+
obj_eval_qs = evaluation_cls.objects.filter(object_id=obj.pk, content_type_id=ct.id)
263+
obj_assignment_qs = assignment_cls.objects.filter(**{f'object_role__{reverse_name}__in': obj_eval_qs})
264+
265+
if permission:
266+
global_assignment_qs = assignment_cls.objects.filter(content_type=None, role_definition__permissions=permission)
267+
else:
268+
global_assignment_qs = assignment_cls.objects.filter(content_type=None, role_definition__permissions__content_type=ct)
269+
270+
assignment_qs = obj_assignment_qs | global_assignment_qs
271+
actor_qs = actor_cls.objects.filter(role_assignments__in=assignment_qs)
272+
if actor_cls._meta.model_name == 'user':
273+
actor_qs |= actor_qs.filter(is_superuser=True)
274+
return actor_qs
275+
276+
def get_serializer_class(self):
277+
actor_cls = self.get_actor_model()
278+
279+
class DynamicActorSerializer(self.serializer_mixin):
280+
class Meta:
281+
model = actor_cls
282+
fields = self.serializer_mixin.Meta.fields + self.serializer_mixin._expected_fields
283+
284+
return DynamicActorSerializer
285+
286+
def get_serializer_context(self):
287+
ctx = super().get_serializer_context()
288+
permission, ct, obj = self.get_data_from_url()
289+
290+
ctx.update(
291+
{
292+
"permission": permission,
293+
"related_object": obj,
294+
"content_type": ct,
295+
}
296+
)
297+
return ctx
298+
299+
300+
class TeamAccessViewSet(UserAccessViewSet):
301+
serializer_mixin = TeamAccessListMixin
302+
303+
def get_actor_model(self):
304+
return get_team_model()

ansible_base/rbac/service_api/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.apps import apps
12
from rest_framework import serializers
23

34
from ..models import DABContentType, DABPermission, RoleTeamAssignment, RoleUserAssignment
@@ -28,6 +29,8 @@ class BaseAssignmentSerializer(serializers.ModelSerializer):
2829
role_definition = serializers.SlugRelatedField(read_only=True, slug_field='name')
2930
created_by_ansible_id = serializers.SerializerMethodField()
3031
object_ansible_id = serializers.SerializerMethodField()
32+
# TODO: use the from_service to control what we sync back to
33+
from_service = serializers.CharField(write_only=True)
3134

3235
def get_created_by_ansible_id(self, obj):
3336
return str(obj.created_by.resource.ansible_id)
@@ -40,9 +43,20 @@ def get_object_ansible_id(self, obj):
4043
return str(content_object.resource.ansible_id)
4144
return None
4245

46+
def find_existing_assignment(self, queryset):
47+
actor_ansible_id = self.validated_data[f'{self.actor_field}_ansible_id']
48+
object_id = self.validated_data['object_id']
49+
role_definition = self.validated_data['role_definition']
50+
51+
resource_cls = apps.get_model('dab_resource_registry', 'Resource')
52+
actor_resource = resource_cls.objects.get(ansible_id=actor_ansible_id)
53+
actor = actor_resource.content_object
54+
return queryset.filter(object_id=object_id, role_definition=role_definition, **{self.actor_field: actor}).first()
55+
4356

4457
class RoleUserAssignmentSerializer(BaseAssignmentSerializer):
4558
user_ansible_id = serializers.SerializerMethodField()
59+
actor_field = 'user'
4660

4761
class Meta:
4862
model = RoleUserAssignment
@@ -54,6 +68,7 @@ def get_user_ansible_id(self, obj):
5468

5569
class RoleTeamAssignmentSerializer(BaseAssignmentSerializer):
5670
user_ansible_id = serializers.SerializerMethodField()
71+
actor_field = 'team'
5772

5873
class Meta:
5974
model = RoleTeamAssignment

ansible_base/rbac/service_api/views.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from rest_framework import status
2+
from rest_framework.decorators import action
3+
from rest_framework.response import Response
14
from rest_framework.viewsets import GenericViewSet, mixins
25

36
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
7+
from ansible_base.rest_filters.rest_framework import ansible_id_backend
48

59
from ..models import DABContentType, DABPermission, RoleTeamAssignment, RoleUserAssignment
610
from . import serializers as service_serializers
@@ -43,6 +47,42 @@ class ServiceRoleUserAssignmentViewSet(
4347

4448
queryset = RoleUserAssignment.objects.prefetch_related('user__resource', *prefetch_related)
4549
serializer_class = service_serializers.RoleUserAssignmentSerializer
50+
filter_backends = AnsibleBaseDjangoAppApiView.filter_backends + [
51+
ansible_id_backend.UserAnsibleIdAliasFilterBackend,
52+
ansible_id_backend.RoleAssignmentFilterBackend,
53+
]
54+
55+
@action(detail=False, methods=['post'], url_path='assign')
56+
def assign(self, request):
57+
serializer = self.get_serializer(data=request.data)
58+
serializer.is_valid(raise_exception=True)
59+
60+
existing = serializer.find_existing_assignment(self.get_queryset())
61+
if existing:
62+
return Response(
63+
{"detail": "This assignment already exists."},
64+
status=status.HTTP_409_CONFLICT,
65+
)
66+
67+
instance = serializer.save()
68+
output_serializer = self.get_serializer(instance)
69+
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
70+
71+
@action(detail=False, methods=['post'], url_path='unassign')
72+
def unassign(self, request):
73+
serializer = self.get_serializer(data=request.data)
74+
serializer.is_valid(raise_exception=True)
75+
76+
existing = serializer.find_existing_assignment(self.get_queryset())
77+
if not existing:
78+
return Response(
79+
{"detail": "No such assignment exists."},
80+
status=status.HTTP_409_CONFLICT,
81+
)
82+
83+
# Use standard DRF delete logic
84+
self.perform_destroy(existing)
85+
return Response(status=status.HTTP_204_NO_CONTENT)
4686

4787

4888
class ServiceRoleTeamAssignmentViewSet(
@@ -54,3 +94,7 @@ class ServiceRoleTeamAssignmentViewSet(
5494

5595
queryset = RoleTeamAssignment.objects.prefetch_related('team__resource', *prefetch_related)
5696
serializer_class = service_serializers.RoleTeamAssignmentSerializer
97+
filter_backends = AnsibleBaseDjangoAppApiView.filter_backends + [
98+
ansible_id_backend.TeamAnsibleIdAliasFilterBackend,
99+
ansible_id_backend.RoleAssignmentFilterBackend,
100+
]

ansible_base/rbac/urls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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
4+
from ansible_base.rbac.api.views import RoleMetadataView, TeamAccessViewSet, UserAccessViewSet
55
from ansible_base.rbac.apps import AnsibleRBACConfig
66

77
from .service_api.router import service_router
@@ -13,10 +13,15 @@
1313
path('', include(service_router.urls)),
1414
]
1515

16+
user_access_view = UserAccessViewSet.as_view({'get': 'list'})
17+
team_access_view = TeamAccessViewSet.as_view({'get': 'list'})
18+
1619
api_version_urls = [
1720
path('', include(router.urls)),
1821
path('service-index/', include(service_urls)),
1922
path(r'role_metadata/', RoleMetadataView.as_view(), name="role-metadata"),
23+
path('role_user_access/<str:model_name>/<int:pk>/', user_access_view, name="role-user-access"),
24+
path('role_team_access/<str:model_name>/<int:pk>/', team_access_view, name="role-team-access"),
2025
]
2126

2227
root_urls = []

0 commit comments

Comments
 (0)