From 34ba5bd919d3ddff660fa42735c6697737d5e967 Mon Sep 17 00:00:00 2001 From: Kelly Lockhart <2926089+kelockhart@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:55:06 -0500 Subject: [PATCH] Fixed bug w/ not checking permissions in copy (#174) * Fixed bug w/ not checking permissions in copy * Check read access for secondary libraries * added access check for public libraries and fixed unittests * adding a not * fixing tests * changed it to stop using merge * created variables and changed logic * added tests and removed write access check * fixed bug * changing accesses hoping it's final --------- Co-authored-by: femalves --- biblib/tests/unit_tests/test_webservices.py | 274 +++++++++++++++++++- biblib/views/base_view.py | 26 +- biblib/views/operations_view.py | 85 +++--- 3 files changed, 351 insertions(+), 34 deletions(-) diff --git a/biblib/tests/unit_tests/test_webservices.py b/biblib/tests/unit_tests/test_webservices.py index 63efab0..4aacb3a 100644 --- a/biblib/tests/unit_tests/test_webservices.py +++ b/biblib/tests/unit_tests/test_webservices.py @@ -718,7 +718,7 @@ def test_operations_view_post_types(self): self.assertEqual(response.json['error'], NO_LIBRARY_SPECIFIED_ERROR['body']) post_data['action'] = 'copy' - post_data['libraries'] = ['lib1', 'lib2'] + post_data['libraries'] = [library_id_1, library_id_2] response = self.client.post( url, data=json.dumps(post_data), @@ -727,6 +727,276 @@ def test_operations_view_post_types(self): self.assertEqual(response.status_code, TOO_MANY_LIBRARIES_SPECIFIED_ERROR['number']) self.assertEqual(response.json['error'], TOO_MANY_LIBRARIES_SPECIFIED_ERROR['body']) + def test_permissions_on_operations_view_post_types(self): + + # Create admin user to give out permissions + admin_user = UserShop() + + # Create stub user to use libraries + stub_user = UserShop() + + stub_library_1 = LibraryShop() + stub_library_2 = LibraryShop() + stub_library_3 = LibraryShop(public=True) + + # Make libraries + # Make the libraries with admin as admin + url = url_for('userview') + response_1 = self.client.post( + url, + data=stub_library_1.user_view_post_data_json, + headers=admin_user.headers + ) + library_id_1 = response_1.json['id'] + + response_2 = self.client.post( + url, + data=stub_library_2.user_view_post_data_json, + headers=admin_user.headers + ) + library_id_2 = response_2.json['id'] + + response_3 = self.client.post( + url, + data=stub_library_3.user_view_post_data_json, + headers=admin_user.headers + ) + library_id_3 = response_3.json['id'] + + # Copy success: User has read access primary and write access secondary + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': True, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='copy', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 200) + + # Copy success: User is admin + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': False, 'admin': True, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': True, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='copy', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 200) + + # Copy fail: User does not have right permissions + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': True, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='copy', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 403) + + # Copy success: User does not have right permissions for primary but library is public + url = url_for('permissionview', library=library_id_3) + + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': True, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_3) + post_data = stub_library_3.operations_view_post_data(action='copy', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 200) + + # Union success: User has read access primary and read access secondary + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='union', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 200) + + # Union success: User does not have the right permissions for secondary but it's public + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_3) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='union', libraries=[library_id_3]) + post_data['name'] = 'New Library 1' + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + self.assertEqual(response.status_code, 200) + + # Union fail: User does not have right permissions + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='union', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 403) + + # Union fail: User does not have right permissions + url = url_for('permissionview', library=library_id_1) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': True, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('permissionview', library=library_id_2) + with MockEmailService(stub_user): + response = self.client.post( + url, + data=stub_user.permission_view_post_data_json({'read': False, 'write': False, 'admin': False, 'owner': False}), + headers=admin_user.headers + ) + self.assertEqual(response.status_code, 200) + + url = url_for('operationsview', library=library_id_1) + post_data = stub_library_1.operations_view_post_data(action='union', libraries=[library_id_2]) + response = self.client.post( + url, + data=json.dumps(post_data), + headers=stub_user.headers + ) + + self.assertEqual(response.status_code, 403) + + def test_document_view_put_types(self): """ Tests that the content passed to the UserView POST end point @@ -1487,7 +1757,7 @@ def _create_libraries(self, n=2, lib_data=None): lib_ids = [] libraries = [] for nl in range(n): - stub_library = LibraryShop() + stub_library = LibraryShop(public=True) # Make the libraries url = url_for('userview') diff --git a/biblib/views/base_view.py b/biblib/views/base_view.py index 7e5d897..5f079a3 100644 --- a/biblib/views/base_view.py +++ b/biblib/views/base_view.py @@ -29,6 +29,9 @@ class BaseView(Resource): #default permissions for write_access() write_allowed = ['write', 'admin', 'owner'] + # default permissions for read_access() + read_allowed = ['read', 'write', 'admin', 'owner'] + @staticmethod def helper_uuid_to_slug(library_uuid): """ @@ -223,8 +226,27 @@ def delete_access(cls, service_uid, library_id): delete_allowed = cls.helper_access_allowed(service_uid=service_uid, library_id=library_id, access_type='owner') - return delete_allowed - + return delete_allowed + + @classmethod + def read_access(cls, service_uid, library_id): + """ + Defines which type of user has read permissions to a library. + + :param service_uid: the user ID within this microservice + :param library_id: the unique ID of the library + + :return: boolean, access (True), no access (False) + """ + + for access_type in cls.read_allowed: + if cls.helper_access_allowed(service_uid=service_uid, + library_id=library_id, + access_type=access_type): + return True + + return False + @classmethod def write_access(cls, service_uid, library_id): """ diff --git a/biblib/views/operations_view.py b/biblib/views/operations_view.py index 0c5501a..a321b22 100644 --- a/biblib/views/operations_view.py +++ b/biblib/views/operations_view.py @@ -188,12 +188,6 @@ def post(self, library): user_editing_uid = \ self.helper_absolute_uid_to_service_uid(absolute_uid=user_editing) - - # Check the permissions of the user - if not self.write_access(service_uid=user_editing_uid, - library_id=library_uuid): - return err(NO_PERMISSION_ERROR) - try: data = get_post_data( request, @@ -204,34 +198,65 @@ def post(self, library): .format(request.data, error)) return err(WRONG_TYPE_ERROR) - if data['action'] in ['union', 'intersection', 'difference']: - if 'libraries' not in data: - return err(NO_LIBRARY_SPECIFIED_ERROR) - if 'name' not in data: - data['name'] = 'Untitled {0}.'.format(get_date().isoformat()) - if 'public' not in data: - data['public'] = False + action = data["action"] - if data['action'] == 'copy': - if 'libraries' not in data: + if action in ["union", "intersection", "difference"]: + if "libraries" not in data: return err(NO_LIBRARY_SPECIFIED_ERROR) - if len(data['libraries']) > 1: + if "name" not in data: + data["name"] = "Untitled {0}.".format(get_date().isoformat()) + if "public" not in data: + data["public"] = False + elif action == "copy": + if "libraries" not in data: + return err(NO_LIBRARY_SPECIFIED_ERROR) + if len(data["libraries"]) > 1: return err(TOO_MANY_LIBRARIES_SPECIFIED_ERROR) lib_names = [] + with current_app.session_scope() as session: primary = session.query(Library).filter_by(id=library_uuid).one() lib_names.append(primary.name) - if 'libraries' in data: - for lib in data['libraries']: - try: - secondary_uuid = self.helper_slug_to_uuid(lib) - except TypeError: - return err(BAD_LIBRARY_ID_ERROR) - secondary = session.query(Library).filter_by(id=secondary_uuid).one() - lib_names.append(secondary.name) - - if data['action'] == 'union': + + if action == "empty": + permission_check_primary = self.update_access( + service_uid=user_editing_uid, + library_id=library_uuid + ) + else: + permission_check_primary = primary.public or self.read_access( + service_uid=user_editing_uid, + library_id=library_uuid + ) + + if not permission_check_primary: + return err(NO_PERMISSION_ERROR) + + + secondary_libraries = data.get("libraries", []) + for lib in secondary_libraries: + try: + secondary_uuid = self.helper_slug_to_uuid(lib) + except TypeError: + return err(BAD_LIBRARY_ID_ERROR) + + secondary = session.query(Library).filter_by(id=secondary_uuid).one() + lib_names.append(secondary.name) + + if action in ["union", "intersection", "difference"]: + permission_check_secondary = secondary.public or self.read_access( + service_uid=user_editing_uid, library_id=secondary_uuid + ) + elif action == "copy": + permission_check_secondary = self.write_access( + service_uid=user_editing_uid, library_id=secondary_uuid + ) + + if not permission_check_secondary: + return err(NO_PERMISSION_ERROR) + + if action == 'union': bib_union = self.setops_libraries( library_id=library_uuid, document_data=data, @@ -263,7 +288,7 @@ def post(self, library): return library_dict, 200 - elif data['action'] == 'intersection': + elif action == 'intersection': bib_intersect = self.setops_libraries( library_id=library_uuid, document_data=data, @@ -292,7 +317,7 @@ def post(self, library): return err(WRONG_TYPE_ERROR) return library_dict, 200 - elif data['action'] == 'difference': + elif action == 'difference': bib_diff = self.setops_libraries( library_id=library_uuid, document_data=data, @@ -316,7 +341,7 @@ def post(self, library): return err(WRONG_TYPE_ERROR) return library_dict, 200 - elif data['action'] == 'copy': + elif action == 'copy': library_dict = self.copy_library( library_id=library_uuid, document_data=data @@ -333,7 +358,7 @@ def post(self, library): return library_dict, 200 - elif data['action'] == 'empty': + elif action == 'empty': library_dict = self.empty_library( library_id=library_uuid )