elody 0.0.62__py3-none-any.whl → 0.0.162__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. elody/client.py +70 -23
  2. elody/csv.py +118 -21
  3. elody/error_codes.py +112 -0
  4. elody/exceptions.py +14 -0
  5. elody/job.py +95 -0
  6. elody/loader.py +33 -5
  7. elody/migration/__init__.py +0 -0
  8. elody/migration/base_object_migrator.py +18 -0
  9. elody/object_configurations/__init__.py +0 -0
  10. elody/object_configurations/base_object_configuration.py +174 -0
  11. elody/object_configurations/elody_configuration.py +144 -0
  12. elody/object_configurations/job_configuration.py +65 -0
  13. elody/policies/authentication/base_user_tenant_validation_policy.py +48 -15
  14. elody/policies/authorization/filter_generic_objects_policy.py +68 -22
  15. elody/policies/authorization/filter_generic_objects_policy_v2.py +166 -0
  16. elody/policies/authorization/generic_object_detail_policy.py +10 -27
  17. elody/policies/authorization/generic_object_mediafiles_policy.py +82 -0
  18. elody/policies/authorization/generic_object_metadata_policy.py +8 -27
  19. elody/policies/authorization/generic_object_relations_policy.py +12 -29
  20. elody/policies/authorization/generic_object_request_policy.py +56 -55
  21. elody/policies/authorization/generic_object_request_policy_v2.py +133 -0
  22. elody/policies/authorization/mediafile_derivatives_policy.py +92 -0
  23. elody/policies/authorization/mediafile_download_policy.py +71 -0
  24. elody/policies/authorization/multi_tenant_policy.py +14 -6
  25. elody/policies/authorization/tenant_request_policy.py +3 -1
  26. elody/policies/helpers.py +37 -0
  27. elody/policies/permission_handler.py +217 -211
  28. elody/policies/tenant_id_resolver.py +375 -0
  29. elody/schemas.py +0 -3
  30. elody/util.py +165 -11
  31. {elody-0.0.62.dist-info → elody-0.0.162.dist-info}/METADATA +16 -11
  32. elody-0.0.162.dist-info/RECORD +47 -0
  33. {elody-0.0.62.dist-info → elody-0.0.162.dist-info}/WHEEL +1 -1
  34. {elody-0.0.62.dist-info → elody-0.0.162.dist-info}/top_level.txt +1 -0
  35. tests/__init_.py +0 -0
  36. tests/data.py +74 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_csv.py +410 -0
  39. tests/unit/test_utils.py +293 -0
  40. elody-0.0.62.dist-info/RECORD +0 -27
  41. {elody-0.0.62.dist-info → elody-0.0.162.dist-info}/LICENSE +0 -0
@@ -1,61 +1,34 @@
1
+ import re as regex
2
+
3
+ from configuration import get_object_configuration_mapper # pyright: ignore
1
4
  from copy import deepcopy
2
- from elody.util import get_item_metadata_value
5
+ from elody.error_codes import ErrorCode, get_error_code, get_read
6
+ from elody.util import flatten_dict, interpret_flat_key
3
7
  from inuits_policy_based_auth.contexts.user_context import UserContext
8
+ from logging_elody.log import log # pyright: ignore
4
9
 
5
10
 
6
11
  _permissions = {}
12
+ _placeholders = ["X_TENANT_ID", "TENANT_DEFINING_ENTITY_ID"]
7
13
 
8
14
 
9
- def set_permissions(permissions: dict):
15
+ def set_permissions(permissions: dict, placeholders: list[str] = []):
10
16
  global _permissions
11
17
  _permissions = permissions
18
+ _placeholders.extend(placeholders)
12
19
 
13
20
 
14
21
  def get_permissions(role: str, user_context: UserContext):
15
22
  permissions = deepcopy(_permissions)
16
- placeholders = ["X_TENANT_ID", "TENANT_DEFINING_ENTITY_ID"]
17
23
 
18
- for placeholder in placeholders:
24
+ for placeholder_key in _placeholders:
25
+ placeholder_value = user_context.bag.get(placeholder_key.lower())
19
26
  permissions = __replace_permission_placeholders(
20
- permissions, placeholder, user_context.bag[placeholder.lower()]
27
+ permissions, placeholder_key, placeholder_value
21
28
  )
22
29
  return permissions.get(role, {}) # pyright: ignore
23
30
 
24
31
 
25
- def handle_single_item_request(
26
- user_context: UserContext, item, permissions, crud, request_body={}
27
- ):
28
- is_allowed_to_crud_item = __is_allowed_to_crud_item(item, permissions, crud)
29
- if not is_allowed_to_crud_item:
30
- return is_allowed_to_crud_item
31
-
32
- return __is_allowed_to_crud_item_keys(
33
- user_context, item, permissions, crud, request_body
34
- )
35
-
36
-
37
- def get_mask_protected_content_post_request_hook(
38
- user_context: UserContext, permissions, item_type=None
39
- ):
40
- def __post_request_hook(
41
- response, *, is_single_item_response=False, single_item_sub_route_key=""
42
- ):
43
- if is_single_item_response:
44
- if single_item_sub_route_key in ["metadata", "relations"]:
45
- items = [{single_item_sub_route_key: response[0], "type": item_type}]
46
- else:
47
- items = [response[0]]
48
- else:
49
- items = response[0]["results"]
50
-
51
- for item in items:
52
- __is_allowed_to_crud_item_keys(user_context, item, permissions, "read")
53
-
54
- return response
55
-
56
- return __post_request_hook
57
-
58
-
59
32
  def __replace_permission_placeholders(data, placeholder_key, placeholder_value):
60
33
  if isinstance(data, dict):
61
34
  for key, value in data.items():
@@ -68,195 +41,228 @@ def __replace_permission_placeholders(data, placeholder_key, placeholder_value):
68
41
  for item in data
69
42
  ]
70
43
  elif isinstance(data, str):
71
- data = data.replace(placeholder_key, placeholder_value)
44
+ if isinstance(placeholder_value, str):
45
+ data = data.replace(placeholder_key, placeholder_value)
46
+ elif isinstance(placeholder_value, list) and data == placeholder_key:
47
+ data = placeholder_value
72
48
  return data
73
49
 
74
50
 
75
- def __is_allowed_to_crud_item(item, permissions, crud):
76
- if item["type"] not in permissions[crud].keys():
77
- return None
78
-
79
- restrictions = permissions[crud][item["type"]].get("restrictions", {})
51
+ def handle_single_item_request(
52
+ user_context: UserContext, item, permissions, crud, request_body: dict = {}
53
+ ):
54
+ try:
55
+ item_in_storage_format, flat_item, object_lists, restrictions_schema = (
56
+ __prepare_item_for_permission_check(item, permissions, crud)
57
+ )
80
58
 
81
- for metadata in restrictions.get("metadata", []):
82
- value = get_item_metadata_value(item, metadata["key"])
83
- if isinstance(value, str):
84
- if value not in metadata["value"]:
85
- return None
86
- elif isinstance(value, list):
87
- for expected_value in metadata["value"]:
88
- if expected_value in value:
89
- return True
90
- return None
59
+ is_allowed_to_crud_item = (
60
+ __is_allowed_to_crud_item(flat_item, restrictions_schema)
61
+ if flat_item
62
+ else None
63
+ )
64
+ if not is_allowed_to_crud_item:
65
+ return is_allowed_to_crud_item
66
+
67
+ return __is_allowed_to_crud_item_keys(
68
+ user_context,
69
+ item_in_storage_format,
70
+ flat_item,
71
+ restrictions_schema,
72
+ crud,
73
+ object_lists,
74
+ flatten_dict(object_lists, request_body),
75
+ )
76
+ except Exception as exception:
77
+ log.debug(
78
+ f"{exception.__class__.__name__}: {str(exception)}",
79
+ item.get("storage_format", item),
80
+ )
81
+ if crud != "read":
82
+ log.debug(f"Request body: {request_body}", {})
83
+ raise exception
91
84
 
92
- for relation in restrictions.get("relations", []):
93
- keys = _get_relation_keys(item, relation["key"])
94
- for expected_value in relation["value"]:
95
- if expected_value in keys:
96
- return True
97
- return None
98
-
99
- for root in restrictions.get("root", []):
100
- value = item[root["key"]]
101
- if isinstance(value, str):
102
- if value not in root["value"]:
103
- return None
104
- elif isinstance(value, list):
105
- for expected_value in root["value"]:
106
- if expected_value in value:
107
- return True
108
- return None
109
85
 
86
+ def mask_protected_content_post_request_hook(user_context: UserContext, permissions):
87
+ def __post_request_hook(response):
88
+ items = []
89
+ for item in response["results"]:
90
+ try:
91
+ (
92
+ item_in_storage_format,
93
+ flat_item,
94
+ object_lists,
95
+ restrictions_schema,
96
+ ) = __prepare_item_for_permission_check(item, permissions, "read")
97
+ if not flat_item:
98
+ continue
99
+
100
+ __is_allowed_to_crud_item_keys(
101
+ user_context,
102
+ item_in_storage_format,
103
+ flat_item,
104
+ restrictions_schema,
105
+ "read",
106
+ object_lists,
107
+ )
108
+ items.append(user_context.bag["requested_item"])
109
+ except Exception as exception:
110
+ log.debug(
111
+ f"{exception.__class__.__name__}: {str(exception)}",
112
+ item.get("storage_format", item),
113
+ )
114
+ raise exception
115
+
116
+ response["results"] = items
117
+ return response
110
118
 
111
- return True
119
+ return __post_request_hook
112
120
 
113
121
 
114
- def _get_relation_keys(item: dict, relation_type: str):
115
- return [
116
- relation["key"]
117
- for relation in item["relations"]
118
- if relation["type"] == relation_type
119
- ]
122
+ def __prepare_item_for_permission_check(item, permissions, crud):
123
+ item = deepcopy(item.get("storage_format", item))
124
+ if item.get("type", "") not in permissions[crud].keys():
125
+ return item, None, None, None
120
126
 
127
+ config = get_object_configuration_mapper().get(item["type"])
128
+ object_lists = config.document_info().get("object_lists", {})
129
+ flat_item = flatten_dict(object_lists, item)
121
130
 
122
- def __is_allowed_to_crud_item_keys(
123
- user_context: UserContext, item, permissions, crud, request_body={}
124
- ):
125
- user_context.bag["soft_call_response_body"] = []
126
- keys_permissions, negate_condition = __get_keys_permissions(
127
- permissions[crud][item["type"]]
131
+ return (
132
+ item,
133
+ flat_item,
134
+ object_lists,
135
+ __get_restrictions_schema(flat_item, permissions, crud),
128
136
  )
129
137
 
130
- if keys_permissions:
131
- initial_item = deepcopy(item)
132
- for key in item.keys():
133
- data_key = ""
134
- if key == "metadata":
135
- data_key = "key"
136
- (
137
- permission_key_data_map,
138
- data_value_key,
139
- ) = __determine_data_per_permission_key(item, key, data_key, "value")
140
- elif key == "relations":
141
- data_key = "type"
142
- (
143
- permission_key_data_map,
144
- data_value_key,
145
- ) = __determine_data_per_permission_key(item, key, data_key, "key")
146
- else:
147
- (
148
- permission_key_data_map,
149
- data_value_key,
150
- ) = __determine_data_per_permission_key(item, key)
151
-
152
- for permission_key, data in permission_key_data_map.items():
153
- if __is_not_valid_request_on_key(
154
- initial_item,
155
- keys_permissions,
156
- negate_condition,
157
- key,
158
- permission_key,
159
- data_value_key,
160
- ):
161
- if crud == "read":
162
- data[data_value_key] = "[protected content]"
163
- else:
164
- if key == data_value_key:
165
- if request_body.get(key):
166
- user_context.bag["soft_call_response_body"].append(key)
167
- else:
168
- for element in request_body.get(key, []):
169
- if f"{key}.{element[data_key]}" == permission_key:
170
- user_context.bag["soft_call_response_body"].append(
171
- permission_key
172
- )
173
-
174
- return len(user_context.bag["soft_call_response_body"]) == 0
175
-
176
-
177
- def __get_keys_permissions(item_permissions):
178
- if item_permissions.get("keys"):
179
- negate_condition = False
180
- keys_permissions = item_permissions["keys"].get("allowed_only", {})
181
- if not keys_permissions:
182
- negate_condition = True
183
- keys_permissions = item_permissions["keys"].get("disallowed_only", {})
184
-
185
- return keys_permissions, negate_condition
186
-
187
- return None, None
188
-
189
-
190
- def __determine_data_per_permission_key(item, root_key, data_key="", data_value_key=""):
191
- key_data_map = {}
192
-
193
- if data_key and data_value_key:
194
- for data in item[root_key]:
195
- key_data_map.update({f"{root_key}.{data[data_key]}": data})
196
- value_key = data_value_key
197
- else:
198
- key_data_map.update({root_key: item})
199
- value_key = root_key
200
-
201
- return key_data_map, value_key
202
-
203
-
204
- def __is_not_valid_request_on_key(
205
- initial_item,
206
- keys_permissions,
207
- negate_condition,
208
- root_key,
209
- key,
210
- data_value_key,
211
- ):
212
- if root_key == data_value_key:
213
- is_in_keys_permissions = lambda: key in keys_permissions.keys()
214
- else:
215
- is_in_keys_permissions = (
216
- lambda: key in keys_permissions.keys()
217
- or f"{root_key}.*" in keys_permissions.keys()
218
- )
219
138
 
220
- if is_in_keys_permissions():
221
- if __check_key_conditions_disallow_request(
222
- initial_item,
223
- keys_permissions.get(key, keys_permissions.get(f"{root_key}.*")),
224
- negate_condition,
225
- ):
226
- return True
227
- elif not negate_condition:
228
- return True
139
+ def __get_restrictions_schema(flat_item, permissions, crud):
140
+ schema_type = flat_item.get("schema.type", "elody")
141
+ schema_version = flat_item.get("schema.version", "1")
142
+ schema = f"{schema_type}:{schema_version}"
143
+
144
+ schemas = permissions[crud][flat_item["type"]]
145
+ if restrictions_schema := schemas.get(schema):
146
+ return restrictions_schema
147
+
148
+ for schema in reversed(schemas.keys()):
149
+ if regex.match(f"^{schema_type}:[0-9]{1,3}?$", schema):
150
+ break
151
+ schema = None
152
+ return schemas[schema] if schemas and schema else {}
153
+
154
+
155
+ def __is_allowed_to_crud_item(flat_item, restrictions_schema):
156
+ restrictions = restrictions_schema.get("object_restrictions", {})
229
157
 
230
- return False
158
+ for restricted_key, restricting_values in restrictions.items():
159
+ restricted_key = restricted_key.split(":")[1]
160
+ item_value_in_restricting_values = __item_value_in_values(
161
+ flat_item, restricted_key, restricting_values
162
+ )
163
+ if not item_value_in_restricting_values:
164
+ return None
165
+
166
+ return True
231
167
 
232
168
 
233
- def __check_key_conditions_disallow_request(
234
- initial_item, key_conditions, negate_condition
169
+ def __is_allowed_to_crud_item_keys(
170
+ user_context: UserContext,
171
+ item_in_storage_format,
172
+ flat_item,
173
+ restrictions_schema,
174
+ crud,
175
+ object_lists,
176
+ flat_request_body: dict = {},
235
177
  ):
236
- multiple_conditions = len(key_conditions) > 1
237
- return_value = False if negate_condition and len(key_conditions) == 0 else True
238
- switch_return_value = False
239
-
240
- for key_condition in key_conditions:
241
- metadata_key, expected_value = key_condition.split("==")
242
- actual_value = get_item_metadata_value(initial_item, metadata_key)
243
- if isinstance(actual_value, list):
244
- is_condition_met = lambda: expected_value in actual_value
245
- else:
246
- is_condition_met = (
247
- lambda: f"{expected_value}".lower() == f"{actual_value}".lower()
178
+ user_context.bag["restricted_keys"] = []
179
+ restrictions = restrictions_schema.get("key_restrictions", {})
180
+
181
+ for restricted_key, restricting_conditions in restrictions.items():
182
+ restricted_key = restricted_key.split(":")[1]
183
+ condition_match = True
184
+ for condition_key, condition_values in restricting_conditions.items():
185
+ condition_match = __item_value_in_values(
186
+ flat_item, condition_key, condition_values, flat_request_body
187
+ )
188
+ if not condition_match:
189
+ break
190
+
191
+ if condition_match:
192
+ if crud == "read":
193
+ keys_info = interpret_flat_key(restricted_key, object_lists)
194
+ for info in keys_info:
195
+ if info["object_list"]:
196
+ element = __get_element_from_object_list_of_item(
197
+ item_in_storage_format,
198
+ info["key"],
199
+ info["object_key"],
200
+ object_lists,
201
+ )
202
+ item_in_storage_format[info["key"]].remove(element)
203
+ break
204
+ else:
205
+ try:
206
+ del item_in_storage_format[keys_info[0]["key"]][
207
+ keys_info[1]["key"]
208
+ ]
209
+ except KeyError:
210
+ pass
211
+ else:
212
+ if flat_request_body.get(restricted_key):
213
+ user_context.bag["restricted_keys"].append(restricted_key)
214
+
215
+ user_context.bag["requested_item"] = item_in_storage_format
216
+ return len(user_context.bag["restricted_keys"]) == 0
217
+
218
+
219
+ def __item_value_in_values(flat_item, key, values: list, flat_request_body: dict = {}):
220
+ negate_condition = False
221
+ is_optional = False
222
+
223
+ if key[0] == "!":
224
+ key = key[1:]
225
+ negate_condition = True
226
+ if key[0] == "?":
227
+ key = key[1:]
228
+ is_optional = True
229
+
230
+ try:
231
+ item_value = flat_request_body.get(key, flat_item[key])
232
+ except KeyError:
233
+ if not is_optional:
234
+ raise Exception(
235
+ f"{get_error_code(ErrorCode.METADATA_KEY_UNDEFINED, get_read())} | key:{key} | document:{flat_item.get('_id', flat_item["type"])} - Key {key} not found in document {flat_item.get('_id', flat_item["type"])}. Either prefix the key with '?' in your permission configuration to make it an optional restriction, or patch the document to include the key. '?' will allow access if key does not exist, '!?' will deny access if key does not exist."
248
236
  )
237
+ return not negate_condition
238
+
239
+ expected_values = []
240
+ for value in values:
241
+ if flat_item_key_value := flat_item.get(value):
242
+ value = flat_item_key_value
243
+ if isinstance(value, list):
244
+ expected_values.extend(value)
245
+ else:
246
+ expected_values.append(value)
247
+
248
+ if isinstance(item_value, (str, int, float, bool)):
249
+ if negate_condition:
250
+ return item_value not in expected_values
251
+ else:
252
+ return item_value in expected_values
253
+ elif isinstance(item_value, list):
254
+ for expected_value in expected_values:
255
+ if expected_value in item_value:
256
+ return True != negate_condition
257
+ return False != negate_condition
249
258
 
250
- if is_condition_met():
251
- if negate_condition:
252
- if not multiple_conditions:
253
- return return_value
254
- elif not switch_return_value:
255
- return_value = not return_value
256
- switch_return_value = True
257
- elif not negate_condition:
258
- return return_value
259
- elif switch_return_value:
260
- return False
261
-
262
- return not return_value
259
+ raise Exception(f"Invalid item_value: {item_value}")
260
+
261
+
262
+ def __get_element_from_object_list_of_item(
263
+ item: dict, object_list: str, key: str, object_lists: dict
264
+ ):
265
+ for element in item[object_list]:
266
+ if element[object_lists[object_list]] == key:
267
+ return element
268
+ return {}