diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py index 023594b67ad9d5f700bb0a6976b5151bacd4fd49..724aa45accf73a8abe4bfd69fd902038a10dc9fa 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py @@ -31,4 +31,7 @@ class Migration(migrations.Migration): migrations.RunPython(populate_misc), migrations.RunPython(populate_resources), migrations.RunPython(populate_cycles), - migrations.RunPython(populate_projects) ] + migrations.RunPython(populate_projects)]#, + #migrations.RunPython(populate_system_permissions), + #migrations.RunPython(populate_system_roles), + #migrations.RunPython(populate_system_test_users) ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/CMakeLists.txt b/SAS/TMSS/backend/src/tmss/tmssapp/models/CMakeLists.txt index 64b472d0532f38dcee337722ff16d68881b85099..3496efd57358ab186b665fe2dc3bd40264d4deaa 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/CMakeLists.txt +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/CMakeLists.txt @@ -3,6 +3,7 @@ include(PythonInstall) set(_py_files __init__.py + permissions.py specification.py scheduling.py common.py diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py index 9ff0dd5a806e427b40cf485bf700181440d16b75..8cdbf52b01eb27e1c3e8467cbba34e4494c37b3d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py @@ -9,6 +9,9 @@ from .common import NamedCommonPK, AbstractChoice from django.db.models import ManyToManyField from enum import Enum +from rest_framework.permissions import DjangoModelPermissions + + # # Project Permissions # @@ -31,3 +34,20 @@ class ProjectPermission(NamedCommonPK): POST = ManyToManyField('ProjectRole', related_name='can_POST', blank=True) PATCH = ManyToManyField('ProjectRole', related_name='can_PATCH', blank=True) DELETE = ManyToManyField('ProjectRole', related_name='can_DELETE', blank=True) + + +# todo: move to viewsets / merge with TMSSDjangoModelPermissions class +class TMSSBasePermissions(DjangoModelPermissions): + # This enforces permissions as "deny any" by default. + view_permissions = ['%(app_label)s.view_%(model_name)s'] + + perms_map = { + 'GET': view_permissions, + 'OPTIONS': view_permissions, + 'HEAD': view_permissions, + 'POST': DjangoModelPermissions.perms_map['POST'], + 'PUT': DjangoModelPermissions.perms_map['PUT'], + 'PATCH': DjangoModelPermissions.perms_map['PATCH'], + 'DELETE': DjangoModelPermissions.perms_map['DELETE'], + } + diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index 6eae000487adfd897cdfcc6a9491f9f343cf4d54..4aa9fe38cc2774eb3f5f72075e8ff3e567656fbe 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -255,3 +255,102 @@ def populate_permissions(): scientist_group = Group.objects.create(name='Scientist') scientist_group.permissions.add(Permission.objects.get(codename='view_cycle')) scientist_group.permissions.add(Permission.objects.get(codename='view_project')) + + +# TODO: Merge with project permissions +def populate_system_permissions(apps, schema_editor): + import inspect + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + from lofar.sas.tmss.tmss.tmssapp import viewsets + + # For each viewset create custom permissions for extra actions. + for name, obj in inspect.getmembers(viewsets): + if inspect.isclass(obj): + try: + ct = ContentType.objects.get_for_model(obj.serializer_class.Meta.model) + extra_actions = obj.get_extra_actions() + if extra_actions: + for action in extra_actions: + codename = ("%s_%s" % (action.__name__, obj.serializer_class.Meta.model.__name__)).lower() + name = f'Can {action.__name__} {obj.serializer_class.Meta.model.__name__.lower()}' + Permission.objects.create(codename=codename, name=name, content_type=ct) + except: + pass + + +def populate_system_roles(apps, schema_editor): + from django.contrib.contenttypes.models import ContentType + from django.contrib.auth.models import Group, Permission + to_observer_group = Group.objects.create(name='TO observer') + # TODO: Define permissions and add to the proper roles. Refactoring. + + # Subtask permissions + ct = ContentType.objects.get(model='subtask') + perm, created = Permission.objects.get_or_create(codename='view_subtask', name='Can view subtask', content_type=ct) + to_observer_group.permissions.add(perm) + to_observer_group.permissions.add(Permission.objects.get(codename='schedule_subtask')) + + # Template permissions + import inspect + from lofar.sas.tmss.tmss.tmssapp import models + import re + # For each template add permissions for TO observer role. + for name, obj in inspect.getmembers(models): + if inspect.isclass(obj) and issubclass(obj, Template) and obj is not Template: + ct = ContentType.objects.get(model=name.lower()) + perm_name = ''.join('_'.join(re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', name)).lower()) + perm, created = Permission.objects.get_or_create(codename=f'add_{name.lower()}', + name=f'Can add {perm_name}', content_type=ct) + to_observer_group.permissions.add(perm) + perm, created = Permission.objects.get_or_create(codename=f'view_{name.lower()}', + name=f'Can view {perm_name}', content_type=ct) + to_observer_group.permissions.add(perm) + perm, created = Permission.objects.get_or_create(codename=f'change_{name.lower()}', + name=f'Can change {perm_name}', content_type=ct) + to_observer_group.permissions.add(perm) + + # SchedulingUnit permissions + for template in ['schedulingunitdraft', 'schedulingunitblueprint']: + ct = ContentType.objects.get(model=template) + perm, created = Permission.objects.get_or_create(codename=f'add_{template}', name=f'Can add {template}', + content_type=ct) + to_observer_group.permissions.add(perm) + perm, created = Permission.objects.get_or_create(codename=f'view_{template}', name=f'Can view {template}', + content_type=ct) + to_observer_group.permissions.add(perm) + + sdco_support_group = Group.objects.create(name='SDCO support') + tmss_maintainer_group = Group.objects.create(name='TMSS Maintainer') + tmss_admin_group = Group.objects.create(name='TMSS Admin') + to_maintenance_group = Group.objects.create(name='TO maintenance') + scientist_group = Group.objects.create(name='Scientist') + e_scientist_group = Group.objects.create(name='Scientist (Expert)') + guest_group = Group.objects.create(name='Guest') + lta_user_group = Group.objects.create(name='LTA User') + + +def populate_system_test_users(apps, schema_editor): + from django.contrib.auth.models import User, Group + + # TODO: Superuser only for testing purposes, remove it when finished. + User.objects.create_superuser('admin', 'admin@example.com', 'admin') + + to_observer_user = User.objects.create(username='to_observer', password='to_observer') + to_observer_user.groups.add(Group.objects.get(name='TO observer')) + sdco_support_user = User.objects.create(username='sdco_support', password='sdco_support') + sdco_support_user.groups.add(Group.objects.get(name='SDCO support')) + tmss_maintainer_user = User.objects.create(username='tmss_maintainer', password='tmss_maintainer') + tmss_maintainer_user.groups.add(Group.objects.get(name='TMSS Maintainer')) + tmss_admin_user = User.objects.create(username='tmss_admin', password='tmss_admin') + tmss_admin_user.groups.add(Group.objects.get(name='TMSS Admin')) + to_maintenance_user = User.objects.create(username='to_maintenance', password='to_maintenance') + to_maintenance_user.groups.add(Group.objects.get(name='TO maintenance')) + scientist_user = User.objects.create(username='scientist', password='scientist') + scientist_user.groups.add(Group.objects.get(name='Scientist')) + e_scientist_user = User.objects.create(username='e_scientist', password='e_scientist') + e_scientist_user.groups.add(Group.objects.get(name='Scientist (Expert)')) + guest_user = User.objects.create(username='guest', password='guest') + guest_user.groups.add(Group.objects.get(name='Guest')) + lta_user = User.objects.create(username='lta_user', password='lta_user') + lta_user.groups.add(Group.objects.get(name='LTA User')) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py index 6e8807ec170b99044936ac30b9ac7c4509564143..abe9383eb92c077fb0ca4b452d9c8b1fd2575675 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py @@ -18,15 +18,56 @@ from django.urls import reverse as revese_url from rest_framework.decorators import action from lofar.common import json_utils from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import TMSSPermissions, IsProjectMemberFilterBackend - -class LOFARViewSet(viewsets.ModelViewSet): +from lofar.sas.tmss.tmss.tmssapp.models import permissions + + +class TMSSPermissionsMixin: + + def __init__(self, *args, **kwargs): + self.permission_classes = (TMSSPermissions,) + self.filter_backends = (IsProjectMemberFilterBackend,) + self.extra_action_permission_classes = [] + self._create_extra_action_permission_classes() + super(TMSSPermissionsMixin, self).__init__(*args, **kwargs) + + # TODO: Cache this method to avoid redundancy and overhead. + def _create_extra_action_permission_classes(self): + extra_actions = [a.__name__ for a in self.get_extra_actions()] + for ea in extra_actions: # Create permission classes + permission_name = f'{ea}_{self.serializer_class.Meta.model.__name__.lower()}' + permission_class_name = f'Can {ea} {self.serializer_class.Meta.model.__name__.lower()}' + new_permission_class = type(f'{permission_class_name}', (permissions.TMSSBasePermissions,), { + # TODO: Is it necessary to have both permissions and object permissions? + # TODO: Find a way to use the "%(app_label)s." syntax. + 'permission_name': permission_name, + 'has_permission': lambda self, request, view: request.user.has_perm(f'tmssapp.{self.permission_name}'), + 'has_object_permission': lambda self, request, view, obj: request.user.has_perm(f'tmssapp.{self.permission_name}'), + }) + new_permission_class.__setattr__(self, 'permission_name', permission_name) + self.extra_action_permission_classes.append({ea: new_permission_class},) + + # TODO: Refactoring. + def get_model_permissions(self): + extra_actions = [a.__name__ for a in self.get_extra_actions()] + if self.action in extra_actions: + for ea_permission_class in self.extra_action_permission_classes: + if ea_permission_class.get(self.action): + return [permissions.TMSSBasePermissions, ea_permission_class.get(self.action),] + else: + return [permissions.TMSSBasePermissions,] + else: + return [permissions.TMSSBasePermissions, ] + + #def get_permissions(self): + # self.get_extra_action_permission_classes() + # return super(TMSSPermissionsMixin, self).get_permissions() + + +class LOFARViewSet(TMSSPermissionsMixin, viewsets.ModelViewSet): """ If you're using format suffixes, make sure to also include the `format=None` keyword argument for each action. """ - permission_classes = (TMSSPermissions,) - filter_backends = (IsProjectMemberFilterBackend,) - @swagger_auto_schema(responses={403: 'forbidden'}) def list(self, request, **kwargs): @@ -57,9 +98,6 @@ class LOFARNestedViewSet(mixins.CreateModelMixin, #mixins.RetrieveModelMixin, viewsets.GenericViewSet): - permission_classes = (TMSSPermissions,) - filter_backends = (IsProjectMemberFilterBackend,) - @swagger_auto_schema(responses={403: 'forbidden'}) def list(self, request, **kwargs): return super(LOFARNestedViewSet, self).list(request, **kwargs) @@ -78,9 +116,6 @@ class LOFARCopyViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): return super(LOFARCopyViewSet, self).list(request, **kwargs) """ - permission_classes = (TMSSPermissions,) - filter_backends = (IsProjectMemberFilterBackend,) - @swagger_auto_schema(responses={400: 'invalid specification', 403: 'forbidden'}) def create(self, request, **kwargs): return super(LOFARCopyViewSet, self).create(request, **kwargs) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py index dd702ea2c2bcc2483f181181c3b9633f951556ef..30e8d1c255c66eb7bbd6aa3cca0579b25ef4bdba 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py @@ -180,15 +180,14 @@ 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 + return (all(model_permission().has_permission(request, view) for model_permission in view.get_model_permissions()) 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 + return self.project_permissions.has_object_permission(request, view, obj) and request.user.is_authenticated # diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py index 42fc0210cbabfc8e86b05c51f2b0657aaca6ea53..b6045d85cd416eb6ed0a0e3036182a66a0a2d6a7 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py @@ -17,6 +17,7 @@ from drf_yasg.inspectors import SwaggerAutoSchema from drf_yasg.openapi import Parameter from rest_framework.decorators import action +from rest_framework.decorators import permission_classes from django.http import HttpResponse, JsonResponse, HttpResponseRedirect, HttpResponseNotFound from rest_framework.response import Response as RestResponse diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index e77738e5735924f1f0a8301ce9b514209b0ef734..ee92b5de210d8952f551362dd8f34b36fb8174b9 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -735,8 +735,6 @@ class SchedulingUnitBlueprintNestedViewSet(LOFARNestedViewSet): class TaskDraftViewSet(LOFARViewSet): queryset = models.TaskDraft.objects.all() serializer_class = serializers.TaskDraftSerializer - #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') \ diff --git a/SAS/TMSS/test/t_tmssapp_permissions_system_roles.py b/SAS/TMSS/test/t_tmssapp_permissions_system_roles.py new file mode 100644 index 0000000000000000000000000000000000000000..56573ae527a2bd3be7e0c22d22352185b8f86b54 --- /dev/null +++ b/SAS/TMSS/test/t_tmssapp_permissions_system_roles.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2018 ASTRON (Netherlands Institute for Radio Astronomy) +# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# The LOFAR software suite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. + +# $Id: $ + +import logging +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) + +# Do Mandatory setup step: +# use setup/teardown magic for tmss test database, ldap server and django server +# (ignore pycharm unused import statement, python unittests does use at RunTime the tmss_test_environment_unittest_setup module) +from lofar.sas.tmss.test.tmss_test_environment_unittest_setup import * + +# import and setup test data creator +from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator + +from lofar.sas.tmss.tmss.tmssapp import models + +from django.contrib.auth.models import User, Group, Permission +from datetime import datetime +import unittest +import requests + + +class SystemRolesTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + # cls.test_data_creator = TMSSRESTTestDataCreator(BASE_URL, requests.auth.HTTPBasicAuth('paulus', 'pauluspass')) + # response = requests.get(cls.test_data_creator.django_api_url + '/', auth=cls.test_data_creator.auth) + + # System roles creation. More at https://support.astron.nl/confluence/display/TMSS/User+roles + cls.to_observer_group = Group.objects.create(name='TO observer') + # TODO: Define permissions and add to the proper roles. + # cls.to_observer_group.permissions.add(Permission.objects.get(codename='add_user')) + # cls.to_observer_group.permissions.add(Permission.objects.get(codename='add_group')) + + cls.sdco_support_group = Group.objects.create(name='SDCO support') + cls.tmss_maintainer_group = Group.objects.create(name='TMSS Maintainer') + cls.tmss_admin_group = Group.objects.create(name='TMSS Admin') + cls.to_maintenance_group = Group.objects.create(name='TO maintenance') + cls.scientist_group = Group.objects.create(name='Scientist') + cls.e_scientist_group = Group.objects.create(name='Scientist (Expert)') + cls.guest_group = Group.objects.create(name='Guest') + cls.lta_user_group = Group.objects.create(name='LTA User') + + def test(self): + logger.info('Created new group: %s', self.to_observer_group) + + +class SystemUserTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create test users and assign them the proper system role. + cls.to_observer_user = User.objects.create(username='to_observer', password='to_observer') + cls.to_observer_user.groups.add(Group.objects.get(name='TO observer')) + cls.sdco_support_user = User.objects.create(username='sdco_support', password='sdco_support') + cls.sdco_support_user.groups.add(Group.objects.get(name='SDCO support')) + cls.tmss_maintainer_user = User.objects.create(username='tmss_maintainer', password='tmss_maintainer') + cls.tmss_maintainer_user.groups.add(Group.objects.get(name='TMSS Maintainer')) + cls.tmss_admin_user = User.objects.create(username='tmss_admin', password='tmss_admin') + cls.tmss_admin_user.groups.add(Group.objects.get(name='TMSS Admin')) + cls.to_maintenance_user = User.objects.create(username='to_maintenance', password='to_maintenance') + cls.to_maintenance_user.groups.add(Group.objects.get(name='TO maintenance')) + cls.scientist_user = User.objects.create(username='scientist', password='scientist') + cls.scientist_user.groups.add(Group.objects.get(name='Scientist')) + cls.e_scientist_user = User.objects.create(username='e_scientist', password='e_scientist') + cls.e_scientist_user.groups.add(Group.objects.get(name='Scientist (Expert)')) + cls.guest_user = User.objects.create(username='guest', password='guest') + cls.guest_user.groups.add(Group.objects.get(name='Guest')) + cls.lta_user = User.objects.create(username='lta_user', password='lta_user') + cls.lta_user.groups.add(Group.objects.get(name='LTA User')) + + def test(self): + logger.info('Created new user: %s', self.to_observer_user) + + +if __name__ == "__main__": + unittest.main()