diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py index 1179bc5ec2ece1c57719aba222b74036f746f85a..3e68aeba65f43de96852b093d3c05f289fb4329f 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py @@ -8,7 +8,6 @@ from .. import serializers from .lofar_viewset import LOFARViewSet from lofar.sas.tmss.tmss.exceptions import * from django.core.exceptions import ObjectDoesNotExist -from rest_framework.permissions import DjangoModelPermissions import logging logger = logging.getLogger(__name__) logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) @@ -36,7 +35,6 @@ class ProjectPermissionViewSet(LOFARViewSet): # A. apply a filter to prevent object without permission to be included in the list with full details, or # B. customize get_queryset on the view to call check_object_permissions. - def get_project_roles_for_user(user): # todo: this set of project/role pairs needs to be provided by the OIDC federation and will probably enter TMSS @@ -53,7 +51,7 @@ def get_project_roles_for_user(user): if user == models.User.objects.get(username='paulus'): return ({'project': 'test_user_is_shared_support', 'role': 'shared_support_user'}, {'project': 'test_user_is_contact', 'role': 'contact_author'}, - {'project': 'high', 'role': 'contact_author'}) + {'project': 'high', 'role': 'shared_support_user'}) except: pass @@ -84,10 +82,12 @@ class IsProjectMember(drf_permissions.DjangoObjectPermissions): # we always have permission as superuser (e.g. in test environment, where a regular user is created to test permission specifically) if request.user.is_superuser: logger.info("IsProjectMember: User=%s is superuser. Not enforcing project permissions!" % request.user) + logger.info('### IsProjectMember.has_object_permission %s %s True' % (request._request, request.method)) return True - # todo: do we want to marshal permissions for that as well? Then we add it to the ProjectPermission model, but it seems cumbersome...? + # todo: do we want to restrict access for that as well? Then we add it to the ProjectPermission model, but it seems cumbersome...? if request.method == 'OPTIONS': + logger.info('### IsProjectMember.has_object_permission %s %s True' % (request._request, request.method)) return True # determine which roles are allowed to access this object... @@ -112,9 +112,11 @@ class IsProjectMember(drf_permissions.DjangoObjectPermissions): if project_role['project'] == obj.project.name and \ models.ProjectRole.objects.get(value=project_role['role']) in permitted_project_roles: logger.info('user=%s is permitted to access object=%s' % (request.user, obj)) + logger.info('### IsProjectMember.has_object_permission %s %s True' % (request._request, request.method)) return True logger.info('User=%s is not permitted to access object=%s with related project=%s since it requires one of project_roles=%s' % (request.user, obj, obj.project, permitted_project_roles)) + logger.info('### IsProjectMember.has_object_permission %s False' % (request._request)) return False def has_permission(self, request, view): @@ -135,11 +137,18 @@ class IsProjectMember(drf_permissions.DjangoObjectPermissions): if attr == 'project': # has_object_permission checks the project from obj, so we can just check project permission on # something that has the correct project attribute - return self.has_object_permission(request, view, obj) + p=self.has_object_permission(request, view, obj) + logger.info('### IsProjectMember.has_permission %s %s' % (request._request, p)) + return p obj = getattr(obj, attr) - else: - logger.debug('allowing empty POST or non-POST request %s %s' % (request.method, view.action)) - return True # has_object_permission is called on get detail nonetheless + if view.action == 'list': + logger.info('Allowing list action %s %s' % (request.method, view.action)) + logger.info('### IsProjectMember.has_permission %s %s True' % (request._request, request.method)) + return True + + logger.info('Allowing empty POST or non-POST request %s %s' % (request.method, view.action)) + logger.info('### IsProjectMember.has_permission %s %s True' % (request._request, request.method)) + return True class IsProjectMemberOrReadOnly(IsProjectMember): @@ -159,6 +168,43 @@ class IsProjectMemberOrReadOnly(IsProjectMember): return super().has_object_permission(request, view, obj) +class TMSSDjangoModelPermissions(drf_permissions.DjangoModelPermissions): + """ + Modify the vanilla DjangoModelPermissions, which are apparently readonly by default + """ + view_permissions = ['%(app_label)s.view_%(model_name)s'] + perms_map = { + 'GET': view_permissions, + 'OPTIONS': view_permissions, + 'HEAD': view_permissions, + 'POST': drf_permissions.DjangoModelPermissions.perms_map['POST'], + 'PUT': drf_permissions.DjangoModelPermissions.perms_map['PUT'], + 'PATCH': drf_permissions.DjangoModelPermissions.perms_map['PATCH'], + 'DELETE': drf_permissions.DjangoModelPermissions.perms_map['DELETE'], + } + + def has_permission(self, request, view): + p = super().has_permission(request, view) + logger.info('### TMSSDjangoModelPermissions.has_permission %s %s' % (request._request, p)) + return p + + +class TMSSPermissions(drf_permissions.DjangoObjectPermissions): + """ + Create custom permission class + Note: required because the composition using & and | in the permission_classes does not seem to work as it should. + """ + model_permissions = TMSSDjangoModelPermissions() + project_permissions = IsProjectMember() + + def has_permission(self, request, view): + return (self.model_permissions.has_permission(request, view) or + self.project_permissions.has_permission(request, view)) and request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + return IsProjectMember().has_object_permission(request, view, obj) and request.user.is_authenticated + + # # Custom Filters # diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py index 9df5c7644524908c25ad74f67972df38044bdae7..55333fa71af5021092ccbae465e89b1c9d06a8dc 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py @@ -13,14 +13,14 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import permission_classes -from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions +from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import action from drf_yasg.utils import swagger_auto_schema from drf_yasg.openapi import Parameter from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOFARNestedViewSet, AbstractTemplateViewSet, LOFARCopyViewSet -from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import IsProjectMemberFilterBackend, IsProjectMember, IsProjectMemberOrReadOnly +from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import IsProjectMemberFilterBackend, IsProjectMember, IsProjectMemberOrReadOnly, TMSSDjangoModelPermissions, TMSSPermissions from lofar.sas.tmss.tmss.tmssapp import models from lofar.sas.tmss.tmss.tmssapp import serializers from django.http import JsonResponse @@ -231,7 +231,7 @@ class TaskConnectorTypeViewSet(LOFARViewSet): serializer_class = serializers.TaskConnectorTypeSerializer -@permission_classes((DjangoModelPermissions,)) +@permission_classes((TMSSDjangoModelPermissions,)) class CycleViewSet(LOFARViewSet): queryset = models.Cycle.objects.all() serializer_class = serializers.CycleSerializer @@ -253,7 +253,7 @@ class CycleQuotaViewSet(LOFARViewSet): return queryset -@permission_classes((DjangoModelPermissions,)) +@permission_classes((TMSSDjangoModelPermissions,)) class ProjectViewSet(LOFARViewSet): queryset = models.Project.objects.all() serializer_class = serializers.ProjectSerializer @@ -270,7 +270,7 @@ class ProjectViewSet(LOFARViewSet): return queryset -@permission_classes((DjangoModelPermissions,)) +@permission_classes((TMSSDjangoModelPermissions,)) class ProjectNestedViewSet(LOFARNestedViewSet): queryset = models.Project.objects.all() serializer_class = serializers.ProjectSerializer @@ -714,8 +714,8 @@ class SchedulingUnitBlueprintNestedViewSet(LOFARNestedViewSet): class TaskDraftViewSet(LOFARViewSet): queryset = models.TaskDraft.objects.all() serializer_class = serializers.TaskDraftSerializer - permission_classes = (IsProjectMember, IsAuthenticated) - filter_backends = (IsProjectMemberFilterBackend,) + permission_classes = (TMSSPermissions,) # todo: move to LOFARViewSet eventually? # Note: this should work the same, but something funny is going on: [IsProjectMember | TMSSDjangoModelPermissions] + filter_backends = (IsProjectMemberFilterBackend,) # todo: move to LOFARViewSet eventually? # prefetch all reverse related references from other models on their related_name to avoid a ton of duplicate queries queryset = queryset.prefetch_related('first_scheduling_relation') \ @@ -755,7 +755,7 @@ class TaskDraftViewSet(LOFARViewSet): # todo: it seems there is no way to pass IsProjectMember as a permission class to actions without explicitly # registering them in the router for some reason. but explicitly doing a check_object_permission works as well. # We may want to extend get_object_or_404 to also perform a permission check before returning an object...? - self.check_object_permissions(self.request, task_draft) + self.check_object_permissions(self.request, task_draft) # or request.user.has_perm('create_task_blueprint') task_blueprint = create_task_blueprint_from_task_draft(task_draft) # url path magic to construct the new task_blueprint_path url diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py index 1ba4c5d30de2d0d93c3757f133f7df8bba6e5ab2..55518e6f348f1e37fa96b12c604b41fceb55d7dc 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py @@ -2986,7 +2986,6 @@ class SystemRolePermissionTestCase(unittest.TestCase): # Cycle - @unittest.skip('Fix this test. Why can we view this without permission?') # todo! def test_Cycle_cannot_be_viewed_without_scientist_group(self): user = User.objects.get(username='paulus') user.groups.set([]) @@ -3069,7 +3068,6 @@ class SystemRolePermissionTestCase(unittest.TestCase): # Project - @unittest.skip('Fix this test. Why can we view this without permission?') # todo! def test_Project_cannot_be_viewed_without_scientist_group(self): user = User.objects.get(username='paulus') user.groups.set([]) diff --git a/SAS/TMSS/test/test_utils.py b/SAS/TMSS/test/test_utils.py index 3cc7daa0cb7645e5fc7ed8c010aadf5d991e6a87..e1c0aec9040c2bd9d814469733fb13e1b2e69034 100644 --- a/SAS/TMSS/test/test_utils.py +++ b/SAS/TMSS/test/test_utils.py @@ -562,7 +562,7 @@ def main_test_environment(): parser.add_option_group(group) group.add_option('-d', '--data', dest='data', action='store_true', help='populate the test-database with test/example data. This implies -s/--schemas because these schemas are needed to create test data.') group.add_option('-s', '--schemas', dest='schemas', action='store_true', help='populate the test-database with the TMSS JSON schemas') - group.add_option('-p', '--permissions', dest='permissions', action='store_true', help='populate the test-database with the TMSS permissions') + group.add_option('-M', '--permissions', dest='permissions', action='store_true', help='populate the test-database with the TMSS permissions') group.add_option('-m', '--eventmessages', dest='eventmessages', action='store_true', help='Send event messages over the messagebus for changes in the TMSS database (for (sub)tasks/scheduling_units etc).') group.add_option('-r', '--ra_test_environment', dest='ra_test_environment', action='store_true', help='start the Resource Assigner test environment which enables scheduling.') group.add_option('-S', '--scheduling', dest='scheduling', action='store_true', help='start the TMSS background scheduling services for dynamic scheduling of schedulingunits and subtask scheduling of chains of dependend subtasks.')