elody 0.0.63__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 -199
  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.63.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.63.dist-info → elody-0.0.162.dist-info}/WHEEL +1 -1
  34. {elody-0.0.63.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.63.dist-info/RECORD +0 -27
  41. {elody-0.0.63.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,183 +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
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
+ )
78
58
 
79
- restrictions = permissions[crud][item["type"]].get("restrictions", {})
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
80
84
 
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
91
85
 
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
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
98
118
 
99
- return True
119
+ return __post_request_hook
100
120
 
101
121
 
102
- def _get_relation_keys(item: dict, relation_type: str):
103
- return [
104
- relation["key"]
105
- for relation in item["relations"]
106
- if relation["type"] == relation_type
107
- ]
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
108
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)
109
130
 
110
- def __is_allowed_to_crud_item_keys(
111
- user_context: UserContext, item, permissions, crud, request_body={}
112
- ):
113
- user_context.bag["soft_call_response_body"] = []
114
- keys_permissions, negate_condition = __get_keys_permissions(
115
- permissions[crud][item["type"]]
131
+ return (
132
+ item,
133
+ flat_item,
134
+ object_lists,
135
+ __get_restrictions_schema(flat_item, permissions, crud),
116
136
  )
117
137
 
118
- if keys_permissions:
119
- initial_item = deepcopy(item)
120
- for key in item.keys():
121
- data_key = ""
122
- if key == "metadata":
123
- data_key = "key"
124
- (
125
- permission_key_data_map,
126
- data_value_key,
127
- ) = __determine_data_per_permission_key(item, key, data_key, "value")
128
- elif key == "relations":
129
- data_key = "type"
130
- (
131
- permission_key_data_map,
132
- data_value_key,
133
- ) = __determine_data_per_permission_key(item, key, data_key, "key")
134
- else:
135
- (
136
- permission_key_data_map,
137
- data_value_key,
138
- ) = __determine_data_per_permission_key(item, key)
139
-
140
- for permission_key, data in permission_key_data_map.items():
141
- if __is_not_valid_request_on_key(
142
- initial_item,
143
- keys_permissions,
144
- negate_condition,
145
- key,
146
- permission_key,
147
- data_value_key,
148
- ):
149
- if crud == "read":
150
- data[data_value_key] = "[protected content]"
151
- else:
152
- if key == data_value_key:
153
- if request_body.get(key):
154
- user_context.bag["soft_call_response_body"].append(key)
155
- else:
156
- for element in request_body.get(key, []):
157
- if f"{key}.{element[data_key]}" == permission_key:
158
- user_context.bag["soft_call_response_body"].append(
159
- permission_key
160
- )
161
-
162
- return len(user_context.bag["soft_call_response_body"]) == 0
163
-
164
-
165
- def __get_keys_permissions(item_permissions):
166
- if item_permissions.get("keys"):
167
- negate_condition = False
168
- keys_permissions = item_permissions["keys"].get("allowed_only", {})
169
- if not keys_permissions:
170
- negate_condition = True
171
- keys_permissions = item_permissions["keys"].get("disallowed_only", {})
172
-
173
- return keys_permissions, negate_condition
174
-
175
- return None, None
176
-
177
-
178
- def __determine_data_per_permission_key(item, root_key, data_key="", data_value_key=""):
179
- key_data_map = {}
180
-
181
- if data_key and data_value_key:
182
- for data in item[root_key]:
183
- key_data_map.update({f"{root_key}.{data[data_key]}": data})
184
- value_key = data_value_key
185
- else:
186
- key_data_map.update({root_key: item})
187
- value_key = root_key
188
-
189
- return key_data_map, value_key
190
-
191
-
192
- def __is_not_valid_request_on_key(
193
- initial_item,
194
- keys_permissions,
195
- negate_condition,
196
- root_key,
197
- key,
198
- data_value_key,
199
- ):
200
- if root_key == data_value_key:
201
- is_in_keys_permissions = lambda: key in keys_permissions.keys()
202
- else:
203
- is_in_keys_permissions = (
204
- lambda: key in keys_permissions.keys()
205
- or f"{root_key}.*" in keys_permissions.keys()
206
- )
207
138
 
208
- if is_in_keys_permissions():
209
- if __check_key_conditions_disallow_request(
210
- initial_item,
211
- keys_permissions.get(key, keys_permissions.get(f"{root_key}.*")),
212
- negate_condition,
213
- ):
214
- return True
215
- elif not negate_condition:
216
- 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 {}
217
153
 
218
- return False
219
154
 
155
+ def __is_allowed_to_crud_item(flat_item, restrictions_schema):
156
+ restrictions = restrictions_schema.get("object_restrictions", {})
157
+
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
220
165
 
221
- def __check_key_conditions_disallow_request(
222
- initial_item, key_conditions, negate_condition
166
+ return True
167
+
168
+
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 = {},
223
177
  ):
224
- multiple_conditions = len(key_conditions) > 1
225
- return_value = False if negate_condition and len(key_conditions) == 0 else True
226
- switch_return_value = False
227
-
228
- for key_condition in key_conditions:
229
- metadata_key, expected_value = key_condition.split("==")
230
- actual_value = get_item_metadata_value(initial_item, metadata_key)
231
- if isinstance(actual_value, list):
232
- is_condition_met = lambda: expected_value in actual_value
233
- else:
234
- is_condition_met = (
235
- 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."
236
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
237
258
 
238
- if is_condition_met():
239
- if negate_condition:
240
- if not multiple_conditions:
241
- return return_value
242
- elif not switch_return_value:
243
- return_value = not return_value
244
- switch_return_value = True
245
- elif not negate_condition:
246
- return return_value
247
- elif switch_return_value:
248
- return False
249
-
250
- 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 {}