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,10 +1,13 @@
1
1
  import re as regex
2
2
 
3
+ from elody.policies.helpers import get_content
4
+ from configuration import get_object_configuration_mapper # pyright: ignore
3
5
  from elody.policies.permission_handler import (
4
6
  get_permissions,
5
- get_mask_protected_content_post_request_hook,
7
+ handle_single_item_request,
8
+ mask_protected_content_post_request_hook,
6
9
  )
7
- from elody.util import get_item_metadata_value
10
+ from elody.util import interpret_flat_key
8
11
  from flask import Request # pyright: ignore
9
12
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
10
13
  from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
@@ -20,9 +23,7 @@ class GenericObjectRequestPolicy(BaseAuthorizationPolicy):
20
23
  self, policy_context: PolicyContext, user_context: UserContext, request_context
21
24
  ):
22
25
  request: Request = request_context.http_request
23
- if not user_context.auth_objects.get("token") or not regex.match(
24
- "^/[^/]+$|^/ngsi-ld/v1/entities$", request.path
25
- ):
26
+ if not regex.match("^(/[^/]+/v[0-9]+)?/[^/]+$", request.path):
26
27
  return policy_context
27
28
 
28
29
  for role in user_context.x_tenant.roles:
@@ -37,7 +38,7 @@ class GenericObjectRequestPolicy(BaseAuthorizationPolicy):
37
38
  if access_verdict != None:
38
39
  policy_context.access_verdict = access_verdict
39
40
  if not policy_context.access_verdict:
40
- return policy_context
41
+ break
41
42
 
42
43
  if policy_context.access_verdict:
43
44
  return policy_context
@@ -46,25 +47,20 @@ class GenericObjectRequestPolicy(BaseAuthorizationPolicy):
46
47
 
47
48
 
48
49
  class PostRequestRules:
49
- def apply(self, _, request: Request, permissions) -> bool | None:
50
+ def apply(
51
+ self, user_context: UserContext, request: Request, permissions
52
+ ) -> bool | None:
50
53
  if request.method != "POST":
51
54
  return None
52
-
53
- item = request.json or {}
54
- if item["type"] in permissions["create"].keys():
55
- restrictions = permissions["create"][item["type"]].get("restrictions", {})
56
- for metadata in restrictions.get("metadata", []):
57
- value = get_item_metadata_value(item, metadata["key"])
58
- if isinstance(value, str):
59
- if value not in metadata["value"]:
60
- return None
61
- elif isinstance(value, list):
62
- for expected_value in metadata["value"]:
63
- if expected_value not in value:
64
- return None
55
+ if request.args.get("dry_run", False):
56
+ return True
57
+ if regex.match(r"^/batch?$", request.path):
65
58
  return True
66
59
 
67
- return None
60
+ content = get_content(request.json, request, request.json)
61
+ return handle_single_item_request(
62
+ user_context, request.json, permissions, "create", content
63
+ )
68
64
 
69
65
 
70
66
  class GetRequestRules:
@@ -73,45 +69,27 @@ class GetRequestRules:
73
69
  ) -> bool | None:
74
70
  if request.method != "GET":
75
71
  return None
76
-
77
- type_query_parameter = request.args.get("type")
72
+ type_query_parameter = (
73
+ "mediafile"
74
+ if regex.match(r"^/mediafiles(?:\?(.*))?$", request.path)
75
+ else request.args.get("type")
76
+ )
78
77
  allowed_item_types = list(permissions["read"].keys())
79
78
  filters = []
80
79
 
81
80
  if type_query_parameter:
82
81
  if type_query_parameter in allowed_item_types:
82
+ config = get_object_configuration_mapper().get(type_query_parameter)
83
+ object_lists = config.document_info()["object_lists"]
84
+
83
85
  restrictions = permissions["read"][type_query_parameter].get(
84
- "restrictions", {}
86
+ "object_restrictions", {}
85
87
  )
86
- for parent_key in restrictions.keys():
87
- all_matches = []
88
- if parent_key == "metadata":
89
- for metadata in restrictions[parent_key]:
90
- all_matches.append(
91
- {
92
- "$elemMatch": {
93
- "key": metadata["key"],
94
- "value": {"$in": metadata["value"]},
95
- }
96
- }
97
- )
98
- filters.append({parent_key: {"$all": all_matches}})
99
- elif parent_key == "relations":
100
- for relation in restrictions[parent_key]:
101
- all_matches.append(
102
- {
103
- "$elemMatch": {
104
- "type": relation["key"],
105
- "key": {"$in": relation["value"]},
106
- }
107
- }
108
- )
109
- filters.append({parent_key: {"$all": all_matches}})
110
- elif parent_key == "root":
111
- for restriction in restrictions[parent_key]:
112
- filters.append(
113
- {restriction["key"]: {"$in": restriction["value"]}}
114
- )
88
+ for key, value in restrictions.items():
89
+ keys_info = interpret_flat_key(key, object_lists)
90
+ filters.append(
91
+ _build_nested_matcher(object_lists, keys_info, value)
92
+ )
115
93
  else:
116
94
  return None
117
95
  else:
@@ -123,7 +101,10 @@ class GetRequestRules:
123
101
  "key": user_context.bag.get(
124
102
  "tenant_defining_entity_id", user_context.x_tenant.id
125
103
  ),
126
- "type": user_context.bag["tenant_relation_type"],
104
+ "type": [
105
+ user_context.bag["tenant_relation_type"],
106
+ "belongsTo",
107
+ ],
127
108
  }
128
109
  }
129
110
  },
@@ -131,6 +112,26 @@ class GetRequestRules:
131
112
 
132
113
  user_context.access_restrictions.filters = filters
133
114
  user_context.access_restrictions.post_request_hook = (
134
- get_mask_protected_content_post_request_hook(user_context, permissions)
115
+ mask_protected_content_post_request_hook(user_context, permissions)
135
116
  )
136
117
  return True
118
+
119
+
120
+ def _build_nested_matcher(object_lists, keys_info, value, index=0):
121
+ info = keys_info[index]
122
+
123
+ if info["object_list"]:
124
+ nested_matcher = _build_nested_matcher(
125
+ object_lists, keys_info, value, index + 1
126
+ )
127
+ elem_match = {
128
+ "$elemMatch": {
129
+ object_lists[info["key"]]: info["object_key"],
130
+ keys_info[index + 1]["key"]: nested_matcher,
131
+ }
132
+ }
133
+ return elem_match if index > 0 else {info["key"]: elem_match}
134
+
135
+ if isinstance(value, list):
136
+ value = {"$in": value}
137
+ return value if index > 0 else {info["key"]: value}
@@ -0,0 +1,133 @@
1
+ import re as regex
2
+
3
+ from configuration import get_object_configuration_mapper # pyright: ignore
4
+ from elody.policies.helpers import get_content
5
+ from elody.policies.permission_handler import (
6
+ get_permissions,
7
+ handle_single_item_request,
8
+ mask_protected_content_post_request_hook,
9
+ )
10
+ from flask import Request # pyright: ignore
11
+ from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
12
+ from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
13
+ PolicyContext,
14
+ )
15
+ from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
16
+ UserContext,
17
+ )
18
+
19
+
20
+ class GenericObjectRequestPolicyV2(BaseAuthorizationPolicy):
21
+ def authorize(
22
+ self, policy_context: PolicyContext, user_context: UserContext, request_context
23
+ ):
24
+ request: Request = request_context.http_request
25
+ if not regex.match("^(/[^/]+/v[0-9]+)?/[^/]+$", request.path):
26
+ return policy_context
27
+
28
+ for role in user_context.x_tenant.roles:
29
+ permissions = get_permissions(role, user_context)
30
+ if not permissions:
31
+ continue
32
+
33
+ rules = [PostRequestRules, GetRequestRules]
34
+ access_verdict = None
35
+ for rule in rules:
36
+ access_verdict = rule().apply(user_context, request, permissions)
37
+ if access_verdict != None:
38
+ policy_context.access_verdict = access_verdict
39
+ if not policy_context.access_verdict:
40
+ break
41
+
42
+ if policy_context.access_verdict:
43
+ return policy_context
44
+
45
+ return policy_context
46
+
47
+
48
+ class PostRequestRules:
49
+ def apply(
50
+ self, user_context: UserContext, request: Request, permissions
51
+ ) -> bool | None:
52
+ if request.method != "POST":
53
+ return None
54
+
55
+ content = get_content(request.json, request, request.json)
56
+ schema_type = get_object_configuration_mapper().get(content["type"]).SCHEMA_TYPE
57
+ item = {**content, "schema": {"type": schema_type}}
58
+ return handle_single_item_request(
59
+ user_context, item, permissions, "create", content
60
+ )
61
+
62
+
63
+ class GetRequestRules:
64
+ def apply(
65
+ self, user_context: UserContext, request: Request, permissions
66
+ ) -> bool | None:
67
+ if request.method != "GET":
68
+ return None
69
+
70
+ type_query_parameter = request.args.get("type")
71
+ allowed_item_types = list(permissions["read"].keys())
72
+ filters = []
73
+
74
+ if type_query_parameter:
75
+ if type_query_parameter not in allowed_item_types:
76
+ return None
77
+ type_permissions = permissions["read"][type_query_parameter]
78
+ schemas = list(type_permissions.keys())
79
+ if len(schemas) > 0:
80
+ number_of_object_restrictions = len(
81
+ type_permissions[schemas[0]].get("object_restrictions", {}).keys()
82
+ )
83
+ for i in range(number_of_object_restrictions):
84
+ keys, values = [], []
85
+ for schema in schemas:
86
+ object_restrictions = type_permissions[schema].get(
87
+ "object_restrictions", {}
88
+ )
89
+ key = [
90
+ key
91
+ for key in object_restrictions.keys()
92
+ if key.startswith(f"{i}:")
93
+ ][0]
94
+ keys.append(f"{schema}|{key.split(':')[1]}")
95
+ values = object_restrictions[key]
96
+ filters.append(
97
+ {
98
+ "type": "selection",
99
+ "key": keys,
100
+ "value": values,
101
+ "match_exact": True,
102
+ }
103
+ )
104
+ filters.insert(0, {"type": "type", "value": type_query_parameter})
105
+ else:
106
+ filters.append(
107
+ {
108
+ "type": "selection",
109
+ "key": "type",
110
+ "value": allowed_item_types,
111
+ "match_exact": True,
112
+ }
113
+ )
114
+ if tenant_relation_type := user_context.bag.get("tenant_relation_type"):
115
+ filters.append(
116
+ {
117
+ "type": "selection",
118
+ "key": tenant_relation_type,
119
+ "value": [
120
+ "tenant:super",
121
+ user_context.bag.get(
122
+ "tenant_defining_entity_id", user_context.x_tenant.id
123
+ ),
124
+ ],
125
+ "match_exact": True,
126
+ }
127
+ )
128
+
129
+ user_context.access_restrictions.filters = filters
130
+ user_context.access_restrictions.post_request_hook = (
131
+ mask_protected_content_post_request_hook(user_context, permissions)
132
+ )
133
+ return True
@@ -0,0 +1,92 @@
1
+ import re as regex
2
+
3
+ from elody.error_codes import ErrorCode, get_error_code, get_read, get_write
4
+ from elody.policies.permission_handler import (
5
+ get_permissions,
6
+ handle_single_item_request,
7
+ )
8
+ from flask import Request # pyright: ignore
9
+ from flask_restful import abort # pyright: ignore
10
+ from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
11
+ from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
12
+ PolicyContext,
13
+ )
14
+ from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
15
+ UserContext,
16
+ )
17
+ from storage.storagemanager import StorageManager # pyright: ignore
18
+
19
+
20
+ class MediafileDerivativesPolicy(BaseAuthorizationPolicy):
21
+ def authorize(
22
+ self, policy_context: PolicyContext, user_context: UserContext, request_context
23
+ ):
24
+ request: Request = request_context.http_request
25
+ if not regex.match(r"^/mediafiles/(.+)/derivatives$", request.path):
26
+ return policy_context
27
+
28
+ view_args = request.view_args or {}
29
+ collection = view_args.get("collection", request.path.split("/")[-3])
30
+ id = view_args.get("id")
31
+ item = (
32
+ StorageManager()
33
+ .get_db_engine()
34
+ .get_item_from_collection_by_id(collection, id)
35
+ )
36
+ if not item:
37
+ abort(
38
+ 404,
39
+ message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND_IN_COLLECTION, get_read())} | id:{id} | collection: {collection} - Item with id {id} doesn't exist in collection {collection}",
40
+ )
41
+
42
+ for role in user_context.x_tenant.roles:
43
+ permissions = get_permissions(role, user_context)
44
+ if not permissions:
45
+ continue
46
+
47
+ rules = [
48
+ PostRequestRules,
49
+ GetRequestRules,
50
+ ]
51
+ access_verdict = None
52
+ for rule in rules:
53
+ access_verdict = rule().apply(item, user_context, request, permissions)
54
+ if access_verdict != None:
55
+ policy_context.access_verdict = access_verdict
56
+ if not policy_context.access_verdict:
57
+ break
58
+
59
+ if policy_context.access_verdict:
60
+ return policy_context
61
+
62
+ return policy_context
63
+
64
+
65
+ class PostRequestRules:
66
+ def apply(
67
+ self, item, user_context: UserContext, request: Request, permissions
68
+ ) -> bool | None:
69
+ if request.method != "POST":
70
+ return None
71
+
72
+ return handle_single_item_request(user_context, item, permissions, "create")
73
+
74
+
75
+ class GetRequestRules:
76
+ def apply(
77
+ self, item, user_context: UserContext, request: Request, permissions
78
+ ) -> bool | None:
79
+ if request.method != "GET":
80
+ return None
81
+
82
+ return handle_single_item_request(user_context, item, permissions, "read")
83
+
84
+
85
+ class DeleteRequestRules:
86
+ def apply(
87
+ self, item, user_context: UserContext, request: Request, permissions
88
+ ) -> bool | None:
89
+ if request.method != "DELETE":
90
+ return None
91
+
92
+ return handle_single_item_request(user_context, item, permissions, "delete")
@@ -0,0 +1,71 @@
1
+ import re as regex
2
+
3
+ from elody.error_codes import ErrorCode, get_error_code, get_read
4
+ from elody.policies.permission_handler import (
5
+ get_permissions,
6
+ handle_single_item_request,
7
+ )
8
+ from flask import Request # pyright: ignore
9
+ from flask_restful import abort # pyright: ignore
10
+ from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
11
+ from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
12
+ PolicyContext,
13
+ )
14
+ from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
15
+ UserContext,
16
+ )
17
+ from storage.storagemanager import StorageManager # pyright: ignore
18
+
19
+
20
+ class MediafileDownloadPolicy(BaseAuthorizationPolicy):
21
+ def authorize(
22
+ self, policy_context: PolicyContext, user_context: UserContext, request_context
23
+ ):
24
+ request: Request = request_context.http_request
25
+ if not regex.match(r"^/mediafiles/(.+)/download$", request.path):
26
+ return policy_context
27
+
28
+ view_args = request.view_args or {}
29
+ collection = view_args.get("collection", request.path.split("/")[-3])
30
+ id = view_args.get("id")
31
+ item = (
32
+ StorageManager()
33
+ .get_db_engine()
34
+ .get_item_from_collection_by_id(collection, id)
35
+ )
36
+ if not item:
37
+ abort(
38
+ 404,
39
+ message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND_IN_COLLECTION, get_read())} | id:{id} | collection:{collection} - Item with id {id} doesn't exist in collection {collection}",
40
+ )
41
+
42
+ for role in user_context.x_tenant.roles:
43
+ permissions = get_permissions(role, user_context)
44
+ if not permissions:
45
+ continue
46
+
47
+ rules = [
48
+ GetRequestRules,
49
+ ]
50
+ access_verdict = None
51
+ for rule in rules:
52
+ access_verdict = rule().apply(item, user_context, request, permissions)
53
+ if access_verdict != None:
54
+ policy_context.access_verdict = access_verdict
55
+ if not policy_context.access_verdict:
56
+ break
57
+
58
+ if policy_context.access_verdict:
59
+ return policy_context
60
+
61
+ return policy_context
62
+
63
+
64
+ class GetRequestRules:
65
+ def apply(
66
+ self, item, user_context: UserContext, request: Request, permissions
67
+ ) -> bool | None:
68
+ if request.method != "GET":
69
+ return None
70
+
71
+ return handle_single_item_request(user_context, item, permissions, "read")
@@ -1,3 +1,5 @@
1
+ from elody.error_codes import ErrorCode, get_error_code, get_read, get_write
2
+ from configuration import get_collection_mapper
1
3
  from flask_restful import abort
2
4
  from inuits_policy_based_auth import BaseAuthorizationPolicy, RequestContext
3
5
  from inuits_policy_based_auth.contexts import UserContext, PolicyContext
@@ -41,18 +43,24 @@ class MultiTenantPolicy(BaseAuthorizationPolicy):
41
43
  policy_context.access_verdict = True
42
44
  if item_id:
43
45
  storage = StorageManager().get_db_engine()
44
- collection = request.path.split("/")[1]
46
+ request_name = request.path.split("/")[1]
47
+ collection = get_collection_mapper().get(request_name, request_name)
45
48
  item = storage.get_item_from_collection_by_id(collection, item_id)
46
49
  if not item:
47
50
  abort(
48
51
  404,
49
- message=f"Item with id {id} doesn't exist in collection {collection}",
52
+ message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND_IN_COLLECTION, get_read())} | id:{id} | collection:{collection} - Item with id {id} doesn't exist in collection {collection}",
50
53
  )
51
54
  item_relations = storage.get_collection_item_relations(collection, item_id)
52
- if not any(
53
- x
54
- for x in item_relations
55
- if x["type"] == "isIn" and x["key"] == user_context.x_tenant.raw["_id"]
55
+ if (
56
+ item.get("type") != "ticket"
57
+ and not any(
58
+ x
59
+ for x in item_relations
60
+ if x["type"] == "isIn"
61
+ and x["key"] == user_context.x_tenant.raw["_id"]
62
+ )
63
+ and collection != "mediafiles"
56
64
  ):
57
65
  policy_context.access_verdict = False
58
66
  if "/filter" in request.path:
@@ -1,3 +1,5 @@
1
+ import re as regex
2
+
1
3
  from elody.policies.permission_handler import get_permissions
2
4
  from flask import Request # pyright: ignore
3
5
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
@@ -6,7 +8,7 @@ from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
6
8
  class TenantRequestPolicy(BaseAuthorizationPolicy):
7
9
  def authorize(self, policy_context, user_context, request_context):
8
10
  request: Request = request_context.http_request
9
- if not user_context.auth_objects.get("token") or request.path != "/tenants":
11
+ if not regex.match("^(/[^/]+/v[0-9]+)?/tenants$", request.path):
10
12
  return policy_context
11
13
 
12
14
  set_restricting_filter = True
@@ -0,0 +1,37 @@
1
+ from elody.error_codes import ErrorCode, get_error_code, get_read
2
+ from configuration import get_object_configuration_mapper # pyright: ignore
3
+ from flask_restful import abort # pyright: ignore
4
+ from serialization.serialize import serialize # pyright: ignore
5
+
6
+
7
+ def get_content(item, request, content):
8
+ return serialize(
9
+ content,
10
+ type=item.get("type"),
11
+ from_format=serialize.get_format(
12
+ (request.view_args or {}).get("spec", "elody"), request.args
13
+ ),
14
+ to_format=get_object_configuration_mapper().get(item["type"]).SCHEMA_TYPE,
15
+ )
16
+
17
+
18
+ def get_item(storage_manager, user_context_bag, view_args):
19
+ view_args = view_args or {}
20
+ id = view_args.get("id")
21
+ resolve_collections = user_context_bag.get("collection_resolver")
22
+ if not resolve_collections:
23
+ abort(
24
+ 403,
25
+ message=f"{get_error_code(ErrorCode.UNDEFINED_COLLECTION_RESOLVER, get_read())} - Collection resolver not defined for user.",
26
+ )
27
+ collections = resolve_collections(collection=view_args.get("collection"), id=id)
28
+ for collection in collections:
29
+ if item := storage_manager.get_db_engine().get_item_from_collection_by_id(
30
+ collection, id
31
+ ):
32
+ return item
33
+ else:
34
+ abort(
35
+ 404,
36
+ message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{id} - Item with id {id} does not exist.",
37
+ )