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.
- elody/client.py +70 -23
- elody/csv.py +118 -21
- elody/error_codes.py +112 -0
- elody/exceptions.py +14 -0
- elody/job.py +95 -0
- elody/loader.py +33 -5
- elody/migration/__init__.py +0 -0
- elody/migration/base_object_migrator.py +18 -0
- elody/object_configurations/__init__.py +0 -0
- elody/object_configurations/base_object_configuration.py +174 -0
- elody/object_configurations/elody_configuration.py +144 -0
- elody/object_configurations/job_configuration.py +65 -0
- elody/policies/authentication/base_user_tenant_validation_policy.py +48 -15
- elody/policies/authorization/filter_generic_objects_policy.py +68 -22
- elody/policies/authorization/filter_generic_objects_policy_v2.py +166 -0
- elody/policies/authorization/generic_object_detail_policy.py +10 -27
- elody/policies/authorization/generic_object_mediafiles_policy.py +82 -0
- elody/policies/authorization/generic_object_metadata_policy.py +8 -27
- elody/policies/authorization/generic_object_relations_policy.py +12 -29
- elody/policies/authorization/generic_object_request_policy.py +56 -55
- elody/policies/authorization/generic_object_request_policy_v2.py +133 -0
- elody/policies/authorization/mediafile_derivatives_policy.py +92 -0
- elody/policies/authorization/mediafile_download_policy.py +71 -0
- elody/policies/authorization/multi_tenant_policy.py +14 -6
- elody/policies/authorization/tenant_request_policy.py +3 -1
- elody/policies/helpers.py +37 -0
- elody/policies/permission_handler.py +217 -199
- elody/policies/tenant_id_resolver.py +375 -0
- elody/schemas.py +0 -3
- elody/util.py +165 -11
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/METADATA +16 -11
- elody-0.0.162.dist-info/RECORD +47 -0
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/WHEEL +1 -1
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/top_level.txt +1 -0
- tests/__init_.py +0 -0
- tests/data.py +74 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_csv.py +410 -0
- tests/unit/test_utils.py +293 -0
- elody-0.0.63.dist-info/RECORD +0 -27
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/LICENSE +0 -0
elody/loader.py
CHANGED
|
@@ -2,7 +2,7 @@ import elody.util as util
|
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
6
6
|
from importlib import import_module
|
|
7
7
|
from inuits_policy_based_auth.exceptions import (
|
|
8
8
|
PolicyFactoryException,
|
|
@@ -17,9 +17,32 @@ def load_apps(flask_app, logger):
|
|
|
17
17
|
flask_app.register_blueprint(api_bp)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def load_jobs(scheduler, logger):
|
|
21
|
+
apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST"), logger)
|
|
22
|
+
for app in apps:
|
|
23
|
+
for job, job_properties in apps[app].get("jobs", {}).items():
|
|
24
|
+
try:
|
|
25
|
+
job_class = __get_class_from_module(
|
|
26
|
+
import_module(f"apps.{app}.cron_jobs.{job}")
|
|
27
|
+
)
|
|
28
|
+
scheduler.add_job(
|
|
29
|
+
job_class(),
|
|
30
|
+
CronTrigger.from_crontab(
|
|
31
|
+
job_properties.get("expression", "0 0 * * *")
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
except ModuleNotFoundError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_policies(
|
|
39
|
+
policy_factory, logger, permissions: dict = {}, placeholders: list[str] = []
|
|
40
|
+
):
|
|
21
41
|
if permissions:
|
|
22
|
-
set_permissions
|
|
42
|
+
from elody.policies.permission_handler import set_permissions
|
|
43
|
+
|
|
44
|
+
set_permissions(permissions, placeholders)
|
|
45
|
+
|
|
23
46
|
apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST", ""), logger)
|
|
24
47
|
for app in apps:
|
|
25
48
|
try:
|
|
@@ -43,6 +66,7 @@ def load_policies(policy_factory, logger, permissions={}):
|
|
|
43
66
|
|
|
44
67
|
|
|
45
68
|
def load_queues(logger):
|
|
69
|
+
import_module("resources.queues")
|
|
46
70
|
apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST"), logger)
|
|
47
71
|
for app in apps:
|
|
48
72
|
try:
|
|
@@ -66,11 +90,15 @@ def __get_class(app, auth_type, policy_module_name):
|
|
|
66
90
|
pass
|
|
67
91
|
else:
|
|
68
92
|
raise ModuleNotFoundError(f"Policy {policy_module_name} not found")
|
|
69
|
-
|
|
70
|
-
policy = getattr(module, policy_class_name)
|
|
93
|
+
policy = __get_class_from_module(module)
|
|
71
94
|
return policy
|
|
72
95
|
|
|
73
96
|
|
|
97
|
+
def __get_class_from_module(module):
|
|
98
|
+
class_name = module.__name__.split(".")[-1].title().replace("_", "")
|
|
99
|
+
return getattr(module, class_name)
|
|
100
|
+
|
|
101
|
+
|
|
74
102
|
def __instantiate_authentication_policy(policy_module_name, policy, logger):
|
|
75
103
|
allow_anonymous_users = os.getenv("ALLOW_ANONYMOUS_USERS", False) in [
|
|
76
104
|
"True",
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class BaseObjectMigrator:
|
|
2
|
+
def __init__(self, *, status, silent=False):
|
|
3
|
+
self._status = status
|
|
4
|
+
self._silent = silent
|
|
5
|
+
|
|
6
|
+
@property
|
|
7
|
+
def status(self):
|
|
8
|
+
return self._status
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def silent(self):
|
|
12
|
+
return self._silent
|
|
13
|
+
|
|
14
|
+
def bulk_migrate(self, *, dry_run=False): # pyright: ignore
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def lazy_migrate(self, item, *, dry_run=False): # pyright: ignore
|
|
18
|
+
return item
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from elody.migration.base_object_migrator import BaseObjectMigrator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseObjectConfiguration(ABC):
|
|
7
|
+
SCHEMA_TYPE = "elody"
|
|
8
|
+
SCHEMA_VERSION = 1
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def crud(self):
|
|
12
|
+
return {
|
|
13
|
+
"creator": lambda post_body, **kwargs: post_body,
|
|
14
|
+
"document_content_patcher": lambda *, document, content, overwrite=False, **kwargs: self._document_content_patcher(
|
|
15
|
+
document=document,
|
|
16
|
+
content=content,
|
|
17
|
+
overwrite=overwrite,
|
|
18
|
+
**kwargs,
|
|
19
|
+
),
|
|
20
|
+
"nested_matcher_builder": lambda object_lists, keys_info, value: self.__build_nested_matcher(
|
|
21
|
+
object_lists, keys_info, value
|
|
22
|
+
),
|
|
23
|
+
"post_crud_hook": lambda **kwargs: None,
|
|
24
|
+
"pre_crud_hook": lambda *, document, **kwargs: document,
|
|
25
|
+
"storage_type": "db",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def document_info(self):
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def logging(self, flat_document, **kwargs):
|
|
34
|
+
info_labels = {
|
|
35
|
+
"uuid": flat_document.get("_id"),
|
|
36
|
+
"type": flat_document.get("type"),
|
|
37
|
+
"schema": f"{flat_document.get('schema.type')}:{flat_document.get('schema.version')}",
|
|
38
|
+
}
|
|
39
|
+
try:
|
|
40
|
+
from policy_factory import get_user_context # pyright: ignore
|
|
41
|
+
|
|
42
|
+
user_context = get_user_context()
|
|
43
|
+
info_labels["http_method"] = user_context.bag.get("http_method")
|
|
44
|
+
info_labels["requested_endpoint"] = user_context.bag.get(
|
|
45
|
+
"requested_endpoint"
|
|
46
|
+
)
|
|
47
|
+
info_labels["full_path"] = user_context.bag.get("full_path")
|
|
48
|
+
info_labels["preferred_username"] = user_context.preferred_username
|
|
49
|
+
info_labels["email"] = user_context.email
|
|
50
|
+
info_labels["user_roles"] = ", ".join(user_context.x_tenant.roles)
|
|
51
|
+
info_labels["x_tenant"] = user_context.x_tenant.id
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return {"info_labels": info_labels, "loki_indexed_info_labels": {}}
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def migration(self):
|
|
58
|
+
return BaseObjectMigrator(status="disabled")
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def serialization(self, from_format, to_format):
|
|
62
|
+
def serializer(document, **_):
|
|
63
|
+
return document
|
|
64
|
+
|
|
65
|
+
return serializer
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def validation(self):
|
|
69
|
+
def validator(http_method, content, item, **_):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
return "function", validator
|
|
73
|
+
|
|
74
|
+
def _document_content_patcher(
|
|
75
|
+
self, *, document, content, overwrite=False, **kwargs
|
|
76
|
+
):
|
|
77
|
+
raise NotImplementedError(
|
|
78
|
+
"Provide concrete implementation in child object configuration"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _merge_object_lists(self, source, target, object_list_key):
|
|
82
|
+
for target_item in target:
|
|
83
|
+
for source_item in source:
|
|
84
|
+
if source_item[object_list_key] == target_item[object_list_key]:
|
|
85
|
+
source.remove(source_item)
|
|
86
|
+
return [*source, *target]
|
|
87
|
+
|
|
88
|
+
def _get_user_context_id(self):
|
|
89
|
+
try:
|
|
90
|
+
from policy_factory import get_user_context # pyright: ignore
|
|
91
|
+
|
|
92
|
+
return get_user_context().id
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _sanitize_document(self, *, document, **kwargs):
|
|
97
|
+
sanitized_document = {}
|
|
98
|
+
document_deepcopy = deepcopy(document)
|
|
99
|
+
for key, value in document_deepcopy.items():
|
|
100
|
+
if isinstance(value, dict):
|
|
101
|
+
sanitized_value = BaseObjectConfiguration._sanitize_document(
|
|
102
|
+
self, document=value
|
|
103
|
+
)
|
|
104
|
+
if sanitized_value:
|
|
105
|
+
sanitized_document[key] = sanitized_value
|
|
106
|
+
elif value:
|
|
107
|
+
sanitized_document[key] = value
|
|
108
|
+
return sanitized_document
|
|
109
|
+
|
|
110
|
+
def _should_create_history_object(self):
|
|
111
|
+
try:
|
|
112
|
+
from policy_factory import get_user_context # pyright: ignore
|
|
113
|
+
|
|
114
|
+
get_user_context()
|
|
115
|
+
return bool(self.crud().get("collection_history"))
|
|
116
|
+
except Exception:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def _sort_document_keys(self, document):
|
|
120
|
+
def sort_keys(data):
|
|
121
|
+
if isinstance(data, dict):
|
|
122
|
+
sorted_items = {key: data.pop(key) for key in sorted(data.keys())}
|
|
123
|
+
for key, value in sorted_items.items():
|
|
124
|
+
data[key] = sort_keys(value)
|
|
125
|
+
return data
|
|
126
|
+
elif isinstance(data, list):
|
|
127
|
+
if all(isinstance(i, str) for i in data):
|
|
128
|
+
data.sort()
|
|
129
|
+
return data
|
|
130
|
+
else:
|
|
131
|
+
for index, item in enumerate(data):
|
|
132
|
+
data[index] = sort_keys(item)
|
|
133
|
+
return data
|
|
134
|
+
else:
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
for key, value in self.document_info().get("object_lists", {}).items():
|
|
138
|
+
if document.get(key):
|
|
139
|
+
document[key] = sorted(
|
|
140
|
+
document[key], key=lambda property: property[value]
|
|
141
|
+
)
|
|
142
|
+
sort_keys(document)
|
|
143
|
+
return document
|
|
144
|
+
|
|
145
|
+
def __build_nested_matcher(self, object_lists, keys_info, value, index=0):
|
|
146
|
+
if index == 0 and not any(info["object_list"] for info in keys_info):
|
|
147
|
+
if value in ["ANY_MATCH", "NONE_MATCH"]:
|
|
148
|
+
value = {"$exists": value == "ANY_MATCH"}
|
|
149
|
+
return {".".join(info["key"] for info in keys_info): value}
|
|
150
|
+
|
|
151
|
+
info = keys_info[index]
|
|
152
|
+
|
|
153
|
+
if info["object_list"]:
|
|
154
|
+
elem_match = {
|
|
155
|
+
"$elemMatch": {
|
|
156
|
+
object_lists[info["object_list"]]: info["object_key"],
|
|
157
|
+
**self.__build_nested_matcher(
|
|
158
|
+
object_lists, keys_info[index + 1 :], value, 0
|
|
159
|
+
),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if value in ["ANY_MATCH", "NONE_MATCH"]:
|
|
163
|
+
elem_match_with_exists_operator = deepcopy(elem_match)
|
|
164
|
+
del elem_match["$elemMatch"][keys_info[index + 1]["key"]]
|
|
165
|
+
if value == "NONE_MATCH":
|
|
166
|
+
return {
|
|
167
|
+
"NOR_MATCHER": [
|
|
168
|
+
{info["key"]: {"$all": [elem_match]}},
|
|
169
|
+
{info["key"]: {"$all": [elem_match_with_exists_operator]}},
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
return elem_match if index > 0 else {info["key"]: {"$all": [elem_match]}}
|
|
173
|
+
|
|
174
|
+
raise Exception(f"Unable to build nested matcher. See keys_info: {keys_info}")
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from elody.object_configurations.base_object_configuration import (
|
|
3
|
+
BaseObjectConfiguration,
|
|
4
|
+
)
|
|
5
|
+
from elody.util import flatten_dict
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ElodyConfiguration(BaseObjectConfiguration):
|
|
10
|
+
SCHEMA_TYPE = "elody"
|
|
11
|
+
SCHEMA_VERSION = 1
|
|
12
|
+
|
|
13
|
+
def crud(self):
|
|
14
|
+
crud = {
|
|
15
|
+
"collection": "entities",
|
|
16
|
+
"collection_history": "history",
|
|
17
|
+
"creator": lambda post_body, **kwargs: self._creator(post_body, **kwargs),
|
|
18
|
+
"post_crud_hook": lambda **kwargs: self._post_crud_hook(**kwargs),
|
|
19
|
+
"pre_crud_hook": lambda **kwargs: self._pre_crud_hook(**kwargs),
|
|
20
|
+
}
|
|
21
|
+
return {**super().crud(), **crud}
|
|
22
|
+
|
|
23
|
+
def document_info(self):
|
|
24
|
+
return {"object_lists": {"metadata": "key", "relations": "type"}}
|
|
25
|
+
|
|
26
|
+
def logging(self, flat_document, **kwargs):
|
|
27
|
+
return super().logging(flat_document, **kwargs)
|
|
28
|
+
|
|
29
|
+
def migration(self):
|
|
30
|
+
return super().migration()
|
|
31
|
+
|
|
32
|
+
def serialization(self, from_format, to_format):
|
|
33
|
+
return super().serialization(from_format, to_format)
|
|
34
|
+
|
|
35
|
+
def validation(self):
|
|
36
|
+
return super().validation()
|
|
37
|
+
|
|
38
|
+
def _creator(
|
|
39
|
+
self,
|
|
40
|
+
post_body,
|
|
41
|
+
*,
|
|
42
|
+
flat_post_body={},
|
|
43
|
+
document_defaults={},
|
|
44
|
+
):
|
|
45
|
+
if not flat_post_body:
|
|
46
|
+
flat_post_body = flatten_dict(
|
|
47
|
+
self.document_info()["object_lists"], post_body
|
|
48
|
+
)
|
|
49
|
+
_id = document_defaults.get("_id", str(uuid4()))
|
|
50
|
+
|
|
51
|
+
identifiers = []
|
|
52
|
+
for property in self.document_info().get("identifier_properties", []):
|
|
53
|
+
if identifier := flat_post_body.get(f"metadata.{property}.value"):
|
|
54
|
+
identifiers.append(identifier)
|
|
55
|
+
|
|
56
|
+
template = {
|
|
57
|
+
"_id": _id,
|
|
58
|
+
"computed_values": {
|
|
59
|
+
"created_at": datetime.now(timezone.utc),
|
|
60
|
+
"event": "create",
|
|
61
|
+
},
|
|
62
|
+
"identifiers": list(
|
|
63
|
+
set([_id, *identifiers, *document_defaults.pop("identifiers", [])])
|
|
64
|
+
),
|
|
65
|
+
"metadata": [],
|
|
66
|
+
"relations": [],
|
|
67
|
+
"schema": {"type": self.SCHEMA_TYPE, "version": self.SCHEMA_VERSION},
|
|
68
|
+
}
|
|
69
|
+
if user_context_id := self._get_user_context_id():
|
|
70
|
+
template["computed_values"]["created_by"] = user_context_id
|
|
71
|
+
|
|
72
|
+
for key, object_list_key in self.document_info()["object_lists"].items():
|
|
73
|
+
if not key.startswith("lookup.virtual_relations"):
|
|
74
|
+
post_body[key] = self._merge_object_lists(
|
|
75
|
+
document_defaults.get(key, []),
|
|
76
|
+
post_body.get(key, []),
|
|
77
|
+
object_list_key,
|
|
78
|
+
)
|
|
79
|
+
document = {**template, **document_defaults, **post_body}
|
|
80
|
+
|
|
81
|
+
document = self._sanitize_document(
|
|
82
|
+
document=document,
|
|
83
|
+
object_list_name="metadata",
|
|
84
|
+
object_list_value_field_name="value",
|
|
85
|
+
)
|
|
86
|
+
document = self._sort_document_keys(document)
|
|
87
|
+
return document
|
|
88
|
+
|
|
89
|
+
def _document_content_patcher(
|
|
90
|
+
self, *, document, content, overwrite=False, **kwargs
|
|
91
|
+
):
|
|
92
|
+
object_lists = self.document_info().get("object_lists", {})
|
|
93
|
+
if overwrite:
|
|
94
|
+
document = content
|
|
95
|
+
else:
|
|
96
|
+
for key, value in content.items():
|
|
97
|
+
if key in object_lists:
|
|
98
|
+
if key != "relations":
|
|
99
|
+
for value_element in value:
|
|
100
|
+
for item_element in document[key]:
|
|
101
|
+
if (
|
|
102
|
+
item_element[object_lists[key]]
|
|
103
|
+
== value_element[object_lists[key]]
|
|
104
|
+
):
|
|
105
|
+
document[key].remove(item_element)
|
|
106
|
+
break
|
|
107
|
+
document[key].extend(value)
|
|
108
|
+
else:
|
|
109
|
+
document[key] = value
|
|
110
|
+
|
|
111
|
+
return document
|
|
112
|
+
|
|
113
|
+
def _post_crud_hook(self, **kwargs):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def _pre_crud_hook(self, *, crud, document={}, **kwargs):
|
|
117
|
+
if document:
|
|
118
|
+
document = self._sanitize_document(
|
|
119
|
+
document=document,
|
|
120
|
+
object_list_name="metadata",
|
|
121
|
+
object_list_value_field_name="value",
|
|
122
|
+
)
|
|
123
|
+
document = self.__patch_document_computed_values(crud, document)
|
|
124
|
+
document = self._sort_document_keys(document)
|
|
125
|
+
return document
|
|
126
|
+
|
|
127
|
+
def _sanitize_document(
|
|
128
|
+
self, *, document, object_list_name, object_list_value_field_name, **kwargs
|
|
129
|
+
):
|
|
130
|
+
sanitized_document = super()._sanitize_document(document=document)
|
|
131
|
+
object_list = document[object_list_name]
|
|
132
|
+
for element in object_list:
|
|
133
|
+
if not element[object_list_value_field_name]:
|
|
134
|
+
sanitized_document[object_list_name].remove(element)
|
|
135
|
+
return sanitized_document
|
|
136
|
+
|
|
137
|
+
def __patch_document_computed_values(self, crud, document):
|
|
138
|
+
if not document.get("computed_values"):
|
|
139
|
+
document["computed_values"] = {}
|
|
140
|
+
document["computed_values"].update({"event": crud})
|
|
141
|
+
document["computed_values"].update({"modified_at": datetime.now(timezone.utc)})
|
|
142
|
+
if email := self._get_user_context_id():
|
|
143
|
+
document["computed_values"].update({"modified_by": email})
|
|
144
|
+
return document
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from elody.object_configurations.elody_configuration import (
|
|
2
|
+
ElodyConfiguration,
|
|
3
|
+
)
|
|
4
|
+
from elody.util import send_cloudevent
|
|
5
|
+
from os import getenv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JobConfiguration(ElodyConfiguration):
|
|
9
|
+
SCHEMA_TYPE = "elody"
|
|
10
|
+
SCHEMA_VERSION = 1
|
|
11
|
+
|
|
12
|
+
def crud(self):
|
|
13
|
+
crud = {"collection": "jobs", "collection_history": ""}
|
|
14
|
+
return {**super().crud(), **crud}
|
|
15
|
+
|
|
16
|
+
def document_info(self):
|
|
17
|
+
return super().document_info()
|
|
18
|
+
|
|
19
|
+
def logging(self, flat_document, **kwargs):
|
|
20
|
+
return super().logging(flat_document, **kwargs)
|
|
21
|
+
|
|
22
|
+
def migration(self):
|
|
23
|
+
return super().migration()
|
|
24
|
+
|
|
25
|
+
def serialization(self, from_format, to_format):
|
|
26
|
+
return super().serialization(from_format, to_format)
|
|
27
|
+
|
|
28
|
+
def validation(self):
|
|
29
|
+
return super().validation()
|
|
30
|
+
|
|
31
|
+
def _creator(self, post_body, *, get_user_context={}, **_):
|
|
32
|
+
document = super()._creator(post_body)
|
|
33
|
+
if email := get_user_context().email:
|
|
34
|
+
document["computed_values"]["created_by"] = email
|
|
35
|
+
return document
|
|
36
|
+
|
|
37
|
+
def _post_crud_hook(self, *, crud, document, get_rabbit, **kwargs):
|
|
38
|
+
if crud == "create":
|
|
39
|
+
send_cloudevent(
|
|
40
|
+
get_rabbit(),
|
|
41
|
+
getenv("MQ_EXCHANGE", "dams"),
|
|
42
|
+
"dams.job_created",
|
|
43
|
+
document,
|
|
44
|
+
)
|
|
45
|
+
if parent_id := kwargs.get("parent_id"):
|
|
46
|
+
send_cloudevent(
|
|
47
|
+
get_rabbit(),
|
|
48
|
+
getenv("MQ_EXCHANGE", "dams"),
|
|
49
|
+
"dams.job_changed",
|
|
50
|
+
{
|
|
51
|
+
"id": parent_id,
|
|
52
|
+
"patch": {
|
|
53
|
+
"relations": [
|
|
54
|
+
{"key": document["_id"], "type": "isParentJobOf"}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
elif crud == "update":
|
|
60
|
+
send_cloudevent(
|
|
61
|
+
get_rabbit(),
|
|
62
|
+
getenv("MQ_EXCHANGE", "dams"),
|
|
63
|
+
"dams.job_changed",
|
|
64
|
+
document,
|
|
65
|
+
)
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import re as regex
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from configuration import get_object_configuration_mapper # pyright: ignore
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from elody.util import get_raw_id
|
|
2
7
|
from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
|
|
3
8
|
UserContext,
|
|
4
9
|
)
|
|
5
10
|
from inuits_policy_based_auth.helpers.tenant import Tenant # pyright: ignore
|
|
6
11
|
from storage.storagemanager import StorageManager # pyright: ignore
|
|
7
|
-
from werkzeug.exceptions import
|
|
12
|
+
from werkzeug.exceptions import Forbidden # pyright: ignore
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class BaseUserTenantValidationPolicy(ABC):
|
|
@@ -14,16 +19,16 @@ class BaseUserTenantValidationPolicy(ABC):
|
|
|
14
19
|
self.user = {}
|
|
15
20
|
|
|
16
21
|
@abstractmethod
|
|
17
|
-
def get_user(self, id: str) -> dict:
|
|
18
|
-
|
|
22
|
+
def get_user(self, id: str, user_context: UserContext) -> dict:
|
|
23
|
+
user_context.bag["roles_from_idp"] = deepcopy(user_context.x_tenant.roles)
|
|
19
24
|
|
|
20
25
|
@abstractmethod
|
|
21
|
-
def
|
|
26
|
+
def build_user_context_for_authenticated_user(
|
|
27
|
+
self, request, user_context: UserContext, user: dict
|
|
28
|
+
):
|
|
22
29
|
self.user = user
|
|
23
30
|
user_context.x_tenant = Tenant()
|
|
24
|
-
user_context.x_tenant.id =
|
|
25
|
-
"X-tenant-id", self.super_tenant_id
|
|
26
|
-
)
|
|
31
|
+
user_context.x_tenant.id = self._determine_tenant_id(request, user)
|
|
27
32
|
user_context.x_tenant.roles = self.__get_tenant_roles(
|
|
28
33
|
user_context.x_tenant.id, request
|
|
29
34
|
)
|
|
@@ -33,20 +38,45 @@ class BaseUserTenantValidationPolicy(ABC):
|
|
|
33
38
|
user_context.bag["tenant_defining_entity_id"] = user_context.x_tenant.id
|
|
34
39
|
user_context.bag["tenant_relation_type"] = "isIn"
|
|
35
40
|
user_context.bag["user_ids"] = self.user["identifiers"]
|
|
41
|
+
user_context.bag["http_method"] = request.method
|
|
42
|
+
user_context.bag["requested_endpoint"] = request.endpoint
|
|
43
|
+
user_context.bag["full_path"] = request.full_path
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def build_user_context_for_anonymous_user(
|
|
47
|
+
self, user_context: UserContext, user: dict
|
|
48
|
+
):
|
|
49
|
+
self.user = user
|
|
50
|
+
user_context.id = get_raw_id(user)
|
|
51
|
+
user_context.x_tenant = Tenant()
|
|
52
|
+
user_context.x_tenant.id = self.super_tenant_id
|
|
53
|
+
user_context.x_tenant.roles = ["anonymous"]
|
|
54
|
+
user_context.x_tenant.raw = self.__get_x_tenant_raw(user_context.x_tenant.id)
|
|
55
|
+
user_context.tenants = [user_context.x_tenant]
|
|
56
|
+
user_context.bag["x_tenant_id"] = user_context.x_tenant.id
|
|
57
|
+
user_context.bag["tenant_defining_entity_id"] = user_context.x_tenant.id
|
|
58
|
+
user_context.bag["tenant_relation_type"] = "isIn"
|
|
59
|
+
user_context.bag["user_ids"] = self.user["identifiers"]
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def _determine_tenant_id(self, request, user) -> str:
|
|
63
|
+
pass
|
|
36
64
|
|
|
37
65
|
def __get_tenant_roles(self, x_tenant_id: str, request) -> list[str]:
|
|
38
66
|
roles = self.__get_user_tenant_relation(self.super_tenant_id).get("roles", [])
|
|
39
67
|
if x_tenant_id != self.super_tenant_id:
|
|
40
68
|
try:
|
|
41
69
|
user_tenant_relation = self.__get_user_tenant_relation(x_tenant_id)
|
|
42
|
-
except
|
|
70
|
+
except Forbidden as error:
|
|
43
71
|
user_tenant_relation = {}
|
|
44
72
|
if len(roles) == 0:
|
|
45
|
-
raise
|
|
73
|
+
raise Forbidden(error.description)
|
|
46
74
|
roles.extend(user_tenant_relation.get("roles", []))
|
|
47
75
|
|
|
48
|
-
if len(roles) == 0 and
|
|
49
|
-
|
|
76
|
+
if len(roles) == 0 and not regex.match(
|
|
77
|
+
"(/[^/]+/v[0-9]+)?/tenants$", request.path
|
|
78
|
+
):
|
|
79
|
+
raise Forbidden("User has no global roles, switch to a specific tenant.")
|
|
50
80
|
return roles
|
|
51
81
|
|
|
52
82
|
def __get_user_tenant_relation(self, x_tenant_id: str) -> dict:
|
|
@@ -58,18 +88,21 @@ class BaseUserTenantValidationPolicy(ABC):
|
|
|
58
88
|
|
|
59
89
|
if not user_tenant_relation:
|
|
60
90
|
if x_tenant_id != self.super_tenant_id:
|
|
61
|
-
raise
|
|
91
|
+
raise Forbidden(f"User is not a member of tenant {x_tenant_id}.")
|
|
62
92
|
else:
|
|
63
93
|
return {}
|
|
64
94
|
|
|
65
95
|
return user_tenant_relation
|
|
66
96
|
|
|
67
97
|
def __get_x_tenant_raw(self, x_tenant_id: str) -> dict:
|
|
98
|
+
collection = (
|
|
99
|
+
get_object_configuration_mapper().get("tenant").crud()["collection"]
|
|
100
|
+
)
|
|
68
101
|
x_tenant_raw = (
|
|
69
|
-
self.storage.get_item_from_collection_by_id(
|
|
102
|
+
self.storage.get_item_from_collection_by_id(collection, x_tenant_id) or {}
|
|
70
103
|
)
|
|
71
104
|
if x_tenant_raw.get("type") != "tenant":
|
|
72
|
-
raise
|
|
105
|
+
raise Forbidden(f"No tenant {x_tenant_id} exists.")
|
|
73
106
|
|
|
74
107
|
return x_tenant_raw
|
|
75
108
|
|
|
@@ -85,7 +118,7 @@ class BaseUserTenantValidationPolicy(ABC):
|
|
|
85
118
|
tenant_defining_entity_id = relation["key"]
|
|
86
119
|
break
|
|
87
120
|
if not tenant_defining_entity_id:
|
|
88
|
-
raise
|
|
121
|
+
raise Forbidden(
|
|
89
122
|
f"{x_tenant_raw['_id']} has no relation with a tenant defining entity."
|
|
90
123
|
)
|
|
91
124
|
|