diff --git a/docs/changelog.rst b/docs/changelog.rst index a2b4addb8..85e119fb1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ Currently in development. - Give GRANT permissions to the user creating an object even if also granting permissions to a group - Added name-based filtering to action and instrument lists - Added config value for MAX_FORM_MEMORY_SIZE +- Add name-based filtering to action and instrument lists +- Add OIDC session expiration and Back-Channel Logout +- Add endpoints for permission management Version 0.32 diff --git a/docs/developer_guide/api.rst b/docs/developer_guide/api.rst index e56938e4e..7e6757e3f 100644 --- a/docs/developer_guide/api.rst +++ b/docs/developer_guide/api.rst @@ -537,6 +537,162 @@ Setting the permissions for anonymous users :statuscode 404: the object does not exist +Reading all permissions of an object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. http:get:: /api/v1/objects/(int:object_id)/permissions/ + + Get all permission mappings. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/objects/1/permissions/ HTTP/1.1 + Host: iffsamples.fz-juelich.de + Accept: application/json + Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "users": { + 1: "grant" + }, + "groups": { + 2: "read" + }, + "projects": { + 2: "read" + }, + "authenticated_users" : "none", + "anonymous_users": "none" + } + + :', endpoint='location', view_func=Location.as_view('location')) api.add_url_rule('/api/v1/location_types/', endpoint='location_types', view_func=LocationTypes.as_view('location_types')) api.add_url_rule('/api/v1/location_types/', endpoint='location_type', view_func=LocationType.as_view('location_type')) +api.add_url_rule('/api/v1/objects/permissions/copy/', endpoint='copy_objects_permissions', view_func=CopyObjectsPermissions.as_view('copy_objects_permissions')) api.add_url_rule('/api/v1/objects//files/', endpoint='object_files', view_func=ObjectFiles.as_view('object_files')) api.add_url_rule('/api/v1/objects//files/', endpoint='object_file', view_func=ObjectFile.as_view('object_file')) api.add_url_rule('/api/v1/objects//locations/', endpoint='object_location_assignments', view_func=ObjectLocationAssignments.as_view('object_location_assignments')) api.add_url_rule('/api/v1/objects//locations/', endpoint='object_location_assignment', view_func=ObjectLocationAssignment.as_view('object_location_assignment')) +api.add_url_rule('/api/v1/objects//permissions/', endpoint='object_permissions', view_func=ObjectPermissions.as_view('object_permissions')) api.add_url_rule('/api/v1/objects//permissions/users/', endpoint='users_object_permissions', view_func=UsersObjectPermissions.as_view('users_object_permissions')) api.add_url_rule('/api/v1/objects//permissions/users/', endpoint='user_object_permissions', view_func=UserObjectPermissions.as_view('user_object_permissions')) api.add_url_rule('/api/v1/objects//permissions/groups/', endpoint='groups_object_permissions', view_func=GroupsObjectPermissions.as_view('groups_object_permissions')) diff --git a/sampledb/api/server/object_permissions.py b/sampledb/api/server/object_permissions.py index 7bb3ce6b9..407137e4f 100644 --- a/sampledb/api/server/object_permissions.py +++ b/sampledb/api/server/object_permissions.py @@ -3,16 +3,294 @@ RESTful API for SampleDB """ +import functools +import typing import flask -from .authentication import object_permissions_required +from .authentication import object_permissions_required, multi_auth from ..utils import Resource, ResponseData -from ...logic import users, groups, projects, errors, object_permissions +from ...logic import users, groups, projects, errors, object_permissions, objects from ...models import Permissions __author__ = 'Florian Rhiem ' +def all_object_permissions_dict_to_json(permissions: object_permissions.AllObjectPermissionsDict) -> typing.Dict[str, typing.Any]: + return { + "users": { + user_id: permission.name.lower() for user_id, permission in permissions["users"].items() + }, + "groups": { + group_id: permission.name.lower() for group_id, permission in permissions["basic_groups"].items() + }, + "projects": { + project_id: permission.name.lower() for project_id, permission in permissions["projects"].items() + }, + "authenticated_users": permissions["authenticated"].name.lower(), + "anonymous_users": permissions["anonymous"].name.lower(), + } + + +class ObjectPermissions(Resource): + @object_permissions_required(Permissions.READ) + def get(self, object_id: int) -> ResponseData: + return all_object_permissions_dict_to_json( + object_permissions.get_all_object_permissions(object_id=object_id) + ), 200 + + @object_permissions_required(Permissions.GRANT) + def put(self, object_id: int) -> ResponseData: + request_json = flask.request.get_json(force=True) + update_functions = [] + if not isinstance(request_json, dict): + return { + "message": "JSON dict body required" + }, 400 + + # User-Permissions + if (user_perms_json := request_json.get("users")) is not None: + if not isinstance(user_perms_json, dict): + return { + "message": "'users' requires to be a json-object" + }, 400 + for user_id, permission in user_perms_json.items(): + try: + users.check_user_exists(user_id) + except errors.UserDoesNotExistError: + return { + "message": f"user {user_id} does not exist" + }, 404 + if not isinstance(permission, str): + return { + "message": "JSON string for permission required" + }, 400 + try: + user_permissions = Permissions.from_name(permission) + except ValueError: + return { + "message": f"Permissions name for user {user_id} required" + }, 400 + + update_functions.append( + functools.partial( + object_permissions.set_user_object_permissions, + object_id, + user_id, + user_permissions, + ) + ) + + # Group-Permissions + if (group_perms_json := request_json.get("groups")) is not None: + if not isinstance(group_perms_json, dict): + return { + "message": "'groups' requires to be a json-object" + }, 400 + for group_id, permission in group_perms_json.items(): + try: + groups.get_group(group_id) + except errors.GroupDoesNotExistError: + return { + "message": f"group {group_id} does not exist" + }, 404 + if not isinstance(permission, str): + return { + "message": "JSON string for permission required" + }, 400 + try: + group_permissions = Permissions.from_name(permission) + except ValueError: + return { + "message": f"Permissions name for group {group_id} required" + }, 400 + + update_functions.append( + functools.partial( + object_permissions.set_group_object_permissions, + object_id, + group_id, + group_permissions, + ) + ) + + # Project-Permissions + if (project_perms_json := request_json.get("projects")) is not None: + if not isinstance(project_perms_json, dict): + return { + "message": "'users' requires to be a json-object" + }, 400 + for project_id, permission in project_perms_json.items(): + try: + projects.get_project(project_id) + except errors.ProjectDoesNotExistError: + return { + "message": f"project {project_id} does not exist" + }, 404 + if not isinstance(permission, str): + return { + "message": "JSON string for permission required" + }, 400 + try: + project_permissions = Permissions.from_name(permission) + except ValueError: + return { + "message": f"Permissions name for project {project_id} required" + }, 400 + + update_functions.append( + functools.partial( + object_permissions.set_project_object_permissions, + object_id, + project_id, + project_permissions, + ) + ) + + # Authenticated + if (authenticated_perms_json := request_json.get("authenticated_users")) is not None: + if not isinstance(authenticated_perms_json, str): + return { + "message": "JSON string for permission required" + }, 400 + try: + authenticated_permissions = Permissions.from_name(authenticated_perms_json) + except ValueError: + return { + "message": "Permissions name for authenticated users required" + }, 400 + + if authenticated_permissions not in {Permissions.NONE, Permissions.READ}: + return { + "message": 'expected "none" or "read"' + }, 400 + + update_functions.append( + functools.partial( + object_permissions.set_object_permissions_for_all_users, + object_id, + authenticated_permissions, + ) + ) + + # Anonymous + if (public_perms_json := request_json.get("anonymous_users")) is not None: + if public_perms_json is not None and not flask.current_app.config['ENABLE_ANONYMOUS_USERS']: + return { + "message": "anonymous users are disabled" + }, 400 + if not isinstance(public_perms_json, str): + return { + "message": "JSON string for permission required" + }, 400 + try: + public_permissions = Permissions.from_name(public_perms_json) + except ValueError: + return { + "message": "Permissions name for anonymous users required" + }, 400 + + if public_permissions not in {Permissions.NONE, Permissions.READ}: + return { + "message": 'expected "none" or "read"' + }, 400 + + update_functions.append( + functools.partial( + object_permissions.set_object_permissions_for_anonymous_users, + object_id, + public_permissions, + ) + ) + + for item in update_functions: + item() + + return all_object_permissions_dict_to_json( + object_permissions.get_all_object_permissions(object_id=object_id) + ), 200 + + +class CopyObjectsPermissions(Resource): + @multi_auth.login_required + def post(self) -> ResponseData: + user_id = flask.g.user.id + user = users.get_user(user_id=user_id) + if user.is_readonly: + return { + "message": "No permission to copy object's permissions" + }, 403 + request_json = flask.request.get_json(force=True) + if isinstance(request_json, dict): + request_json = [request_json] + elif not isinstance(request_json, list): + return { + "message": "JSON list or dict body required" + }, 400 + try: + for item in request_json: + if not isinstance(item, dict): + return { + "message": "JSON list or dict body required" + }, 400 + source_object_id = item.get("source_object_id", None) + target_object_id = item.get("target_object_id", None) + if not isinstance(source_object_id, int): + return { + "message": "source_object_id for JSON object required" + }, 400 + if not isinstance(target_object_id, int): + return { + "message": "target_object_id for JSON object required" + }, 400 + # Validating all incoming object ids before changing any permission + for object_id in [source_object_id, target_object_id]: + try: + objects.check_object_exists(object_id) + except errors.ObjectDoesNotExistError: + return { + "message": f"object {object_id} does not exist" + }, 404 + + # Assure highest permission for source object is atleast READ + # to view object's permission + if max( + object_permissions.get_object_permissions_for_all_users(source_object_id), + object_permissions.get_user_object_permissions( + object_id=source_object_id, + user_id=user_id, + ) + ) < Permissions.READ: + return { + "message": f"missing object permission for source object {source_object_id}" + }, 403 + + # Assure highest permission for target object is atleast GRANT + # to allow permission-manipulation + if object_permissions.get_user_object_permissions( + object_id=target_object_id, + user_id=user_id, + ) < Permissions.GRANT: + return { + "message": f"missing object permission for target object {target_object_id}" + }, 403 + + except ValueError: + return { + "message": "body parameters 'source' and 'target' required to be object-ids" + }, 400 + except KeyError: + return { + "message": "JSON dict body required 'source' and 'target' key" + }, 400 + + for item in request_json: + object_permissions.copy_permissions( + source_object_id=item["source_object_id"], + target_object_id=item["target_object_id"], + ) + return "", 200 + + class UserObjectPermissions(Resource): @object_permissions_required(Permissions.READ) def get(self, object_id: int, user_id: int) -> ResponseData: diff --git a/sampledb/logic/object_permissions.py b/sampledb/logic/object_permissions.py index 5047f16ab..5f4d62dec 100644 --- a/sampledb/logic/object_permissions.py +++ b/sampledb/logic/object_permissions.py @@ -97,6 +97,44 @@ def set_project_object_permissions(object_id: int, project_id: int, permissions: object_permissions.set_permissions_for_project(resource_id=object_id, project_id=project_id, permissions=permissions) +class AllObjectPermissionsDict(typing.TypedDict): + users: typing.Dict[int, Permissions] + basic_groups: typing.Dict[int, Permissions] + projects: typing.Dict[int, Permissions] + authenticated: Permissions + anonymous: Permissions + + +def get_all_object_permissions(object_id: int) -> AllObjectPermissionsDict: + user_permissions = get_object_permissions_for_users( + object_id=object_id, + include_instrument_responsible_users=False, + include_groups=False, + include_projects=False, + include_admin_permissions=False, + ) + basic_group_permissions = get_object_permissions_for_groups( + object_id=object_id, + include_projects=False, + ) + project_permissions = get_object_permissions_for_projects( + object_id=object_id, + ) + all_user_permissions = get_object_permissions_for_all_users( + object_id=object_id + ) + anonymous_user_permissions = get_object_permissions_for_anonymous_users( + object_id=object_id + ) + return { + "users": user_permissions, + "basic_groups": basic_group_permissions, + "projects": project_permissions, + "authenticated": all_user_permissions, + "anonymous": anonymous_user_permissions + } + + def _get_object_responsible_user_ids(object_id: int) -> typing.List[int]: object = objects.get_object(object_id) if object.action_id is None: diff --git a/tests/api/server/test_object_permissions.py b/tests/api/server/test_object_permissions.py index 7f89bf041..ad09ace33 100644 --- a/tests/api/server/test_object_permissions.py +++ b/tests/api/server/test_object_permissions.py @@ -487,3 +487,219 @@ def test_set_anonymous_user_object_permissions(flask_server, auth, object_id): assert r.json() == { "message": "anonymous users are disabled" } + + +def test_get_all_object_permissions(flask_server, auth, user, other_user, object_id): + group_id = sampledb.logic.groups.create_group("Example Group", "", other_user.id).id + sampledb.logic.object_permissions.set_group_object_permissions(object_id, group_id, sampledb.models.Permissions.WRITE) + project_id = sampledb.logic.projects.create_project("Example Project", "", other_user.id).id + sampledb.logic.object_permissions.set_project_object_permissions(object_id, project_id, sampledb.models.Permissions.GRANT) + sampledb.logic.projects.add_group_to_project(project_id, group_id, sampledb.models.Permissions.GRANT) + sampledb.logic.object_permissions.set_user_object_permissions(object_id=object_id, user_id=user.id, permissions=sampledb.models.Permissions.READ) + sampledb.logic.object_permissions.set_user_object_permissions(object_id=object_id, user_id=other_user.id, permissions=sampledb.models.Permissions.READ) + + r_all = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), auth=auth) + r_users = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions/users'.format(object_id), auth=auth) + r_groups = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions/groups'.format(object_id), auth=auth) + r_projects = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions/projects'.format(object_id), auth=auth) + r_authenticated = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions/authenticated_users'.format(object_id), auth=auth) + r_anonymous = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions/anonymous_users'.format(object_id), auth=auth) + + assert r_all.status_code == 200 + assert r_all.json()["users"] == r_users.json() + assert r_all.json()["groups"] == r_groups.json() + assert r_all.json()["projects"] == r_projects.json() + assert r_all.json()["authenticated_users"] == r_authenticated.json() + assert r_all.json()["anonymous_users"] == (r_anonymous.json() if r_anonymous.ok else 'none') + + r_all = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id+1), auth=auth) + assert r_all.status_code == 404 + + +def test_set_object_permissions(flask_server, auth, user, other_user, object_id): + # Apply perms for 2 out of 3 (users, groups, projects) + # Stick with the first permission; + # Remove (change) perm for second; + # Add perm for third (user, group, project) + sampledb.logic.object_permissions.set_user_object_permissions( + object_id, + user.id, + sampledb.models.Permissions.GRANT + ) + sampledb.logic.object_permissions.set_user_object_permissions( + object_id, + other_user.id, + sampledb.models.Permissions.GRANT + ) + with flask_server.app.app_context(): + third_user = sampledb.models.User(name="Third User", email="third_user@example.com", type=sampledb.models.UserType.PERSON) + sampledb.db.session.add(third_user) + sampledb.db.session.commit() + assert third_user.id is not None + group_ids = [sampledb.logic.groups.create_group(f"Example Group{i}", "", other_user.id).id for i in range(3)] + [ + sampledb.logic.object_permissions.set_group_object_permissions( + object_id, + group_id, + sampledb.models.Permissions.WRITE, + ) + for group_id in group_ids + ] + project_ids = [sampledb.logic.projects.create_project(f"Example Project{i}", "", other_user.id).id for i in range(3)] + [ + sampledb.logic.object_permissions.set_project_object_permissions( + object_id, + project_id, + sampledb.models.Permissions.GRANT, + ) + for project_id in project_ids + ] + + sampledb.logic.object_permissions.set_object_permissions_for_all_users(object_id,sampledb.models.Permissions.READ) + flask_server.app.config['ENABLE_ANONYMOUS_USERS'] = True + sampledb.logic.object_permissions.set_object_permissions_for_anonymous_users(object_id,sampledb.models.Permissions.READ) + request_json = { + "users": { + other_user.id: "none", + third_user.id: "read", + }, + "groups": { + group_ids[1]: 'none', + group_ids[2]: 'read', + }, + "projects": { + project_ids[1]: 'none', + project_ids[2]: 'read', + }, + } + r = requests.put(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), json=request_json, auth=auth) + assert r.status_code == 200 + + assert all([ + r.json()["users"][str(user.id)] == 'grant', + r.json()["users"].get(str(other_user.id)) == None, + r.json()["users"][str(third_user.id)] == "read" + ]) + assert all([ + r.json()["groups"][str(group_ids[0])] == 'write', + r.json()["groups"].get(str(group_ids[1])) == None, + r.json()["groups"][str(group_ids[2])] == 'read', + ]) + assert all([ + r.json()["projects"][str(project_ids[0])] == 'grant', + r.json()["projects"].get(str(project_ids[1])) == None, + r.json()["projects"][str(project_ids[2])] == 'read', + ]) + assert r.json()["authenticated_users"] == 'read' + assert r.json()["anonymous_users"] == 'read' + + request_json = { + 'authenticated_users': 'none', + 'anonymous_users': 'none', + } + r = requests.put(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), json=request_json, auth=auth) + assert r.status_code == 200 + assert r.json()["authenticated_users"] == 'none' + assert r.json()["anonymous_users"] == 'none' + + r = requests.put(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id+1), json=request_json, auth=auth) + assert r.status_code == 404 + + flask_server.app.config['ENABLE_ANONYMOUS_USERS'] = False + request_json = { + 'anonymous_users': 'read', + } + r = requests.put(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), json=request_json, auth=auth) + assert r.status_code == 400 + assert r.json() == { + "message": "anonymous users are disabled" + } + + +def test_copy_object_permissions(flask_server, auth, user, other_user, object_id, action): + data = { + 'name': { + '_type': 'text', + 'text': 'Example' + } + } + sampledb.logic.object_permissions.set_user_object_permissions(object_id, other_user.id, sampledb.models.Permissions.GRANT) + group_id = sampledb.logic.groups.create_group("Example Group", "", other_user.id).id + sampledb.logic.object_permissions.set_group_object_permissions(object_id, group_id, sampledb.models.Permissions.WRITE) + project_id = sampledb.logic.projects.create_project("Example Project", "", other_user.id).id + sampledb.logic.object_permissions.set_project_object_permissions(object_id, project_id, sampledb.models.Permissions.WRITE) + other_object = sampledb.logic.objects.create_object(action_id=action.id, data=data, user_id=user.id) + other_object_id = other_object.object_id + request_json = { + "source_object_id": object_id, + "target_object_id": other_object_id, + } + r = requests.post(flask_server.base_url + '/api/v1/objects/permissions/copy/', json=request_json, auth=auth) + assert r.status_code == 200 + r = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(other_object_id), json=request_json, auth=auth) + assert r.status_code == 200 + perms_source_object = sampledb.api.server.object_permissions.all_object_permissions_dict_to_json(sampledb.logic.object_permissions.get_all_object_permissions(object_id)) + perms_source_object["users"] = {str(k): v for k,v in perms_source_object["users"].items()} if perms_source_object.get("users") else None + perms_source_object["groups"] = {str(k): v for k,v in perms_source_object["groups"].items()} if perms_source_object.get("groups") else None + perms_source_object["projects"] = {str(k): v for k,v in perms_source_object["projects"].items()} if perms_source_object.get("projects") else None + + for key in perms_source_object.keys(): + assert r.json()[key] == perms_source_object[key] + + # Multiple copy requests at once + b_object = sampledb.logic.objects.create_object(action_id=action.id, data=data, user_id=user.id) + b_object_id = b_object.object_id + c_object = sampledb.logic.objects.create_object(action_id=action.id, data=data, user_id=user.id) + c_object_id = c_object.object_id + + # Make sure c_object and b_object do not have the same permissions as object + perms_source = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), json=request_json, auth=auth).json() + perms_b_obj = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(b_object_id), json=request_json, auth=auth).json() + perms_c_obj = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(c_object_id), json=request_json, auth=auth).json() + assert not all([perms_source.get(key, None) == perms_b_obj.get(key, None) for key in perms_source.keys()]) # Fails if object and b_object have same permissions + assert not all([perms_source.get(key, None) == perms_c_obj.get(key, None) for key in perms_source.keys()]) # Failes if object and c_object have same permissions + + request_json = [ + { + "source_object_id": object_id, + "target_object_id": b_object_id, + }, + { + "source_object_id": object_id, + "target_object_id": c_object_id, + }, + ] + r = requests.post(flask_server.base_url + '/api/v1/objects/permissions/copy/', json=request_json, auth=auth) + assert r.status_code == 200 + perms_source = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(object_id), json=request_json, auth=auth).json() + perms_b_obj = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(b_object_id), json=request_json, auth=auth).json() + perms_c_obj = requests.get(flask_server.base_url + 'api/v1/objects/{}/permissions'.format(c_object_id), json=request_json, auth=auth).json() + for key in perms_source.keys(): + assert perms_source[key] == perms_b_obj[key] + assert perms_source[key] == perms_c_obj[key] + + request_json = { + "source_object_id": object_id, + "target_object_id": other_object_id, + } + + # Not enough permission to view source object's permissions + sampledb.logic.object_permissions.set_user_object_permissions(object_id, user.id, sampledb.models.Permissions.NONE) + r = requests.post(flask_server.base_url + '/api/v1/objects/permissions/copy/', json=request_json, auth=auth) + assert r.status_code == 403 + + # Not enough permissions to perform a grant action on target object + sampledb.logic.object_permissions.set_user_object_permissions(object_id, user.id, sampledb.models.Permissions.READ) + sampledb.logic.object_permissions.set_user_object_permissions(other_object_id, user.id, sampledb.models.Permissions.READ) + r = requests.post(flask_server.base_url + '/api/v1/objects/permissions/copy/', json=request_json, auth=auth) + assert r.status_code == 403 + + # Not existing object + last_object = sampledb.logic.objects.create_object(action_id=action.id, data=data, user_id=user.id) + last_object_id = last_object.object_id + request_json = { + "source_object_id": object_id, + "target_object_id": last_object_id+1, + } + r = requests.post(flask_server.base_url + '/api/v1/objects/permissions/copy/', json=request_json, auth=auth) + assert r.status_code == 404 diff --git a/tests/frontend/test_status_codes.py b/tests/frontend/test_status_codes.py index 9a916c800..f0e0bcedc 100644 --- a/tests/frontend/test_status_codes.py +++ b/tests/frontend/test_status_codes.py @@ -331,6 +331,7 @@ def test_status_codes(flask_server, user, driver): f'api/v1/objects/{object_id}/files/{file_id}': 200, f'api/v1/objects/{object_id}/locations/': 200, f'api/v1/objects/{object_id}/locations/{object_location_assignment_index}': 200, + f'api/v1/objects/{object_id}/permissions/': 200, f'api/v1/objects/{object_id}/permissions/anonymous_users': 400, # 400 because anonymous users are disabled f'api/v1/objects/{object_id}/permissions/authenticated_users': 200, f'api/v1/objects/{object_id}/permissions/groups/': 200, @@ -349,6 +350,7 @@ def test_status_codes(flask_server, user, driver): f'api/v1/objects/{other_object_id}/files/{file_id}': 404, f'api/v1/objects/{other_object_id}/locations/': 200, f'api/v1/objects/{other_object_id}/locations/{object_location_assignment_index}': 404, + f'api/v1/objects/{other_object_id}/permissions/': 200, f'api/v1/objects/{other_object_id}/permissions/anonymous_users': 400, # 400 because anonymous users are disabled f'api/v1/objects/{other_object_id}/permissions/authenticated_users': 200, f'api/v1/objects/{other_object_id}/permissions/groups/': 200,