diff --git a/ansys/rep/client/jms/__init__.py b/ansys/rep/client/jms/__init__.py index ad0ed904b..5e8289536 100644 --- a/ansys/rep/client/jms/__init__.py +++ b/ansys/rep/client/jms/__init__.py @@ -21,8 +21,8 @@ JobSelection, Licensing, ParameterMapping, + Permission, Project, - ProjectPermission, ResourceRequirements, Software, StringParameterDefinition, diff --git a/ansys/rep/client/jms/api/jms_api.py b/ansys/rep/client/jms/api/jms_api.py index da7e61fb2..a1c6bdd21 100644 --- a/ansys/rep/client/jms/api/jms_api.py +++ b/ansys/rep/client/jms/api/jms_api.py @@ -7,11 +7,15 @@ from ansys.rep.client.client import Client from ansys.rep.client.exceptions import REPError +from ansys.rep.client.jms.resource import ( + Evaluator, + Operation, + Permission, + Project, + TaskDefinitionTemplate, +) +from ansys.rep.client.jms.schema.project import ProjectSchema -from ..resource import Operation -from ..resource.evaluator import Evaluator -from ..resource.project import Project, ProjectSchema -from ..resource.task_definition_template import TaskDefinitionTemplate from .base import create_objects, delete_objects, get_object, get_objects, update_objects log = logging.getLogger(__name__) @@ -153,6 +157,32 @@ def delete_task_definition_templates(self, templates: List[TaskDefinitionTemplat """ return delete_objects(self.client.session, self.url, templates) + # Task Definition Template Permissions + def get_task_definition_template_permissions( + self, template_id: str, as_objects: bool = True + ) -> List[Permission]: + """Get permissions of a task definition template""" + return get_objects( + self.client.session, + f"{self.url}/task_definition_templates/{template_id}", + Permission, + as_objects, + ) + + def update_task_definition_template_permissions( + self, + template_id: str, + permissions: List[Permission], + as_objects: bool = True, + ) -> List[Permission]: + """Update permissions of a task definition template""" + return update_objects( + self.client.session, + f"{self.url}/task_definition_templates/{template_id}", + permissions, + as_objects, + ) + ################################################################ # Operations def get_operations(self, as_objects=True, **query_params) -> List[Operation]: diff --git a/ansys/rep/client/jms/api/project_api.py b/ansys/rep/client/jms/api/project_api.py index d7ce45974..3b3d660e9 100644 --- a/ansys/rep/client/jms/api/project_api.py +++ b/ansys/rep/client/jms/api/project_api.py @@ -11,19 +11,22 @@ from ansys.rep.client.client import Client from ansys.rep.client.common import Object from ansys.rep.client.exceptions import ClientError, REPError +from ansys.rep.client.jms.resource import ( + Algorithm, + File, + Job, + JobDefinition, + JobSelection, + LicenseContext, + ParameterDefinition, + ParameterMapping, + Permission, + Project, + Task, + TaskDefinition, +) +from ansys.rep.client.jms.schema.job import JobSchema -from ..resource.algorithm import Algorithm -from ..resource.file import File -from ..resource.job import Job, JobSchema -from ..resource.job_definition import JobDefinition -from ..resource.license_context import LicenseContext -from ..resource.parameter_definition import ParameterDefinition -from ..resource.parameter_mapping import ParameterMapping -from ..resource.project import Project -from ..resource.project_permission import ProjectPermission, ProjectPermissionSchema -from ..resource.selection import JobSelection -from ..resource.task import Task -from ..resource.task_definition import TaskDefinition from .base import create_objects, delete_objects, get_objects, update_objects from .jms_api import JmsApi, _monitor_operation, get_project @@ -331,12 +334,11 @@ def delete_algorithms(self, algorithms: List[Algorithm]): ################################################################ # Permissions - def get_permissions(self, as_objects=True) -> List[ProjectPermission]: - return self._get_objects(ProjectPermission, as_objects=as_objects) + def get_permissions(self, as_objects=True) -> List[Permission]: + return self._get_objects(Permission, as_objects=as_objects, fields=None) - def update_permissions(self, permissions: List[ProjectPermission]): - # the rest api currently doesn't return anything on permissions update - update_permissions(self.client, self.url, permissions) + def update_permissions(self, permissions: List[Permission], as_objects=True): + return self._update_objects(permissions, as_objects=as_objects) ################################################################ # License contexts @@ -560,19 +562,6 @@ def sync_jobs(project_api: ProjectApi, jobs: List[Job]): r = project_api.client.session.put(f"{url}", data=json_data) -def update_permissions(client, project_api_url, permissions): - - if not permissions: - return - - url = f"{project_api_url}/permissions" - - schema = ProjectPermissionSchema(many=True) - serialized_data = schema.dump(permissions) - json_data = json.dumps({"permissions": serialized_data}) - r = client.session.put(f"{url}", data=json_data) - - @cached(cache=TTLCache(1024, 60), key=lambda project: project.id) def get_fs_url(project: Project): if project.file_storages == missing: diff --git a/ansys/rep/client/jms/resource/__init__.py b/ansys/rep/client/jms/resource/__init__.py index 4b93521e7..1175b62f9 100644 --- a/ansys/rep/client/jms/resource/__init__.py +++ b/ansys/rep/client/jms/resource/__init__.py @@ -18,7 +18,7 @@ IntParameterDefinition, StringParameterDefinition, BoolParameterDefinition from .parameter_mapping import ParameterMapping from .project import Project -from .project_permission import ProjectPermission +from .permission import Permission from .selection import JobSelection from .task import Task from .task_definition import Licensing, SuccessCriteria, Software, ResourceRequirements, TaskDefinition diff --git a/ansys/rep/client/jms/resource/project_permission.py b/ansys/rep/client/jms/resource/permission.py similarity index 63% rename from ansys/rep/client/jms/resource/project_permission.py rename to ansys/rep/client/jms/resource/permission.py index 77d2b53ff..dfc331652 100644 --- a/ansys/rep/client/jms/resource/project_permission.py +++ b/ansys/rep/client/jms/resource/permission.py @@ -1,22 +1,25 @@ # autogenerated code from marshmallow.utils import missing from ansys.rep.client.common import Object -from ..schema.project_permission import ProjectPermissionSchema +from ..schema.permission import PermissionSchema -class ProjectPermission(Object): - """ProjectPermission resource. +class Permission(Object): + """Permission resource. Parameters ---------- permission_type : str - value_id : str + Either 'user', 'group', or 'anyone'. + value_id : str, optional + Can be the ID of a user or group. value_name : str, optional role : str + Either 'admin', 'writer', or 'reader'. """ class Meta: - schema = ProjectPermissionSchema + schema = PermissionSchema rest_name = "permissions" def __init__(self, @@ -32,4 +35,4 @@ def __init__(self, self.obj_type = self.__class__.__name__ -ProjectPermissionSchema.Meta.object_class = ProjectPermission +PermissionSchema.Meta.object_class = Permission diff --git a/ansys/rep/client/jms/schema/permission.py b/ansys/rep/client/jms/schema/permission.py new file mode 100644 index 000000000..891f11bd6 --- /dev/null +++ b/ansys/rep/client/jms/schema/permission.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from ansys.rep.client.common import BaseSchema + + +class PermissionSchema(BaseSchema): + class Meta(BaseSchema.Meta): + pass + + permission_type = fields.String( + required=True, metadata={"description": "Either 'user', 'group', or 'anyone'."} + ) + value_id = fields.String( + allow_none=True, metadata={"description": "Can be the ID of a user or group."} + ) + value_name = fields.String(allow_none=True) + role = fields.String( + required=True, metadata={"description": "Either 'admin', 'writer', or 'reader'."} + ) diff --git a/ansys/rep/client/jms/schema/project_permission.py b/ansys/rep/client/jms/schema/project_permission.py deleted file mode 100644 index ce41317da..000000000 --- a/ansys/rep/client/jms/schema/project_permission.py +++ /dev/null @@ -1,21 +0,0 @@ -# ---------------------------------------------------------- -# Copyright (C) 2019 by -# ANSYS Switzerland GmbH -# www.ansys.com -# -# Author(s): F.Negri -# ---------------------------------------------------------- - -from marshmallow import fields - -from ansys.rep.client.common import BaseSchema - - -class ProjectPermissionSchema(BaseSchema): - class Meta(BaseSchema.Meta): - pass - - permission_type = fields.String(required=True) - value_id = fields.String(required=True) - value_name = fields.String(allow_none=True) - role = fields.String(required=True) diff --git a/doc/source/api/jms.rst b/doc/source/api/jms.rst index 664a01a60..9f2c27ec1 100644 --- a/doc/source/api/jms.rst +++ b/doc/source/api/jms.rst @@ -144,4 +144,10 @@ Task Definition Template :members: .. autoclass:: ansys.rep.client.jms.TaskDefinitionTemplate + :members: + +Permissions +^^^^^^^^^^^ + +.. autoclass:: ansys.rep.client.jms.Permission :members: \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 529bb1ec9..c4cd4e836 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -33,7 +33,7 @@ "sphinx.ext.napoleon", "sphinx.ext.todo", "sphinx.ext.extlinks", - # "sphinx.ext.viewcode", # to show pytho source code + # "sphinx.ext.viewcode", # to show python source code "sphinxcontrib.httpdomain", "sphinxcontrib.globalsubs", "sphinx.ext.intersphinx", diff --git a/generate_resources.py b/generate_resources.py index 9476ccaa8..b3200a436 100644 --- a/generate_resources.py +++ b/generate_resources.py @@ -104,12 +104,12 @@ "resource_filename": "project", }, { - "schema": "ProjectPermissionSchema", - "schema_filename": "project_permission", + "schema": "PermissionSchema", + "schema_filename": "permission", "rest_name": "permissions", "additional_fields": [], - "class": "ProjectPermission", - "resource_filename": "project_permission", + "class": "Permission", + "resource_filename": "permission", }, { "schema": "ResourceRequirementsSchema", diff --git a/prepare_documentation.py b/prepare_documentation.py index ff533cb2f..b0915ee4d 100644 --- a/prepare_documentation.py +++ b/prepare_documentation.py @@ -31,8 +31,8 @@ Licensing, Operation, ParameterMapping, + Permission, Project, - ProjectPermission, ResourceRequirements, Software, StringParameterDefinition, @@ -66,7 +66,7 @@ def generate_openapi_specs(): for resource in [ Project, - ProjectPermission, + Permission, LicenseContext, Job, Algorithm, diff --git a/tests/jms/test_project_permissions.py b/tests/jms/test_project_permissions.py index a06061912..1e83cd1dd 100644 --- a/tests/jms/test_project_permissions.py +++ b/tests/jms/test_project_permissions.py @@ -14,7 +14,7 @@ from ansys.rep.client.auth import AuthApi, User from ansys.rep.client.exceptions import ClientError from ansys.rep.client.jms import JmsApi, ProjectApi -from ansys.rep.client.jms.resource import JobDefinition, Project, ProjectPermission +from ansys.rep.client.jms.resource import JobDefinition, Permission, Project from tests.rep_test import REPTestCase log = logging.getLogger(__name__) @@ -31,12 +31,11 @@ def grant_permissions(project_api: ProjectApi, user): permissions = project_api.get_permissions() log.info(f"Permissions before: {permissions}") permissions.append( - ProjectPermission( + Permission( permission_type="user", value_name=user.username, role="writer", value_id=user.id ) ) - project_api.update_permissions(permissions) - permissions = project_api.get_permissions() + permissions = project_api.update_permissions(permissions) log.info(f"Permissions after: {permissions}") @@ -45,8 +44,7 @@ def remove_permissions(project_api: ProjectApi, user): permissions = project_api.get_permissions() log.info(f"Permissions before: {permissions}") permissions = [p for p in permissions if p.value_name != user.username] - project_api.update_permissions(permissions) - permissions = project_api.get_permissions() + permissions = project_api.update_permissions(permissions) log.info(f"Permissions after: {permissions}") diff --git a/tests/jms/test_task_definition_templates.py b/tests/jms/test_task_definition_templates.py index a3abe95ca..8bf8f83cd 100644 --- a/tests/jms/test_task_definition_templates.py +++ b/tests/jms/test_task_definition_templates.py @@ -13,8 +13,10 @@ from marshmallow.utils import missing +from ansys.rep.client import Client, REPError +from ansys.rep.client.auth import AuthApi, User from ansys.rep.client.jms import JmsApi -from ansys.rep.client.jms.resource import TaskDefinitionTemplate +from ansys.rep.client.jms.resource import Permission, TaskDefinitionTemplate from ansys.rep.client.jms.schema.task_definition_template import TaskDefinitionTemplateSchema from tests.rep_test import REPTestCase @@ -143,6 +145,186 @@ def test_template_integration(self): templates = jms_api.get_task_definition_templates(name=template_name) self.assertEqual(len(templates), 0) + def test_template_permissions(self): + + client = self.client() + jms_api = JmsApi(client) + + templates = jms_api.get_task_definition_templates() + + # a regular deployment should have some default templates + # with read all permissions + some user defined ones with + # either user or group permissions + for template in templates: + permissions = jms_api.get_task_definition_template_permissions(template_id=template.id) + for permission in permissions: + self.assertIn(permission.permission_type, ["user", "group", "anyone"]) + + # create new template and check default permissions + template = TaskDefinitionTemplate(name="my_template", version=uuid.uuid4()) + template = jms_api.create_task_definition_templates([template])[0] + permissions = jms_api.get_task_definition_template_permissions(template_id=template.id) + self.assertEqual(len(permissions), 1) + self.assertEqual(permissions[0].permission_type, "user") + self.assertEqual(permissions[0].role, "admin") + self.assertIsNotNone(permissions[0].value_id) + + # create test user + auth_api = AuthApi(client) + user_credentials = { + "user1": {"username": f"testuser-{uuid.uuid4().hex[:8]}", "password": "test"} + } + user1 = auth_api.create_user( + User( + username=user_credentials["user1"]["username"], + password=user_credentials["user1"]["password"], + is_admin=False, + ) + ) + log.info(f"User 1: {user1}") + client1 = Client( + rep_url=self.rep_url, + username=user1.username, + password=user_credentials["user1"]["password"], + ) + jms_api1 = JmsApi(client1) + + # verify test user can't access the template + client1_templates = jms_api1.get_task_definition_templates(id=template.id) + self.assertEqual(len(client1_templates), 0) + + # grant read all permissions + permissions.append(Permission(permission_type="anyone", role="reader", value_id=None)) + permissions = jms_api.update_task_definition_template_permissions(template.id, permissions) + self.assertEqual(len(permissions), 2) + + # verify test user can now access the template + client1_templates = jms_api1.get_task_definition_templates(id=template.id) + self.assertEqual(len(client1_templates), 1) + self.assertEqual(client1_templates[0].name, template.name) + + # verify test user can't edit the template + client1_templates[0].version = client1_templates[0].version + "-dev" + + except_obj = None + try: + client1_templates = jms_api1.update_task_definition_templates(client1_templates) + except REPError as e: + except_obj = e + self.assertEqual(except_obj.response.status_code, 403) + self.assertEqual(except_obj.description, "Access to this resource has been restricted") + + # grant write permissions to the user + permissions.append(Permission(permission_type="user", role="writer", value_id=user1.id)) + permissions = jms_api.update_task_definition_template_permissions(template.id, permissions) + self.assertEqual(len(permissions), 3) + + # verify test user can now edit the template + client1_templates[0].version = client1_templates[0].version + "-dev" + client1_templates = jms_api1.update_task_definition_templates(client1_templates) + + # Delete template + jms_api.delete_task_definition_templates([template]) + + # Let user1 create a template + template = TaskDefinitionTemplate(name="my_template", version=uuid.uuid4()) + template = jms_api1.create_task_definition_templates([template])[0] + permissions = jms_api1.get_task_definition_template_permissions(template_id=template.id) + self.assertEqual(len(permissions), 1) + self.assertEqual(permissions[0].permission_type, "user") + self.assertEqual(permissions[0].role, "admin") + self.assertEqual(permissions[0].value_id, user1.id) + + # verify that an admin user can access the template + admin_templates = jms_api.get_task_definition_templates(id=template.id) + log.info(admin_templates) + self.assertEqual(len(admin_templates), 1) + self.assertEqual(admin_templates[0].name, template.name) + self.assertEqual(admin_templates[0].version, template.version) + + # Delete template + jms_api1.delete_task_definition_templates([template]) + + # Delete user + auth_api.delete_user(user1) + + def test_template_anyone_permission(self): + + client = self.client() + jms_api = JmsApi(client) + + # create new template and check default permissions + template = TaskDefinitionTemplate(name="my_template", version=uuid.uuid4()) + template = jms_api.create_task_definition_templates([template])[0] + permissions = jms_api.get_task_definition_template_permissions(template_id=template.id) + self.assertEqual(len(permissions), 1) + self.assertEqual(permissions[0].permission_type, "user") + self.assertEqual(permissions[0].role, "admin") + self.assertIsNotNone(permissions[0].value_id) + + # create test user + auth_api = AuthApi(client) + user_credentials = { + "user1": {"username": f"testuser-{uuid.uuid4().hex[:8]}", "password": "test"} + } + user1 = auth_api.create_user( + User( + username=user_credentials["user1"]["username"], + password=user_credentials["user1"]["password"], + is_admin=False, + ) + ) + log.info(f"User 1: {user1}") + client1 = Client( + rep_url=self.rep_url, + username=user1.username, + password=user_credentials["user1"]["password"], + ) + jms_api1 = JmsApi(client1) + + # verify test user can't access the template + client1_templates = jms_api1.get_task_definition_templates(id=template.id) + self.assertEqual(len(client1_templates), 0) + + # grant read all permissions + permissions.append(Permission(permission_type="anyone", role="reader", value_id=None)) + permissions = jms_api.update_task_definition_template_permissions(template.id, permissions) + self.assertEqual(len(permissions), 2) + + # verify test user can now access the template + client1_templates = jms_api1.get_task_definition_templates(id=template.id) + self.assertEqual(len(client1_templates), 1) + self.assertEqual(client1_templates[0].name, template.name) + + # verify test user can't edit the template + client1_templates[0].version = client1_templates[0].version + "-dev" + + except_obj = None + try: + client1_templates = jms_api1.update_task_definition_templates(client1_templates) + except REPError as e: + except_obj = e + self.assertEqual(except_obj.response.status_code, 403) + self.assertEqual(except_obj.description, "Access to this resource has been restricted") + + # grant write all permissions + anyone_permission = next(p for p in permissions if p.permission_type == "anyone") + anyone_permission.role = "writer" + permissions = jms_api.update_task_definition_template_permissions(template.id, permissions) + self.assertEqual(len(permissions), 2) + for p in permissions: + if p.permission_type == "anyone": + self.assertEqual(p.role, "writer") + + # verify test user can now edit the template + client1_templates = jms_api1.update_task_definition_templates(client1_templates) + + # Delete template + jms_api.delete_task_definition_templates([template]) + + # Delete user + auth_api.delete_user(user1) + if __name__ == "__main__": unittest.main()