diff --git a/src/elody/csv.py b/src/elody/csv.py index 15661f9..789b46c 100644 --- a/src/elody/csv.py +++ b/src/elody/csv.py @@ -188,9 +188,7 @@ def __fill_objects_from_csv(self): mandatory_columns = [ v for k, v in self.index_mapping.items() if not k.startswith("?") ] - missing_columns = [ - x for x in mandatory_columns if x not in row.keys() - ] + missing_columns = [x for x in mandatory_columns if x not in row.keys()] if missing_columns: raise ColumnNotFoundException(f"{', '.join(missing_columns)}") lang = self.__determine_language(row) diff --git a/src/elody/object_configurations/base_object_configuration.py b/src/elody/object_configurations/base_object_configuration.py index f663983..bac1ecc 100644 --- a/src/elody/object_configurations/base_object_configuration.py +++ b/src/elody/object_configurations/base_object_configuration.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from copy import deepcopy from elody.migration.base_object_migrator import BaseObjectMigrator +from policy_factory import get_user_context # pyright: ignore class BaseObjectConfiguration(ABC): @@ -10,20 +11,24 @@ class BaseObjectConfiguration(ABC): @abstractmethod def crud(self): return { - "collection": "entities", - "collection_history": "history", "creator": lambda post_body, **kwargs: post_body, + "document_content_patcher": lambda *, document, content, overwrite=False, **kwargs: self._document_content_patcher( + document=document, + content=content, + overwrite=overwrite, + **kwargs, + ), "nested_matcher_builder": lambda object_lists, keys_info, value: self.__build_nested_matcher( object_lists, keys_info, value ), "post_crud_hook": lambda **kwargs: None, - "pre_crud_hook": lambda **kwargs: None, + "pre_crud_hook": lambda *, document, **kwargs: document, "storage_type": "db", } @abstractmethod def document_info(self): - return {"object_lists": {"metadata": "key", "relations": "type"}} + return {} @abstractmethod def logging(self, flat_document, **kwargs): @@ -33,7 +38,7 @@ def logging(self, flat_document, **kwargs): "schema": f"{flat_document.get('schema.type')}:{flat_document.get('schema.version')}", } try: - user_context = kwargs.get("get_user_context")() # pyright: ignore + user_context = get_user_context() info_labels["http_method"] = user_context.bag.get("http_method") info_labels["requested_endpoint"] = user_context.bag.get( "requested_endpoint" @@ -65,20 +70,46 @@ def validator(http_method, content, item, **_): return "function", validator - def _get_merged_post_body(self, post_body, document_defaults, object_list_name): - key = self.document_info()["object_lists"][object_list_name] - post_body[object_list_name] = self.__merge_object_lists( - document_defaults.get(object_list_name, []), - post_body.get(object_list_name, []), - key, + def _document_content_patcher( + self, *, document, content, overwrite=False, **kwargs + ): + raise NotImplementedError( + "Provide concrete implementation in child object configuration" ) - return post_body - def _sanitize_document(self, document, object_list_name, value_field_name): - object_list = deepcopy(document[object_list_name]) - for element in object_list: - if not element[value_field_name]: - document[object_list_name].remove(element) + def _merge_object_lists(self, source, target, object_list_key): + for target_item in target: + for source_item in source: + if source_item[object_list_key] == target_item[object_list_key]: + source.remove(source_item) + return [*source, *target] + + def _get_user_context_id(self): + try: + return get_user_context().id + except Exception: + return None + + def _sanitize_document(self, *, document, **kwargs): + sanitized_document = {} + document_deepcopy = deepcopy(document) + for key, value in document_deepcopy.items(): + if isinstance(value, dict): + sanitized_value = BaseObjectConfiguration._sanitize_document( + self, document=value + ) + if sanitized_value: + sanitized_document[key] = sanitized_value + elif value: + sanitized_document[key] = value + return sanitized_document + + def _should_create_history_object(self): + try: + get_user_context() + return bool(self.crud().get("collection_history")) + except Exception: + return False def _sort_document_keys(self, document): def sort_keys(data): @@ -98,12 +129,13 @@ def sort_keys(data): else: return data - for key, value in self.document_info()["object_lists"].items(): + for key, value in self.document_info().get("object_lists", {}).items(): if document.get(key): document[key] = sorted( document[key], key=lambda property: property[value] ) sort_keys(document) + return document def __build_nested_matcher(self, object_lists, keys_info, value, index=0): if index == 0 and not any(info["object_list"] for info in keys_info): @@ -135,10 +167,3 @@ def __build_nested_matcher(self, object_lists, keys_info, value, index=0): return elem_match if index > 0 else {info["key"]: {"$all": [elem_match]}} raise Exception(f"Unable to build nested matcher. See keys_info: {keys_info}") - - def __merge_object_lists(self, source, target, key): - for target_item in target: - for source_item in source: - if source_item[key] == target_item[key]: - source.remove(source_item) - return [*source, *target] diff --git a/src/elody/object_configurations/elody_configuration.py b/src/elody/object_configurations/elody_configuration.py index 34b8889..0020e81 100644 --- a/src/elody/object_configurations/elody_configuration.py +++ b/src/elody/object_configurations/elody_configuration.py @@ -12,6 +12,8 @@ class ElodyConfiguration(BaseObjectConfiguration): def crud(self): crud = { + "collection": "entities", + "collection_history": "history", "creator": lambda post_body, **kwargs: self._creator(post_body, **kwargs), "post_crud_hook": lambda **kwargs: self._post_crud_hook(**kwargs), "pre_crud_hook": lambda **kwargs: self._pre_crud_hook(**kwargs), @@ -37,7 +39,6 @@ def _creator( self, post_body, *, - get_user_context, flat_post_body={}, document_defaults={}, ): @@ -65,36 +66,79 @@ def _creator( "relations": [], "schema": {"type": self.SCHEMA_TYPE, "version": self.SCHEMA_VERSION}, } - if email := self.__get_email(get_user_context): - template["computed_values"]["created_by"] = email - - for key in self.document_info()["object_lists"].keys(): - post_body = self._get_merged_post_body(post_body, document_defaults, key) + if user_context_id := self._get_user_context_id(): + template["computed_values"]["created_by"] = user_context_id + + for key, object_list_key in self.document_info()["object_lists"].items(): + if not key.startswith("lookup.virtual_relations"): + post_body[key] = self._merge_object_lists( + document_defaults.get(key, []), + post_body.get(key, []), + object_list_key, + ) document = {**template, **document_defaults, **post_body} - self._sanitize_document(document, "metadata", "value") - self._sort_document_keys(document) + document = self._sanitize_document( + document=document, + object_list_name="metadata", + object_list_value_field_name="value", + ) + document = self._sort_document_keys(document) + return document + + def _document_content_patcher( + self, *, document, content, overwrite=False, **kwargs + ): + object_lists = self.document_info().get("object_lists", {}) + if overwrite: + document = content + else: + for key, value in content.items(): + if key in object_lists: + if key != "relations": + for value_element in value: + for item_element in document[key]: + if ( + item_element[object_lists[key]] + == value_element[object_lists[key]] + ): + document[key].remove(item_element) + break + document[key].extend(value) + else: + document[key] = value + return document - def _post_crud_hook(self, **_): + def _post_crud_hook(self, **kwargs): pass - def _pre_crud_hook(self, *, crud, document={}, get_user_context=None, **_): + def _pre_crud_hook(self, *, crud, document={}, **kwargs): if document: - self._sanitize_document(document, "metadata", "value") - self.__patch_document_computed_values( - crud, document, get_user_context=get_user_context + document = self._sanitize_document( + document=document, + object_list_name="metadata", + object_list_value_field_name="value", ) - self._sort_document_keys(document) - - def __get_email(self, get_user_context): - try: - return get_user_context().email - except Exception: - return None + document = self.__patch_document_computed_values(crud, document) + document = self._sort_document_keys(document) + return document - def __patch_document_computed_values(self, crud, document, **kwargs): + def _sanitize_document( + self, *, document, object_list_name, object_list_value_field_name, **kwargs + ): + sanitized_document = super()._sanitize_document(document=document) + object_list = document[object_list_name] + for element in object_list: + if not element[object_list_value_field_name]: + sanitized_document[object_list_name].remove(element) + return sanitized_document + + def __patch_document_computed_values(self, crud, document): + if not document.get("computed_values"): + document["computed_values"] = {} document["computed_values"].update({"event": crud}) document["computed_values"].update({"modified_at": datetime.now(timezone.utc)}) - if email := self.__get_email(kwargs.get("get_user_context")): + if email := self._get_user_context_id(): document["computed_values"].update({"modified_by": email}) + return document diff --git a/src/elody/object_configurations/job_configuration.py b/src/elody/object_configurations/job_configuration.py index 3a271ec..d25d0b2 100644 --- a/src/elody/object_configurations/job_configuration.py +++ b/src/elody/object_configurations/job_configuration.py @@ -16,8 +16,8 @@ def crud(self): def document_info(self): return super().document_info() - def logging(self, flat_item, **kwargs): - return super().logging(flat_item, **kwargs) + def logging(self, flat_document, **kwargs): + return super().logging(flat_document, **kwargs) def migration(self): return super().migration() diff --git a/src/elody/policies/permission_handler.py b/src/elody/policies/permission_handler.py index b847a6b..62b3a0b 100644 --- a/src/elody/policies/permission_handler.py +++ b/src/elody/policies/permission_handler.py @@ -21,9 +21,10 @@ def set_permissions(permissions: dict, placeholders: list[str] = []): def get_permissions(role: str, user_context: UserContext): permissions = deepcopy(_permissions) - for placeholder in _placeholders: + for placeholder_key in _placeholders: + placeholder_value = user_context.bag.get(placeholder_key.lower()) permissions = __replace_permission_placeholders( - permissions, placeholder, user_context.bag[placeholder.lower()] + permissions, placeholder_key, placeholder_value ) return permissions.get(role, {}) # pyright: ignore @@ -84,8 +85,8 @@ def handle_single_item_request( def mask_protected_content_post_request_hook(user_context: UserContext, permissions): def __post_request_hook(response): - items = response["results"] - for item in items: + items = [] + for item in response["results"]: try: ( item_in_storage_format, @@ -104,6 +105,7 @@ def __post_request_hook(response): "read", object_lists, ) + items.append(user_context.bag["requested_item"]) except Exception as exception: log.debug( f"{exception.__class__.__name__}: {str(exception)}", @@ -111,6 +113,7 @@ def __post_request_hook(response): ) raise exception + response["results"] = items return response return __post_request_hook @@ -122,7 +125,7 @@ def __prepare_item_for_permission_check(item, permissions, crud): return item, None, None, None config = get_object_configuration_mapper().get(item["type"]) - object_lists = config.document_info()["object_lists"] + object_lists = config.document_info().get("object_lists", {}) flat_item = flatten_dict(object_lists, item) return ( @@ -188,16 +191,23 @@ def __is_allowed_to_crud_item_keys( if condition_match: if crud == "read": keys_info = interpret_flat_key(restricted_key, object_lists) - element = item_in_storage_format for info in keys_info: if info["object_list"]: element = __get_element_from_object_list_of_item( - element, + item_in_storage_format, info["key"], info["object_key"], object_lists, ) - element[info["key"]] = "[protected content]" # pyright: ignore + item_in_storage_format[info["key"]].remove(element) + break + else: + try: + del item_in_storage_format[keys_info[0]["key"]][ + keys_info[1]["key"] + ] + except KeyError: + pass else: if flat_request_body.get(restricted_key): user_context.bag["restricted_keys"].append(restricted_key) diff --git a/src/tests/unit/test_utils.py b/src/tests/unit/test_utils.py index ff66285..e997f2a 100644 --- a/src/tests/unit/test_utils.py +++ b/src/tests/unit/test_utils.py @@ -11,7 +11,7 @@ mediafile_is_public, read_json_as_dict, parse_url_unfriendly_string, - CustomJSONEncoder + CustomJSONEncoder, ) from data import mediafile1, mediafile2 from datetime import datetime, timezone @@ -24,12 +24,14 @@ def test_default_method_with_datetime(): result = encoder.default(dt) assert result == "2023-10-01T12:00:00+00:00" + def test_default_method_with_naive_datetime(): encoder = CustomJSONEncoder() dt = datetime(2023, 10, 1, 12, 0, 0) result = encoder.default(dt) assert result == "2023-10-01T10:00:00+00:00" + def test_encode_method_with_non_datetime(): encoder = CustomJSONEncoder() obj = {"key": "value"}