diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py index 8adf2b1840143fd729de7b59f12e0c756f5a3655..94852a52d588946b1a3cf6b495bbb31e44d8623d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py @@ -48,7 +48,7 @@ def get_users_by_role_in_project(role, project): if project in project_persons: return project_persons[project][role] else: - [] + return [] @cachetools.func.ttl_cache(ttl=600) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py index effe24b408c55dbd4b1151971f61b079c35d1281..914f7dd9e15d534f567382535576a09064e06323 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py @@ -28,7 +28,8 @@ def get_project_roles_for_user(user): try: if user == models.User.objects.get(username='paulus'): return ({'project': 'test_user_is_shared_support', 'role': 'shared_support'}, - {'project': 'test_user_is_contact', 'role': 'contact'}) + {'project': 'test_user_is_contact', 'role': 'contact'}, + {'project': 'test_user_is_friend', 'role': 'friend_of_project'}) except: pass diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index 7bfb0f1a186c72099d037c16b8268377ebe254a3..1f4defdc79838614333a9dbb20f9a1b8a8b98e2d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -27,6 +27,7 @@ from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOF from lofar.sas.tmss.tmss.tmssapp import models from lofar.sas.tmss.tmss.tmssapp import serializers from lofar.sas.tmss.tmss.tmssapp.adapters.reports import create_cycle_report, create_project_report +from lofar.sas.tmss.tmss.tmssapp.adapters.keycloak import get_users_by_role_in_project from django.http import JsonResponse from datetime import datetime @@ -34,7 +35,7 @@ from lofar.common.json_utils import get_default_json_object_for_schema from lofar.common.datetimeutils import formatDatetime from lofar.sas.tmss.tmss.tmssapp.tasks import * from lofar.sas.tmss.tmss.tmssapp.subtasks import * -from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import TMSSDjangoModelPermissions +from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import TMSSDjangoModelPermissions, get_project_roles_for_user from django.urls import resolve, get_script_prefix,Resolver404 from rest_framework.filters import OrderingFilter @@ -434,6 +435,28 @@ class ProjectViewSet(LOFARViewSet): return Response('Error: workflowapp is not running. It is needed to retrieve some reporting information.', status=status.HTTP_503_SERVICE_UNAVAILABLE) return Response(result, status=status.HTTP_200_OK) + @swagger_auto_schema(responses={200: 'List of users that have the "friend of project" role for this project', + 403: 'forbidden'}, + operation_description="Get the list of users that have the 'friend of project' role for this project") + @action(methods=['get'], detail=True, url_name="friend", name="Friend(s) of this project") + def friend(self, request, pk=None): + project = get_object_or_404(models.Project, pk=pk) + result = get_users_by_role_in_project(models.ProjectRole.Choices.FRIEND_OF_PROJECT.value, project.name) + return Response(result, status=status.HTTP_200_OK) + + @swagger_auto_schema(responses={200: 'List of roles that the requesting user has in this project', + 403: 'forbidden'}, + operation_description="Get the list of roles that the requesting user has in this project") + @action(methods=['get'], detail=True, url_name="my_roles", name="My roles in this project") + def my_roles(self, request, pk=None): + project = get_object_or_404(models.Project, pk=pk) + project_roles = get_project_roles_for_user(request.user) + result = [] + for project_role in project_roles: + if project.name == project_role['project']: + result.append(project_role['role']) + return Response(result, status=status.HTTP_200_OK) + class ProjectNestedViewSet(LOFARNestedViewSet): queryset = models.Project.objects.all() diff --git a/SAS/TMSS/backend/test/t_permissions_project_roles.py b/SAS/TMSS/backend/test/t_permissions_project_roles.py index 40ec90a4850a149636986c3eccbedb49c606b6bb..0c5db12f20f9ba7b394b995e841f59dcd1aa1bb1 100755 --- a/SAS/TMSS/backend/test/t_permissions_project_roles.py +++ b/SAS/TMSS/backend/test/t_permissions_project_roles.py @@ -51,6 +51,13 @@ from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator from django.test import TestCase +from django.contrib.auth import get_user_model +User = get_user_model() + +from django.conf import settings + +import json + class ProjectPermissionTestCase(TestCase): # This tests that the project permissions are enforced in light of the project roles that are externally provided # for the user through the user admin. This test does not rely on the project permissions as defined in the system, @@ -66,7 +73,8 @@ class ProjectPermissionTestCase(TestCase): cls.project_permission_patcher = mock.patch('lofar.sas.tmss.tmss.tmssapp.viewsets.get_project_roles_for_user') # todo: fix namespace so we get the get_project_roles that gets actually used cls.project_permission_mock = cls.project_permission_patcher.start() cls.project_permission_mock.return_value = ({'project': 'test_user_is_shared_support', 'role': 'shared_support'}, - {'project': 'test_user_is_contact', 'role': 'contact_author'}) + {'project': 'test_user_is_contact', 'role': 'contact_author'}, + {'project': 'test_user_is_friend', 'role': 'friend_of_project'}) # create some stuff as the standard super user, as setup for the tests below cls.test_data_creator = TMSSRESTTestDataCreator(BASE_URL, AUTH) @@ -75,8 +83,9 @@ class ProjectPermissionTestCase(TestCase): # create projects with magic names for which permission exists (or which have no whitelisted generic name) cls.project_shared_support_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.Project(name='test_user_is_shared_support', cycle_urls=[cycle_url]), '/project/') cls.project_contact_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.Project(name='test_user_is_contact', cycle_urls=[cycle_url]), '/project/') + cls.project_friend_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.Project(name='test_user_is_friend', cycle_urls=[cycle_url]), '/project/') cls.project_forbidden_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.Project(name='forbidden', cycle_urls=[cycle_url]), '/project/') - + cls.project_keycloak_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.Project(name="2014LOFAROBS", cycle_urls=[cycle_url]), '/project/') # project is known to Keycloak # user is shared_support cls.scheduling_set_shared_support_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.SchedulingSet(project_url=cls.project_shared_support_url), '/scheduling_set/') @@ -92,8 +101,10 @@ class ProjectPermissionTestCase(TestCase): # create the required permission entries to control what endpoint action requires which project role shared_support_role_url = BASE_URL + '/project_role/shared_support/' + friend_of_project_role_url = BASE_URL + '/project_role/friend_of_project/' cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.ProjectPermission(name='taskdraft', GET=[shared_support_role_url], POST=[shared_support_role_url]), '/project_permission/') cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.ProjectPermission(name='taskdraft-create_task_blueprint', POST=[shared_support_role_url]), '/project_permission/') + cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.ProjectPermission(name='project-my_roles', GET=[shared_support_role_url, friend_of_project_role_url]), '/project_permission/') cls.task_template_url = cls.test_data_creator.post_data_and_get_url(cls.test_data_creator.TaskTemplate(), '/task_template/') @@ -228,7 +239,44 @@ class ProjectPermissionTestCase(TestCase): self.assertEqual(r.status_code, 403) self.assertNotIn('Access-Control-Allow-Methods', r.headers) - # todo: add tests for other models with project permissions + def test_project_get_friend_returns_correct_user(self): + """ + Note: This test relies on real data from Keycloak. + """ + with requests.Session() as session: + session.verify = False + session.auth = (AUTH.username, AUTH.password) + r = session.get(self.project_keycloak_url + '/friend/') + if 'Invalid user credentials' in str(r.content) or \ + 'Service Unavailable' in str(r.content): + self.skipTest('skipping test_project_get_friend_returns_correct_user because the test environment has' + 'no valid admin credentials configured, or Keycloak is not working correctly.') + self.assertEqual(r.status_code, 200) + content = json.loads(r.content.decode('utf-8')) + self.assertEqual(len(content), 2) + for friend in content: + # Todo: find a way to mock the Keycloak response so we can assert more strictly. + self.assertTrue(friend.endswith('@astron.nl')) # redacted expected full email due to GDPR + + def test_project_get_friend_returns_403_if_no_permission_for_project(self): + + r = GET_and_assert_equal_expected_code(self, self.project_forbidden_url + '/friend/', 403, auth=self.auth) + self.assertIn('permission', str(r)) + + def test_project_get_my_roles_returns_correct_roles(self): + + r = GET_and_assert_equal_expected_code(self, self.project_shared_support_url + '/my_roles/', 200, auth=self.auth) + expected_reply = ['shared_support'] + self.assertEqual(expected_reply, r) + + r = GET_and_assert_equal_expected_code(self, self.project_friend_url + '/my_roles/', 200, auth=self.auth) + expected_reply = ['friend_of_project'] + self.assertEqual(expected_reply, r) + + def test_project_get_my_roles_returns_403_if_no_permission_for_project(self): + + r = GET_and_assert_equal_expected_code(self, self.project_forbidden_url + '/my_roles/', 403, auth=self.auth) + self.assertIn('permission', str(r)) if __name__ == "__main__":