elody 0.0.194__tar.gz → 0.0.195__tar.gz

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 (54) hide show
  1. {elody-0.0.194 → elody-0.0.195}/PKG-INFO +1 -1
  2. {elody-0.0.194 → elody-0.0.195}/pyproject.toml +1 -1
  3. elody-0.0.195/src/elody/policies/authentication/base_user_tenant_validation_policy.py +132 -0
  4. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/filter_generic_objects_policy_v2.py +2 -2
  5. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/mediafile_derivatives_policy.py +15 -33
  6. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/mediafile_download_policy.py +6 -20
  7. elody-0.0.195/src/elody/policies/helpers.py +31 -0
  8. {elody-0.0.194 → elody-0.0.195}/src/elody.egg-info/PKG-INFO +1 -1
  9. {elody-0.0.194 → elody-0.0.195}/src/elody.egg-info/SOURCES.txt +0 -1
  10. elody-0.0.194/src/elody/policies/authentication/base_user_tenant_validation_policy.py +0 -125
  11. elody-0.0.194/src/elody/policies/helpers.py +0 -37
  12. elody-0.0.194/src/elody/policies/tenant_id_resolver.py +0 -375
  13. {elody-0.0.194 → elody-0.0.195}/LICENSE +0 -0
  14. {elody-0.0.194 → elody-0.0.195}/README.md +0 -0
  15. {elody-0.0.194 → elody-0.0.195}/setup.cfg +0 -0
  16. {elody-0.0.194 → elody-0.0.195}/src/__init__.py +0 -0
  17. {elody-0.0.194 → elody-0.0.195}/src/elody/__init__.py +0 -0
  18. {elody-0.0.194 → elody-0.0.195}/src/elody/client.py +0 -0
  19. {elody-0.0.194 → elody-0.0.195}/src/elody/csv.py +0 -0
  20. {elody-0.0.194 → elody-0.0.195}/src/elody/error_codes.py +0 -0
  21. {elody-0.0.194 → elody-0.0.195}/src/elody/exceptions.py +0 -0
  22. {elody-0.0.194 → elody-0.0.195}/src/elody/job.py +0 -0
  23. {elody-0.0.194 → elody-0.0.195}/src/elody/loader.py +0 -0
  24. {elody-0.0.194 → elody-0.0.195}/src/elody/migration/__init__.py +0 -0
  25. {elody-0.0.194 → elody-0.0.195}/src/elody/migration/base_object_migrator.py +0 -0
  26. {elody-0.0.194 → elody-0.0.195}/src/elody/object_configurations/__init__.py +0 -0
  27. {elody-0.0.194 → elody-0.0.195}/src/elody/object_configurations/base_object_configuration.py +0 -0
  28. {elody-0.0.194 → elody-0.0.195}/src/elody/object_configurations/elody_configuration.py +0 -0
  29. {elody-0.0.194 → elody-0.0.195}/src/elody/object_configurations/job_configuration.py +0 -0
  30. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/__init__.py +0 -0
  31. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authentication/__init__.py +0 -0
  32. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authentication/multi_tenant_policy.py +0 -0
  33. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/__init__.py +0 -0
  34. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/filter_generic_objects_policy.py +0 -0
  35. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_detail_policy.py +0 -0
  36. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_mediafiles_policy.py +0 -0
  37. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_metadata_policy.py +0 -0
  38. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_relations_policy.py +0 -0
  39. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_request_policy.py +0 -0
  40. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/generic_object_request_policy_v2.py +0 -0
  41. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/multi_tenant_policy.py +0 -0
  42. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/authorization/tenant_request_policy.py +0 -0
  43. {elody-0.0.194 → elody-0.0.195}/src/elody/policies/permission_handler.py +0 -0
  44. {elody-0.0.194 → elody-0.0.195}/src/elody/schemas.py +0 -0
  45. {elody-0.0.194 → elody-0.0.195}/src/elody/util.py +0 -0
  46. {elody-0.0.194 → elody-0.0.195}/src/elody/validator.py +0 -0
  47. {elody-0.0.194 → elody-0.0.195}/src/elody.egg-info/dependency_links.txt +0 -0
  48. {elody-0.0.194 → elody-0.0.195}/src/elody.egg-info/requires.txt +0 -0
  49. {elody-0.0.194 → elody-0.0.195}/src/elody.egg-info/top_level.txt +0 -0
  50. {elody-0.0.194 → elody-0.0.195}/src/tests/__init_.py +0 -0
  51. {elody-0.0.194 → elody-0.0.195}/src/tests/data.py +0 -0
  52. {elody-0.0.194 → elody-0.0.195}/src/tests/unit/__init__.py +0 -0
  53. {elody-0.0.194 → elody-0.0.195}/src/tests/unit/test_csv.py +0 -0
  54. {elody-0.0.194 → elody-0.0.195}/src/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elody
3
- Version: 0.0.194
3
+ Version: 0.0.195
4
4
  Summary: elody SDK for Python
5
5
  Author-email: Inuits <developers@inuits.eu>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "elody"
9
- version = "0.0.194"
9
+ version = "0.0.195"
10
10
  description = "elody SDK for Python"
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Inuits", email = "developers@inuits.eu" }]
@@ -0,0 +1,132 @@
1
+ import re as regex
2
+
3
+ from abc import ABC, abstractmethod
4
+ from configuration import get_object_configuration_mapper # pyright: ignore
5
+ from copy import deepcopy
6
+ from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
7
+ UserContext,
8
+ )
9
+ from inuits_policy_based_auth.helpers.tenant import Tenant # pyright: ignore
10
+ from werkzeug.exceptions import Forbidden # pyright: ignore
11
+
12
+
13
+ class BaseUserTenantValidationPolicy(ABC):
14
+ @abstractmethod
15
+ def get_user(
16
+ self,
17
+ id: str,
18
+ user_context: UserContext,
19
+ storage,
20
+ *,
21
+ user_metadata_key_for_global_roles="roles",
22
+ user_tenant_relation_type="hasTenant",
23
+ ) -> dict:
24
+ config = get_object_configuration_mapper().get("user")
25
+ collection = config.crud()["collection"]
26
+ serialize = config.serialization(config.SCHEMA_TYPE, "elody")
27
+
28
+ user = storage.get_item_from_collection_by_id(collection, id) or {}
29
+ self.user = serialize(user)
30
+ user_context.bag["roles_from_idp"] = deepcopy(user_context.x_tenant.roles)
31
+ user_context.bag["user_metadata_key_for_global_roles"] = (
32
+ user_metadata_key_for_global_roles
33
+ )
34
+ user_context.bag["user_tenant_relation_type"] = user_tenant_relation_type
35
+
36
+ return self.user
37
+
38
+ @abstractmethod
39
+ def build_user_context_for_anonymous_user(
40
+ self, request, user_context: UserContext
41
+ ) -> UserContext:
42
+ user_context = self.__build_user_context(request, user_context)
43
+ user_context.id = "anonymous"
44
+ user_context.x_tenant = Tenant()
45
+ user_context.x_tenant.id = ""
46
+ user_context.x_tenant.roles = ["anonymous"]
47
+ return user_context
48
+
49
+ @abstractmethod
50
+ def build_user_context_for_authenticated_user(
51
+ self, request, user_context: UserContext, user: dict
52
+ ) -> UserContext:
53
+ user_context = self.__build_user_context(request, user_context)
54
+ user_context.x_tenant = Tenant()
55
+ user_context.x_tenant.id = self._determine_tenant_id(request, user_context)
56
+ user_context.x_tenant.roles = self.__get_tenant_roles(request, user_context)
57
+ return user_context
58
+
59
+ @abstractmethod
60
+ def _determine_tenant_id(self, request, user_context: UserContext) -> str:
61
+ pass
62
+
63
+ def __build_user_context(self, request, user_context: UserContext):
64
+ user_context.bag["http_method"] = request.method
65
+ user_context.bag["requested_endpoint"] = request.endpoint
66
+ user_context.bag["full_path"] = request.full_path
67
+ user_context.bag["collection_resolver"] = (
68
+ self._resolve_collections # pyright: ignore
69
+ )
70
+ return user_context
71
+
72
+ def __get_tenant_roles(self, request, user_context: UserContext) -> list[str]:
73
+ """
74
+ Gathering multiple tenants are supported, but there are some important notes:
75
+ - all roles are stored in a combined way in user_context.x_tenant.roles
76
+ - those combination of different tenant roles will work, as long as:
77
+ - the roles are the same
78
+ - the object restrictions allow all tenants that are linked with the user
79
+ => to make it fully work, see #143185
80
+ - with combined roles, when combaring rol_a vs rol_b
81
+ - if rol_a allow and role_b does not allow access: the role allowing access will always win
82
+ - if both roles allow, but just have different restrictions: the first allowing role will win
83
+ - this is completely random
84
+ => currently no logic in policies to determine which allowing role to apply
85
+ """
86
+
87
+ roles = []
88
+ for metadata in self.user.get("metadata", []):
89
+ if (
90
+ metadata["key"]
91
+ == user_context.bag["user_metadata_key_for_global_roles"]
92
+ ):
93
+ roles.extend(metadata["value"])
94
+
95
+ if user_context.x_tenant.id:
96
+ tenant_ids = user_context.x_tenant.id.split(",")
97
+ for tenant_id in tenant_ids:
98
+ try:
99
+ user_tenant_relation = self.__get_user_tenant_relation(
100
+ tenant_id, user_context.bag["user_tenant_relation_type"]
101
+ )
102
+ except Forbidden as error:
103
+ user_tenant_relation = {}
104
+ if len(roles) == 0:
105
+ raise Forbidden(error.description)
106
+ roles.extend(user_tenant_relation.get("roles", []))
107
+
108
+ if len(roles) == 0 and not regex.match(
109
+ "(/[^/]+/v[0-9]+)?/tenants$", request.path
110
+ ):
111
+ raise Forbidden("User has no global roles, switch to a specific tenant.")
112
+
113
+ return list(set(roles))
114
+
115
+ def __get_user_tenant_relation(
116
+ self, tenant_id: str, user_tenant_relation_type: str
117
+ ) -> dict:
118
+ user_tenant_relation = None
119
+ for relation in self.user.get("relations", []):
120
+ if (
121
+ relation["key"] == tenant_id
122
+ and relation["type"] == user_tenant_relation_type
123
+ ):
124
+ user_tenant_relation = relation
125
+ break
126
+
127
+ if not user_tenant_relation:
128
+ if tenant_id:
129
+ raise Forbidden(f"User is not a member of tenant {tenant_id}.")
130
+ return {}
131
+
132
+ return user_tenant_relation
@@ -5,7 +5,7 @@ from elody.policies.permission_handler import (
5
5
  get_permissions,
6
6
  mask_protected_content_post_request_hook,
7
7
  )
8
- from flask import Request # pyright: ignore
8
+ from flask import g, Request # pyright: ignore
9
9
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
10
10
  from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
11
11
  PolicyContext,
@@ -26,7 +26,7 @@ class FilterGenericObjectsPolicyV2(BaseAuthorizationPolicy):
26
26
  if not isinstance(user_context.access_restrictions.filters, list):
27
27
  user_context.access_restrictions.filters = []
28
28
  type_filter, filters = self.__split_type_filter(
29
- user_context, deepcopy(request.json or [])
29
+ user_context, deepcopy(g.get("content") or request.json or [])
30
30
  )
31
31
  if not type_filter:
32
32
  policy_context.access_verdict = True
@@ -1,12 +1,12 @@
1
1
  import re as regex
2
2
 
3
- from elody.error_codes import ErrorCode, get_error_code, get_read, get_write
3
+ from configuration import get_object_configuration_mapper # pyright: ignore
4
+ from elody.policies.helpers import get_content, get_item
4
5
  from elody.policies.permission_handler import (
5
6
  get_permissions,
6
7
  handle_single_item_request,
7
8
  )
8
- from flask import Request # pyright: ignore
9
- from flask_restful import abort # pyright: ignore
9
+ from flask import g, Request # pyright: ignore
10
10
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
11
11
  from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
12
12
  PolicyContext,
@@ -22,32 +22,18 @@ class MediafileDerivativesPolicy(BaseAuthorizationPolicy):
22
22
  self, policy_context: PolicyContext, user_context: UserContext, request_context
23
23
  ):
24
24
  request: Request = request_context.http_request
25
- if not regex.match(r"^/mediafiles/(.+)/derivatives$", request.path):
25
+ if not regex.match(
26
+ "^(/elody/v[0-9]+)?/mediafiles/[^/]+/derivatives$", request.path
27
+ ):
26
28
  return policy_context
27
29
 
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
-
30
+ item = get_item(StorageManager(), user_context.bag, request.view_args)
42
31
  for role in user_context.x_tenant.roles:
43
32
  permissions = get_permissions(role, user_context)
44
33
  if not permissions:
45
34
  continue
46
35
 
47
- rules = [
48
- PostRequestRules,
49
- GetRequestRules,
50
- ]
36
+ rules = [PostRequestRules, GetRequestRules]
51
37
  access_verdict = None
52
38
  for rule in rules:
53
39
  access_verdict = rule().apply(item, user_context, request, permissions)
@@ -69,7 +55,13 @@ class PostRequestRules:
69
55
  if request.method != "POST":
70
56
  return None
71
57
 
72
- return handle_single_item_request(user_context, item, permissions, "create")
58
+ content = g.get("content") or request.json
59
+ content = get_content(content, request, content)
60
+ schema_type = get_object_configuration_mapper().get(content["type"]).SCHEMA_TYPE
61
+ item = {**content, "schema": {"type": schema_type}}
62
+ return handle_single_item_request(
63
+ user_context, item, permissions, "create", content
64
+ )
73
65
 
74
66
 
75
67
  class GetRequestRules:
@@ -80,13 +72,3 @@ class GetRequestRules:
80
72
  return None
81
73
 
82
74
  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")
@@ -1,12 +1,11 @@
1
1
  import re as regex
2
2
 
3
- from elody.error_codes import ErrorCode, get_error_code, get_read
3
+ from elody.policies.helpers import get_item
4
4
  from elody.policies.permission_handler import (
5
5
  get_permissions,
6
6
  handle_single_item_request,
7
7
  )
8
8
  from flask import Request # pyright: ignore
9
- from flask_restful import abort # pyright: ignore
10
9
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
11
10
  from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
12
11
  PolicyContext,
@@ -22,31 +21,18 @@ class MediafileDownloadPolicy(BaseAuthorizationPolicy):
22
21
  self, policy_context: PolicyContext, user_context: UserContext, request_context
23
22
  ):
24
23
  request: Request = request_context.http_request
25
- if not regex.match(r"^/mediafiles/(.+)/download$", request.path):
24
+ if not regex.match(
25
+ "^(/elody/v[0-9]+)?/mediafiles/[^/]+/download$", request.path
26
+ ):
26
27
  return policy_context
27
28
 
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
-
29
+ item = get_item(StorageManager(), user_context.bag, request.view_args)
42
30
  for role in user_context.x_tenant.roles:
43
31
  permissions = get_permissions(role, user_context)
44
32
  if not permissions:
45
33
  continue
46
34
 
47
- rules = [
48
- GetRequestRules,
49
- ]
35
+ rules = [GetRequestRules]
50
36
  access_verdict = None
51
37
  for rule in rules:
52
38
  access_verdict = rule().apply(item, user_context, request, permissions)
@@ -0,0 +1,31 @@
1
+ from configuration import get_object_configuration_mapper # pyright: ignore
2
+ from elody.error_codes import ErrorCode, get_error_code, get_read
3
+ from serialization.serialize import serialize # pyright: ignore
4
+ from werkzeug.exceptions import NotFound
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) -> dict:
19
+ view_args = view_args or {}
20
+ if id := view_args.get("id"):
21
+ resolve_collections = user_context_bag.get("collection_resolver")
22
+ collections = resolve_collections(collection=view_args.get("collection"), id=id)
23
+ for collection in collections:
24
+ if item := storage_manager.get_db_engine().get_item_from_collection_by_id(
25
+ collection, id
26
+ ):
27
+ return item
28
+
29
+ raise NotFound(
30
+ f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{id} - Item with id {id} does not exist."
31
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elody
3
- Version: 0.0.194
3
+ Version: 0.0.195
4
4
  Summary: elody SDK for Python
5
5
  Author-email: Inuits <developers@inuits.eu>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -26,7 +26,6 @@ src/elody/object_configurations/job_configuration.py
26
26
  src/elody/policies/__init__.py
27
27
  src/elody/policies/helpers.py
28
28
  src/elody/policies/permission_handler.py
29
- src/elody/policies/tenant_id_resolver.py
30
29
  src/elody/policies/authentication/__init__.py
31
30
  src/elody/policies/authentication/base_user_tenant_validation_policy.py
32
31
  src/elody/policies/authentication/multi_tenant_policy.py
@@ -1,125 +0,0 @@
1
- import re as regex
2
-
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
7
- from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
8
- UserContext,
9
- )
10
- from inuits_policy_based_auth.helpers.tenant import Tenant # pyright: ignore
11
- from storage.storagemanager import StorageManager # pyright: ignore
12
- from werkzeug.exceptions import Forbidden # pyright: ignore
13
-
14
-
15
- class BaseUserTenantValidationPolicy(ABC):
16
- def __init__(self) -> None:
17
- self.storage = StorageManager().get_db_engine()
18
- self.super_tenant_id = "tenant:super"
19
- self.user = {}
20
-
21
- @abstractmethod
22
- def get_user(self, id: str, user_context: UserContext) -> dict:
23
- user_context.bag["roles_from_idp"] = deepcopy(user_context.x_tenant.roles)
24
-
25
- @abstractmethod
26
- def build_user_context_for_authenticated_user(
27
- self, request, user_context: UserContext, user: dict
28
- ):
29
- self.user = user
30
- user_context.x_tenant = Tenant()
31
- user_context.x_tenant.id = self._determine_tenant_id(request, user)
32
- user_context.x_tenant.roles = self.__get_tenant_roles(
33
- user_context.x_tenant.id, request
34
- )
35
- user_context.x_tenant.raw = self.__get_x_tenant_raw(user_context.x_tenant.id)
36
- user_context.tenants = [user_context.x_tenant]
37
- user_context.bag["x_tenant_id"] = user_context.x_tenant.id
38
- user_context.bag["tenant_defining_entity_id"] = user_context.x_tenant.id
39
- user_context.bag["tenant_relation_type"] = "isIn"
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
64
-
65
- def __get_tenant_roles(self, x_tenant_id: str, request) -> list[str]:
66
- roles = self.__get_user_tenant_relation(self.super_tenant_id).get("roles", [])
67
- if x_tenant_id != self.super_tenant_id:
68
- try:
69
- user_tenant_relation = self.__get_user_tenant_relation(x_tenant_id)
70
- except Forbidden as error:
71
- user_tenant_relation = {}
72
- if len(roles) == 0:
73
- raise Forbidden(error.description)
74
- roles.extend(user_tenant_relation.get("roles", []))
75
-
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.")
80
- return roles
81
-
82
- def __get_user_tenant_relation(self, x_tenant_id: str) -> dict:
83
- user_tenant_relation = None
84
- for relation in self.user.get("relations", []):
85
- if relation["key"] == x_tenant_id and relation["type"] == "hasTenant":
86
- user_tenant_relation = relation
87
- break
88
-
89
- if not user_tenant_relation:
90
- if x_tenant_id != self.super_tenant_id:
91
- raise Forbidden(f"User is not a member of tenant {x_tenant_id}.")
92
- else:
93
- return {}
94
-
95
- return user_tenant_relation
96
-
97
- def __get_x_tenant_raw(self, x_tenant_id: str) -> dict:
98
- collection = (
99
- get_object_configuration_mapper().get("tenant").crud()["collection"]
100
- )
101
- x_tenant_raw = (
102
- self.storage.get_item_from_collection_by_id(collection, x_tenant_id) or {}
103
- )
104
- if x_tenant_raw.get("type") != "tenant":
105
- raise Forbidden(f"No tenant {x_tenant_id} exists.")
106
-
107
- return x_tenant_raw
108
-
109
- def _get_tenant_defining_entity_id(
110
- self, x_tenant_id: str, x_tenant_raw: dict
111
- ) -> str:
112
- if x_tenant_id == self.super_tenant_id:
113
- return x_tenant_id.removeprefix("tenant:")
114
-
115
- tenant_defining_entity_id = None
116
- for relation in x_tenant_raw.get("relations", []):
117
- if relation["type"] == "definedBy":
118
- tenant_defining_entity_id = relation["key"]
119
- break
120
- if not tenant_defining_entity_id:
121
- raise Forbidden(
122
- f"{x_tenant_raw['_id']} has no relation with a tenant defining entity."
123
- )
124
-
125
- return tenant_defining_entity_id
@@ -1,37 +0,0 @@
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
- )
@@ -1,375 +0,0 @@
1
- import re as regex
2
-
3
- from elody.error_codes import ErrorCode, get_error_code, get_read, get_write
4
- from flask import Request
5
- from flask_restful import abort
6
- from storage.storagemanager import StorageManager # pyright: ignore
7
- from elody.util import get_item_metadata_value
8
- from elody.exceptions import NotFoundException, NoTenantException
9
-
10
-
11
- class TenantIdResolver:
12
- def resolve(self, request, user):
13
- endpoints = [
14
- EntityGetRequest,
15
- EntityPostRequest,
16
- EntityDetailGetRequest,
17
- EntityDetailUpdateRequest,
18
- EntityDetailDeleteRequest,
19
- EntityDetailGetRelationsRequest,
20
- EntityDetailUpdateRelationsRequest,
21
- EntityDetailCreateRelationsRequest,
22
- EntityDetailDeleteRelationsRequest,
23
- EntityDetailGetMetadataRequest,
24
- EntityDetailUpdateMetadataRequest,
25
- EntityDetailCreateMetadataRequest,
26
- EntityDetailGetMediafilesRequest,
27
- EntityDetailCreateMediafilesRequest,
28
- MediafileGetRequest,
29
- MediafilePostRequest,
30
- MediafileDetailGetRequest,
31
- MediafileDetailUpdateRequest,
32
- MediafileDetailDeleteRequest,
33
- MediafileDetailGetDerivativesRequest,
34
- MediafileDetailCreateDerivativesRequest,
35
- MediafileDetailDeleteDerivativesRequest,
36
- ]
37
-
38
- for endpoint in endpoints:
39
- tenant_id = endpoint().get_tenant_id(request)
40
- if tenant_id != None:
41
- relations = user.get("relations", [])
42
- if len(relations) < 1:
43
- raise NoTenantException(f"User is not attached to a tenant")
44
- has_tenant_relation = any(
45
- relation.get("type") == "hasTenant"
46
- and relation.get("key") == tenant_id
47
- for relation in relations
48
- )
49
- if has_tenant_relation:
50
- return tenant_id
51
- elif len(relations) == 1 and relations[0].get("key") == "tenant:super":
52
- return "tenant:super"
53
- else:
54
- return relations[0].get("key")
55
- return "tenant:super"
56
-
57
-
58
- class BaseRequest:
59
- def __init__(self) -> None:
60
- self.storage = StorageManager().get_db_engine()
61
- self.super_tenant_id = "tenant:super"
62
- # TODO refactor this in a more generic way
63
- self.global_types = [
64
- "language",
65
- "type",
66
- "collectionForm",
67
- "institution",
68
- "tag",
69
- "triple",
70
- "person",
71
- "externalRecord",
72
- "verzameling",
73
- "arches_record",
74
- "manufacturer",
75
- "photographer",
76
- "creator",
77
- "assetPart",
78
- "set",
79
- "download",
80
- "license",
81
- "share_link"
82
- # TODO Mediafile should have a link to an asset
83
- "mediafile",
84
- "savedSearch",
85
- "original_data",
86
- "user",
87
- ]
88
-
89
- def _get_tenant_id_from_entity(self, entity_id):
90
- entity_relations = self.storage.get_collection_item_relations(
91
- "entities", entity_id
92
- )
93
- entity = self.storage.get_item_from_collection_by_id("entities", entity_id)
94
- if not entity:
95
- abort(
96
- 404,
97
- message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{entity_id} - Item with id {entity_id} doesn't exist",
98
- )
99
- type = entity.get("type")
100
- if type in self.global_types:
101
- return "tenant:super"
102
- if entity_relations:
103
- for entity_relation in entity_relations:
104
- if entity_relation.get("type") == "isIn":
105
- tenant = self.storage.get_item_from_collection_by_id(
106
- "entities", entity_relation.get("key")
107
- )
108
- return tenant["_id"]
109
- if entity_relation.get("type") == "hasInstitution":
110
- instution_relations = self.storage.get_collection_item_relations(
111
- "entities", entity_relation.get("key")
112
- )
113
- for institution_relation in instution_relations:
114
- if institution_relation.get("type") == "defines":
115
- return institution_relation.get("key")
116
-
117
- if entity and get_item_metadata_value(entity, "institution"):
118
- return f"tenant:{get_item_metadata_value(entity, 'institution')}"
119
- abort(
120
- 400,
121
- message=f"{get_error_code(ErrorCode.ENTITY_HAS_NO_TENANT, get_read())} - Entity has no tenant, and is suppose to have one.",
122
- )
123
-
124
- def _get_tenant_id_from_mediafile(self, mediafile_id):
125
- mediafile_relations = self.storage.get_collection_item_relations(
126
- "mediafiles", mediafile_id
127
- )
128
-
129
- for relation in mediafile_relations:
130
- if relation.get("type") == "belongsTo":
131
- return self._get_tenant_id_from_entity(relation.get("key"))
132
-
133
- # TODO Mediafile should have a link to an asset
134
- def _get_tenant_id_from_body(self, item, soft_call=False):
135
- if soft_call:
136
- return "tenant:super"
137
- if item.get("type") in self.global_types or item.get("type") == "institution":
138
- return "tenant:super"
139
- if item.get("type") == "asset":
140
- for relation in item.get("relations", []):
141
- if relation.get("type") in [
142
- "hasArchesLink",
143
- "hasAdlibLink",
144
- "hasBrocadeLink",
145
- ]:
146
- return "tenant:super"
147
- institution_id = get_item_metadata_value(item, "institution")
148
- if not institution_id:
149
- for relation in item.get("relations", []):
150
- if relation.get("type") == "hasInstitution":
151
- institution_id = relation.get("key")
152
- if institution_id:
153
- return f"tenant:{institution_id}"
154
- abort(
155
- 400,
156
- message=f"{get_error_code(ErrorCode.ENTITY_HAS_NO_TENANT, get_read())} - Item in body doesn't have an institution.",
157
- )
158
-
159
-
160
- class EntityGetRequest(BaseRequest):
161
- def get_tenant_id(self, request: Request) -> str | None:
162
- if (
163
- regex.match(r"^/entities(?:\?(.*))?$", request.path)
164
- and request.method == "GET"
165
- ):
166
- return self.super_tenant_id
167
- return None
168
-
169
-
170
- class EntityPostRequest(BaseRequest):
171
- def get_tenant_id(self, request: Request) -> str | None:
172
- if (
173
- regex.match(r"^/entities(?:\?(.*))?$", request.path)
174
- and request.method == "POST"
175
- ):
176
- is_soft_call = request.args.get("soft") is not None
177
- return self._get_tenant_id_from_body(request.json, soft_call=is_soft_call)
178
- return None
179
-
180
-
181
- class EntityDetailGetRequest(BaseRequest):
182
- def get_tenant_id(self, request: Request) -> str | None:
183
- if regex.match(r"/entities/(.+)$", request.path) and request.method == "GET":
184
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
185
- return None
186
-
187
-
188
- class EntityDetailUpdateRequest(BaseRequest):
189
- def get_tenant_id(self, request: Request) -> str | None:
190
- if regex.match(r"^/entities/(.+)$", request.path) and request.method in [
191
- "PUT",
192
- "PATCH",
193
- ]:
194
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
195
- return None
196
-
197
-
198
- class EntityDetailDeleteRequest(BaseRequest):
199
- def get_tenant_id(self, request: Request) -> str | None:
200
- if (
201
- regex.match(r"^/entities/([^/]+)$", request.path)
202
- and request.method == "DELETE"
203
- ):
204
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
205
- return None
206
-
207
-
208
- class EntityDetailGetRelationsRequest(BaseRequest):
209
- def get_tenant_id(self, request: Request) -> str | None:
210
- if (
211
- regex.match(r"^/entities/(.+)/relations$", request.path)
212
- and request.method == "GET"
213
- ):
214
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
215
- return None
216
-
217
-
218
- class EntityDetailUpdateRelationsRequest(BaseRequest):
219
- def get_tenant_id(self, request: Request) -> str | None:
220
- if regex.match(
221
- r"^/entities/(.+)/relations$", request.path
222
- ) and request.method in ["PUT", "PATCH"]:
223
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
224
- return None
225
-
226
-
227
- class EntityDetailCreateRelationsRequest(BaseRequest):
228
- def get_tenant_id(self, request: Request) -> str | None:
229
- if (
230
- regex.match(r"^/entities/(.+)/relations$", request.path)
231
- and request.method == "POST"
232
- ):
233
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
234
- return None
235
-
236
-
237
- class EntityDetailDeleteRelationsRequest(BaseRequest):
238
- def get_tenant_id(self, request: Request) -> str | None:
239
- if (
240
- regex.match(r"^/entities/(.+)/relations$", request.path)
241
- and request.method == "DELETE"
242
- ):
243
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
244
- return None
245
-
246
-
247
- class EntityDetailGetMetadataRequest(BaseRequest):
248
- def get_tenant_id(self, request: Request) -> str | None:
249
- if (
250
- regex.match(r"^/entities/(.+)/metadata$", request.path)
251
- and request.method == "GET"
252
- ):
253
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
254
- return None
255
-
256
-
257
- class EntityDetailUpdateMetadataRequest(BaseRequest):
258
- def get_tenant_id(self, request: Request) -> str | None:
259
- if regex.match(
260
- r"^/entities/(.+)/metadata$", request.path
261
- ) and request.method in ["PUT", "PATCH"]:
262
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
263
- return None
264
-
265
-
266
- class EntityDetailCreateMetadataRequest(BaseRequest):
267
- def get_tenant_id(self, request: Request) -> str | None:
268
- if (
269
- regex.match(r"^/entities/(.+)/metadata$", request.path)
270
- and request.method == "POST"
271
- ):
272
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
273
- return None
274
-
275
-
276
- class EntityDetailGetMediafilesRequest(BaseRequest):
277
- def get_tenant_id(self, request: Request) -> str | None:
278
- if (
279
- regex.match(r"^/entities/(.+)/mediafiles$", request.path)
280
- and request.method == "GET"
281
- ):
282
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
283
- return None
284
-
285
-
286
- class EntityDetailCreateMediafilesRequest(BaseRequest):
287
- def get_tenant_id(self, request: Request) -> str | None:
288
- if (
289
- regex.match(r"^/entities/(.+)/mediafiles$", request.path)
290
- and request.method == "POST"
291
- ):
292
- return self._get_tenant_id_from_entity(request.view_args.get("id"))
293
- return None
294
-
295
-
296
- # Mediafiles
297
- class MediafileGetRequest(BaseRequest):
298
- def get_tenant_id(self, request: Request) -> str | None:
299
- if (
300
- regex.match(r"^/mediafiles(?:\?(.*))?$", request.path)
301
- and request.method == "GET"
302
- ):
303
- return self.super_tenant_id
304
- return None
305
-
306
-
307
- class MediafilePostRequest(BaseRequest):
308
- def get_tenant_id(self, request: Request) -> str | None:
309
- if (
310
- regex.match(r"^/mediafiles(?:\?(.*))?$", request.path)
311
- and request.method == "POST"
312
- ):
313
- is_soft_call = request.args.get("soft") is not None
314
- return self._get_tenant_id_from_body(request.json, soft_call=is_soft_call)
315
- return None
316
-
317
-
318
- class MediafileDetailGetRequest(BaseRequest):
319
- def get_tenant_id(self, request: Request) -> str | None:
320
- if (
321
- regex.match(r"^/mediafiles/([^/]+)$", request.path)
322
- and request.method == "GET"
323
- ):
324
- return self.super_tenant_id
325
- return None
326
-
327
-
328
- class MediafileDetailUpdateRequest(BaseRequest):
329
- def get_tenant_id(self, request: Request) -> str | None:
330
- if regex.match(r"^/mediafiles/(.+)$", request.path) and request.method in [
331
- "PUT",
332
- "PATCH",
333
- ]:
334
- return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
335
- return None
336
-
337
-
338
- class MediafileDetailDeleteRequest(BaseRequest):
339
- def get_tenant_id(self, request: Request) -> str | None:
340
- if (
341
- regex.match(r"^/mediafiles/(.+)$", request.path)
342
- and request.method == "DELETE"
343
- ):
344
- return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
345
- return None
346
-
347
-
348
- class MediafileDetailGetDerivativesRequest(BaseRequest):
349
- def get_tenant_id(self, request: Request) -> str | None:
350
- if (
351
- regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
352
- and request.method == "GET"
353
- ):
354
- return self.super_tenant_id
355
- return None
356
-
357
-
358
- class MediafileDetailCreateDerivativesRequest(BaseRequest):
359
- def get_tenant_id(self, request: Request) -> str | None:
360
- if (
361
- regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
362
- and request.method == "POST"
363
- ):
364
- return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
365
- return None
366
-
367
-
368
- class MediafileDetailDeleteDerivativesRequest(BaseRequest):
369
- def get_tenant_id(self, request: Request) -> str | None:
370
- if (
371
- regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
372
- and request.method == "DELETE"
373
- ):
374
- return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
375
- return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes