diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py index dc4f639d8e8020613a9c6224bd53a5d96dc8459a..845fb9466ffe3d87a049f4eaf9307ebab95459c8 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py @@ -5,7 +5,7 @@ This file contains permissions and filters that are used in the viewsets from rest_framework import permissions as drf_permissions, filters as drf_filters from .. import models from lofar.sas.tmss.tmss.exceptions import * -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, FieldError import logging logger = logging.getLogger(__name__) logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) @@ -40,20 +40,27 @@ def get_project_roles_for_user(user): # todo: remove these test users when not needed any more (required for local frontend testing where Keycloak is unavailable) if user == User.objects.get(username='tmss_friend'): return ({'project': 'high', 'role': 'friend_of_project'}, - {'project': 'high', 'role': 'friend_of_project_primary'}) + {'project': 'high', 'role': 'friend_of_project_primary'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='tmss_friend2'): - return ({'project': 'high', 'role': 'friend_of_project'},) + return ({'project': 'high', 'role': 'friend_of_project'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='tmss_contact'): - return ({'project': 'high', 'role': 'contact'},) + return ({'project': 'high', 'role': 'contact'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='tmss_PI'): - return ({'project': 'high', 'role': 'pi'},) + return ({'project': 'high', 'role': 'pi'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='tmss_shared_support'): return ({'project': 'high', 'role': 'shared_support'}, - {'project': 'high', 'role': 'shared_support_primary'}) + {'project': 'high', 'role': 'shared_support_primary'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='tmss_shared_support2'): - return ({'project': 'high', 'role': 'shared_support'},) + return ({'project': 'high', 'role': 'shared_support'}, + {'project': 'high', 'role': 'co_i'}) if user == User.objects.get(username='scientist'): - return ({'project': 'high', 'role': 'co_i'},) + return ({'project': 'high', 'role': 'co_i'}, + {'project': 'high', 'role': 'co_i'}) try: return tuple(user.project_roles) @@ -146,7 +153,12 @@ class IsProjectMember(drf_permissions.DjangoObjectPermissions): # We need to check the project name before the object is created, but project is an object property. # Turning the path to the project into a (static) model attribute, allows us to use it both in the object # property as well as here (where we did not create an object yet). - if view.action == 'create' and request.data: + # Note: the Viewflow actions are currently create actions but they do not actually create anything + # (and do not contain the expected data to create an instance of the underlying model, but instead e.g. the + # data required to perform a workflow step). So we exclude the offending views here. + if view.action == 'create' and request.data \ + and not 'SchedulingUnitTaskExecuteViewSet' in str(view) \ + and not 'SchedulingUnitTaskAssignViewSet' in str(view): obj = None if view.serializer_class.Meta.model == models.Project: return False # project creation solely depends on system role @@ -298,8 +310,18 @@ class IsProjectMemberFilterBackend(drf_filters.BaseFilterBackend): return permitted_projects if hasattr(view.serializer_class.Meta.model, 'path_to_project'): - return queryset.filter(**{f"{view.serializer_class.Meta.model.path_to_project}__in": permitted_projects}) - + path_to_project = view.serializer_class.Meta.model.path_to_project + try: + return queryset.filter(**{f"{path_to_project}__in": permitted_projects}) + except FieldError as e: + # this is expected for models.schedulingunitflow.SchedulingUnitTask + logger.warning('Cannot perform in-db filtering for project permissions on model=%s with path_to_project=%s: %s -- applying slow filtering based on project property.' % (view.serializer_class.Meta.model, path_to_project, e)) + ids = [] + for candidate in queryset.all(): + if candidate.project in permitted_projects: + ids.append(candidate.id) + return queryset.filter(id__in=ids) + if hasattr(view.serializer_class.Meta.model, 'project'): return queryset.filter(project__in=permitted_projects) diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index d155c5f2b918443c525b0c98862fa0e0cc9c8793..d160af6ac6551cc1f17f52e62c9d99db7e09cde4 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -280,10 +280,10 @@ if isViewflowEnabled(): viewflow_router.register('scheduling_unit_flow/qa_decide_acceptance', workflow_viewsets.DecideAcceptanceViewSet, basename='qa_decide_acceptance') viewflow_router.register('scheduling_unit_flow/qa_scheduling_unit_process', workflow_viewsets.SchedulingUnitProcessViewSet, basename='qa_scheduling_unit_process') viewflow_router.register('scheduling_unit_flow/qa_scheduling_unit_task', workflow_viewsets.SchedulingUnitTaskViewSet, basename='qa_scheduling_unit_task') - viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_task/(?P<qa_scheduling_unit_task_id>\d+)/assign', workflow_viewsets.SchedulingUnitTaskAssignViewSet) - viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_task/(?P<qa_scheduling_unit_task_id>\d+)/unassign', workflow_viewsets.SchedulingUnitTaskUnassignViewSet) - viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_process/(?P<qa_scheduling_unit_process_id>\d+)/current_task', workflow_viewsets.SchedulingUnitGetActiveTasksViewSet) - viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_process/(?P<qa_scheduling_unit_process_id>\d+)/perform', workflow_viewsets.SchedulingUnitTaskExecuteViewSet) + viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_task/(?P<pk>\d+)/assign', workflow_viewsets.SchedulingUnitTaskAssignViewSet) + viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_task/(?P<pk>\d+)/unassign', workflow_viewsets.SchedulingUnitTaskUnassignViewSet) + viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_process/(?P<pk>\d+)/current_task', workflow_viewsets.SchedulingUnitGetActiveTasksViewSet) + viewflow_router.register(r'scheduling_unit_flow/qa_scheduling_unit_process/(?P<pk>\d+)/perform', workflow_viewsets.SchedulingUnitTaskExecuteViewSet) viewflow_urlpatterns.extend(viewflow_router.urls) diff --git a/SAS/TMSS/backend/src/tmss/workflowapp/flows/schedulingunitflow.py b/SAS/TMSS/backend/src/tmss/workflowapp/flows/schedulingunitflow.py index c523b539ce414cb1667c3aabe813228c3010d7cd..e344b40c39848d3562b7e34bfe9a0f04bf34ab36 100644 --- a/SAS/TMSS/backend/src/tmss/workflowapp/flows/schedulingunitflow.py +++ b/SAS/TMSS/backend/src/tmss/workflowapp/flows/schedulingunitflow.py @@ -323,10 +323,11 @@ class SchedulingUnitFlow(Flow): def signal_SUB_allow_ingest(self, activation): logger.info("granting ingest permission for scheduling unit blueprint id=%s", activation.process.su.id) - activation.process.su.ingest_permission_granted_since = round_to_second_precision(datetime.utcnow()) - activation.process.su.ingest_permission_required = True + if activation.process.su.ingest_permission_granted_since is None: + activation.process.su.ingest_permission_granted_since = round_to_second_precision(datetime.utcnow()) + else: + logger.warning("ingest permission on scheduling unit blueprint id=%s has already been granted on %s" % (activation.process.su.id, activation.process.su.ingest_permission_granted_since)) activation.process.su.save() - activation.process.save() def check_SchedulingUnitBlueprint_status_or_beyond(self, activation, scheduling_unit: models.SchedulingUnitBlueprint, expected_status): diff --git a/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py b/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py index 4c05a91512d7c67c0d418b22c88b9361ae500f20..c1e0217b976ee691c93e8c13b4f0727bcbfb0bf9 100644 --- a/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py +++ b/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py @@ -11,29 +11,29 @@ from lofar.sas.tmss.tmss.tmssapp.models.common import ProjectPropertyMixin class QAReportingTO(Model, ProjectPropertyMixin): operator_report = TextField() operator_accept = BooleanField(default=False) - path_to_project = 'flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' class QAReportingSOS(Model, ProjectPropertyMixin): sos_report = TextField() quality_within_policy = BooleanField(default=False) sos_accept_show_pi = BooleanField(default=False) - path_to_project = 'flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' class PIVerification(Model, ProjectPropertyMixin): pi_report = TextField() pi_accept = BooleanField(default=False) - path_to_project = 'flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' class DecideAcceptance(Model, ProjectPropertyMixin): sos_accept_after_pi = BooleanField(default=False) - path_to_project = 'flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' class UnpinData(Model, ProjectPropertyMixin): unpin_data = BooleanField(default=False) - path_to_project = 'flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' class SchedulingUnitTask(Task, ProjectPropertyMixin): - path_to_project ='flow_process__su__draft__scheduling_set__project' # todo: currently not used, verify in TMSS-982 / TMSS-2054 + path_to_project = 'flow_process__su__draft__scheduling_set__project' # Note: flow_process is not a db field but a property here class SchedulingUnitProcess(Process, ProjectPropertyMixin): """ diff --git a/SAS/TMSS/backend/src/tmss/workflowapp/viewsets/schedulingunitflow.py b/SAS/TMSS/backend/src/tmss/workflowapp/viewsets/schedulingunitflow.py index 5bafd25a3044382b6f4336092dd8b20e0d46b996..618f07a4d34de378833a15dbfcc80d3d9053f9c5 100644 --- a/SAS/TMSS/backend/src/tmss/workflowapp/viewsets/schedulingunitflow.py +++ b/SAS/TMSS/backend/src/tmss/workflowapp/viewsets/schedulingunitflow.py @@ -133,6 +133,7 @@ class SchedulingUnitTaskViewSet(PreventSaveByUnassignedUsersMixin, TMSSFilterAnd class TMSSPermissionView(generic.CreateView): # todo: comment in to marshal permissions on the viewflow-internal views, if required. + # Or rather merge with TMSSPermissionGenericViewSet # -> TMSS-982 / TMSS-2054 # permission_classes = (TMSSPermissions,) pass @@ -279,7 +280,7 @@ class SchedulingUnitTaskAssignViewSet(mixins.CreateModelMixin, 422: 'error'}, operation_description="Assign a Scheduling Unit Task to an user") def create(self, request, *args, **kwargs): - if 'qa_scheduling_unit_task_id' in kwargs: + if 'pk' in kwargs: try: user_email = request.GET.get('user_email') if user_email: # For some reason this is GET data, even though it's a POST action... @@ -288,7 +289,7 @@ class SchedulingUnitTaskAssignViewSet(mixins.CreateModelMixin, return Response(content, status=status.HTTP_422_UNPROCESSABLE_ENTITY) user, _ = User.objects.get_or_create(email=user_email, defaults={'username': 'user_%s' % uuid.uuid4()}) # Users that log in via Keycloak are matched with Django users with same email address elif request.GET.get('project_role'): # For some reason this is GET data, even though it's a POST action... - task = models.SchedulingUnitTask.objects.filter(id=self.kwargs['qa_scheduling_unit_task_id'])[0] + task = models.SchedulingUnitTask.objects.filter(id=self.kwargs['pk'])[0] scheduling_unit_blueprint = task.flow_process.su project = scheduling_unit_blueprint.project.name users = get_users_by_role_in_project(role=request.GET.get('project_role'), project=project) @@ -304,7 +305,7 @@ class SchedulingUnitTaskAssignViewSet(mixins.CreateModelMixin, user, _ = User.objects.get_or_create(email=user_email, defaults={'username': 'user_%s' % uuid.uuid4()}) else: user = self.request.user - models.SchedulingUnitTask.objects.filter(id=self.kwargs['qa_scheduling_unit_task_id'])[0].activate().assign(user) + models.SchedulingUnitTask.objects.filter(id=self.kwargs['pk'])[0].activate().assign(user) content = {'Assigned': 'Scheduling Unit Task assigned to user=%s' % user.email} return Response(content, status=status.HTTP_200_OK) except AttributeError: @@ -329,9 +330,9 @@ class SchedulingUnitTaskUnassignViewSet(mixins.CreateModelMixin, 422: 'error'}, operation_description="Unassign a Scheduling Unit Task") def create(self, request, *args, **kwargs): - if 'qa_scheduling_unit_task_id' in kwargs: + if 'pk' in kwargs: try: - models.SchedulingUnitTask.objects.filter(id=self.kwargs['qa_scheduling_unit_task_id'])[0].activate().unassign() + models.SchedulingUnitTask.objects.filter(id=self.kwargs['pk'])[0].activate().unassign() content = {'Unassign': 'Scheduling Unit Task unassigned'} return Response(content, status=status.HTTP_200_OK) except AttributeError: @@ -354,10 +355,10 @@ class SchedulingUnitGetActiveTasksViewSet(mixins.CreateModelMixin, 403: 'forbidden', 422: 'error'}, operation_description="Get the list of active tasks.") - def create(self, request, *args, **kwargs): - if 'qa_scheduling_unit_process_id' in kwargs: + def create(self, request, *args, **kwargs): # todo: should be 'get' + if 'pk' in kwargs: try: - tasks = models.SchedulingUnitProcess.objects.filter(id=self.kwargs['qa_scheduling_unit_process_id'])[0].active_tasks() + tasks = models.SchedulingUnitProcess.objects.filter(id=self.kwargs['pk'])[0].active_tasks() return JsonResponse(list(tasks.values()), safe=False) except IndexError: content = {'Get Active Task(s)': 'No Process with the specified id'} @@ -377,14 +378,14 @@ class SchedulingUnitTaskExecuteViewSet(mixins.CreateModelMixin, @swagger_auto_schema(responses={200: '', 403: 'forbidden', 422: 'error'}, - operation_description="Unassign a Scheduling Unit Task") + operation_description="Perform a Scheduling Unit Task") def create(self, request, *args, **kwargs): - if 'qa_scheduling_unit_process_id' in kwargs: + if 'pk' in kwargs: try: - process= models.SchedulingUnitProcess.objects.get(pk=self.kwargs['qa_scheduling_unit_process_id']) - task = models.SchedulingUnitProcess.objects.get(pk=self.kwargs['qa_scheduling_unit_process_id']).active_tasks()[0] + process= models.SchedulingUnitProcess.objects.get(pk=self.kwargs['pk']) + task = models.SchedulingUnitProcess.objects.get(pk=self.kwargs['pk']).active_tasks()[0] view = task.flow_task._view_class.as_view() act=task.activate()