elody 0.0.194__tar.gz → 0.0.196__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.196}/PKG-INFO +1 -1
  2. {elody-0.0.194 → elody-0.0.196}/pyproject.toml +1 -1
  3. elody-0.0.196/src/elody/policies/authentication/base_user_tenant_validation_policy.py +132 -0
  4. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/filter_generic_objects_policy_v2.py +10 -2
  5. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_request_policy_v2.py +12 -3
  6. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/mediafile_derivatives_policy.py +15 -33
  7. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/mediafile_download_policy.py +6 -20
  8. elody-0.0.196/src/elody/policies/helpers.py +49 -0
  9. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/permission_handler.py +35 -9
  10. {elody-0.0.194 → elody-0.0.196}/src/elody.egg-info/PKG-INFO +1 -1
  11. {elody-0.0.194 → elody-0.0.196}/src/elody.egg-info/SOURCES.txt +0 -1
  12. elody-0.0.194/src/elody/policies/authentication/base_user_tenant_validation_policy.py +0 -125
  13. elody-0.0.194/src/elody/policies/helpers.py +0 -37
  14. elody-0.0.194/src/elody/policies/tenant_id_resolver.py +0 -375
  15. {elody-0.0.194 → elody-0.0.196}/LICENSE +0 -0
  16. {elody-0.0.194 → elody-0.0.196}/README.md +0 -0
  17. {elody-0.0.194 → elody-0.0.196}/setup.cfg +0 -0
  18. {elody-0.0.194 → elody-0.0.196}/src/__init__.py +0 -0
  19. {elody-0.0.194 → elody-0.0.196}/src/elody/__init__.py +0 -0
  20. {elody-0.0.194 → elody-0.0.196}/src/elody/client.py +0 -0
  21. {elody-0.0.194 → elody-0.0.196}/src/elody/csv.py +0 -0
  22. {elody-0.0.194 → elody-0.0.196}/src/elody/error_codes.py +0 -0
  23. {elody-0.0.194 → elody-0.0.196}/src/elody/exceptions.py +0 -0
  24. {elody-0.0.194 → elody-0.0.196}/src/elody/job.py +0 -0
  25. {elody-0.0.194 → elody-0.0.196}/src/elody/loader.py +0 -0
  26. {elody-0.0.194 → elody-0.0.196}/src/elody/migration/__init__.py +0 -0
  27. {elody-0.0.194 → elody-0.0.196}/src/elody/migration/base_object_migrator.py +0 -0
  28. {elody-0.0.194 → elody-0.0.196}/src/elody/object_configurations/__init__.py +0 -0
  29. {elody-0.0.194 → elody-0.0.196}/src/elody/object_configurations/base_object_configuration.py +0 -0
  30. {elody-0.0.194 → elody-0.0.196}/src/elody/object_configurations/elody_configuration.py +0 -0
  31. {elody-0.0.194 → elody-0.0.196}/src/elody/object_configurations/job_configuration.py +0 -0
  32. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/__init__.py +0 -0
  33. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authentication/__init__.py +0 -0
  34. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authentication/multi_tenant_policy.py +0 -0
  35. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/__init__.py +0 -0
  36. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/filter_generic_objects_policy.py +0 -0
  37. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_detail_policy.py +0 -0
  38. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_mediafiles_policy.py +0 -0
  39. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_metadata_policy.py +0 -0
  40. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_relations_policy.py +0 -0
  41. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/generic_object_request_policy.py +0 -0
  42. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/multi_tenant_policy.py +0 -0
  43. {elody-0.0.194 → elody-0.0.196}/src/elody/policies/authorization/tenant_request_policy.py +0 -0
  44. {elody-0.0.194 → elody-0.0.196}/src/elody/schemas.py +0 -0
  45. {elody-0.0.194 → elody-0.0.196}/src/elody/util.py +0 -0
  46. {elody-0.0.194 → elody-0.0.196}/src/elody/validator.py +0 -0
  47. {elody-0.0.194 → elody-0.0.196}/src/elody.egg-info/dependency_links.txt +0 -0
  48. {elody-0.0.194 → elody-0.0.196}/src/elody.egg-info/requires.txt +0 -0
  49. {elody-0.0.194 → elody-0.0.196}/src/elody.egg-info/top_level.txt +0 -0
  50. {elody-0.0.194 → elody-0.0.196}/src/tests/__init_.py +0 -0
  51. {elody-0.0.194 → elody-0.0.196}/src/tests/data.py +0 -0
  52. {elody-0.0.194 → elody-0.0.196}/src/tests/unit/__init__.py +0 -0
  53. {elody-0.0.194 → elody-0.0.196}/src/tests/unit/test_csv.py +0 -0
  54. {elody-0.0.194 → elody-0.0.196}/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.196
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.196"
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
@@ -1,11 +1,12 @@
1
1
  import re as regex
2
2
 
3
3
  from copy import deepcopy
4
+ from elody.policies.helpers import generate_filter_key_and_lookup_from_restricted_key
4
5
  from elody.policies.permission_handler import (
5
6
  get_permissions,
6
7
  mask_protected_content_post_request_hook,
7
8
  )
8
- from flask import Request # pyright: ignore
9
+ from flask import g, Request # pyright: ignore
9
10
  from inuits_policy_based_auth import BaseAuthorizationPolicy # pyright: ignore
10
11
  from inuits_policy_based_auth.contexts.policy_context import ( # pyright: ignore
11
12
  PolicyContext,
@@ -26,7 +27,7 @@ class FilterGenericObjectsPolicyV2(BaseAuthorizationPolicy):
26
27
  if not isinstance(user_context.access_restrictions.filters, list):
27
28
  user_context.access_restrictions.filters = []
28
29
  type_filter, filters = self.__split_type_filter(
29
- user_context, deepcopy(request.json or [])
30
+ user_context, deepcopy(g.get("content") or request.json or [])
30
31
  )
31
32
  if not type_filter:
32
33
  policy_context.access_verdict = True
@@ -123,6 +124,11 @@ class PostRequestRules:
123
124
  restrictions = schemas[schema].get("object_restrictions", {})
124
125
  for restricted_key, restricting_value in restrictions.items():
125
126
  index, restricted_key = restricted_key.split(":")
127
+ restricted_key, lookup = (
128
+ generate_filter_key_and_lookup_from_restricted_key(
129
+ restricted_key
130
+ )
131
+ )
126
132
  key = f"{schema}|{restricted_key}"
127
133
  if group := restrictions_grouped_by_index.get(index):
128
134
  group["key"].append(key)
@@ -130,6 +136,7 @@ class PostRequestRules:
130
136
  restrictions_grouped_by_index.update(
131
137
  {
132
138
  index: {
139
+ "lookup": lookup,
133
140
  "key": [key],
134
141
  "value": restricting_value,
135
142
  }
@@ -155,6 +162,7 @@ class PostRequestRules:
155
162
  for restriction in restrictions_grouped_by_index.values():
156
163
  user_context.access_restrictions.filters.append( # pyright: ignore
157
164
  {
165
+ "lookup": restriction["lookup"],
158
166
  "type": "selection",
159
167
  "key": restriction["key"],
160
168
  "value": restriction["value"],
@@ -1,7 +1,10 @@
1
1
  import re as regex
2
2
 
3
3
  from configuration import get_object_configuration_mapper # pyright: ignore
4
- from elody.policies.helpers import get_content
4
+ from elody.policies.helpers import (
5
+ generate_filter_key_and_lookup_from_restricted_key,
6
+ get_content,
7
+ )
5
8
  from elody.policies.permission_handler import (
6
9
  get_permissions,
7
10
  handle_single_item_request,
@@ -82,7 +85,7 @@ class GetRequestRules:
82
85
  type_permissions[schemas[0]].get("object_restrictions", {}).keys()
83
86
  )
84
87
  for i in range(number_of_object_restrictions):
85
- keys, values = [], []
88
+ lookup, keys, values = {}, [], []
86
89
  for schema in schemas:
87
90
  object_restrictions = type_permissions[schema].get(
88
91
  "object_restrictions", {}
@@ -92,10 +95,16 @@ class GetRequestRules:
92
95
  for key in object_restrictions.keys()
93
96
  if key.startswith(f"{i}:")
94
97
  ][0]
95
- keys.append(f"{schema}|{key.split(':')[1]}")
96
98
  values = object_restrictions[key]
99
+ key, lookup = (
100
+ generate_filter_key_and_lookup_from_restricted_key(
101
+ key.split(":")[1]
102
+ )
103
+ )
104
+ keys.append(f"{schema}|{key}")
97
105
  filters.append(
98
106
  {
107
+ "lookup": lookup,
99
108
  "type": "selection",
100
109
  "key": keys,
101
110
  "value": values,
@@ -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,49 @@
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 generate_filter_key_and_lookup_from_restricted_key(key):
8
+ if (keys := key.split("@", 1)) and len(keys) == 1:
9
+ return key, {}
10
+
11
+ local_field = keys[0]
12
+ document_type, key = keys[1].split("-", 1)
13
+ collection = (
14
+ get_object_configuration_mapper().get(document_type).crud()["collection"]
15
+ )
16
+ lookup = {
17
+ "from": collection,
18
+ "local_field": local_field,
19
+ "foreign_field": "identifiers",
20
+ "as": f"__lookup.virtual_relations.{document_type}",
21
+ }
22
+ return f"{lookup['as']}.{key}", lookup
23
+
24
+
25
+ def get_content(item, request, content):
26
+ return serialize(
27
+ content,
28
+ type=item.get("type"),
29
+ from_format=serialize.get_format(
30
+ (request.view_args or {}).get("spec", "elody"), request.args
31
+ ),
32
+ to_format=get_object_configuration_mapper().get(item["type"]).SCHEMA_TYPE,
33
+ )
34
+
35
+
36
+ def get_item(storage_manager, user_context_bag, view_args) -> dict:
37
+ view_args = view_args or {}
38
+ if id := view_args.get("id"):
39
+ resolve_collections = user_context_bag.get("collection_resolver")
40
+ collections = resolve_collections(collection=view_args.get("collection"), id=id)
41
+ for collection in collections:
42
+ if item := storage_manager.get_db_engine().get_item_from_collection_by_id(
43
+ collection, id
44
+ ):
45
+ return item
46
+
47
+ raise NotFound(
48
+ f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{id} - Item with id {id} does not exist."
49
+ )
@@ -3,9 +3,11 @@ import re as regex
3
3
  from configuration import get_object_configuration_mapper # pyright: ignore
4
4
  from copy import deepcopy
5
5
  from elody.error_codes import ErrorCode, get_error_code, get_read
6
+ from elody.policies.helpers import get_item
6
7
  from elody.util import flatten_dict, interpret_flat_key
7
8
  from inuits_policy_based_auth.contexts.user_context import UserContext
8
9
  from logging_elody.log import log # pyright: ignore
10
+ from storage.storagemanager import StorageManager # pyright: ignore
9
11
 
10
12
 
11
13
  _permissions = {}
@@ -63,7 +65,7 @@ def handle_single_item_request(
63
65
  )
64
66
 
65
67
  is_allowed_to_crud_item = (
66
- __is_allowed_to_crud_item(flat_item, restrictions_schema)
68
+ __is_allowed_to_crud_item(flat_item, restrictions_schema, user_context)
67
69
  if flat_item
68
70
  else None
69
71
  )
@@ -131,10 +133,7 @@ def __prepare_item_for_permission_check(item, permissions, crud):
131
133
  if item.get("type", "") not in permissions[crud].keys():
132
134
  return item, None, None, None
133
135
 
134
- config = get_object_configuration_mapper().get(item["type"])
135
- object_lists = config.document_info().get("object_lists", {})
136
- flat_item = flatten_dict(object_lists, item)
137
-
136
+ flat_item, object_lists = __get_flat_item_and_object_lists(item)
138
137
  return (
139
138
  item,
140
139
  flat_item,
@@ -159,13 +158,15 @@ def __get_restrictions_schema(flat_item, permissions, crud):
159
158
  return schemas[schema] if schemas and schema else {}
160
159
 
161
160
 
162
- def __is_allowed_to_crud_item(flat_item, restrictions_schema):
161
+ def __is_allowed_to_crud_item(
162
+ flat_item, restrictions_schema, user_context: UserContext
163
+ ):
163
164
  restrictions = restrictions_schema.get("object_restrictions", {})
164
165
 
165
166
  for restricted_key, restricting_values in restrictions.items():
166
167
  restricted_key = restricted_key.split(":")[1]
167
168
  item_value_in_restricting_values = __item_value_in_values(
168
- flat_item, restricted_key, restricting_values
169
+ flat_item, restricted_key, restricting_values, {}, user_context
169
170
  )
170
171
  if not item_value_in_restricting_values:
171
172
  return None
@@ -192,7 +193,11 @@ def __is_allowed_to_crud_item_keys(
192
193
  condition_match = True
193
194
  for condition_key, condition_values in restricting_conditions.items():
194
195
  condition_match = __item_value_in_values(
195
- flat_item, condition_key, condition_values, flat_request_body
196
+ flat_item,
197
+ condition_key,
198
+ condition_values,
199
+ flat_request_body,
200
+ user_context,
196
201
  )
197
202
  if not condition_match:
198
203
  break
@@ -228,7 +233,9 @@ def __is_allowed_to_crud_item_keys(
228
233
  return len(user_context.bag["restricted_keys"]) == 0
229
234
 
230
235
 
231
- def __item_value_in_values(flat_item, key, values: list, flat_request_body: dict = {}):
236
+ def __item_value_in_values(
237
+ flat_item, key, values: list, flat_request_body, user_context: UserContext
238
+ ):
232
239
  negate_condition = False
233
240
  is_optional = False
234
241
 
@@ -239,6 +246,10 @@ def __item_value_in_values(flat_item, key, values: list, flat_request_body: dict
239
246
  key = key[1:]
240
247
  is_optional = True
241
248
 
249
+ key_of_relation = None
250
+ if (keys := key.split("@", 1)) and len(keys) == 2:
251
+ key = keys[0]
252
+ key_of_relation = keys[1].split("-", 1)[1]
242
253
  try:
243
254
  item_value = flat_request_body.get(key, flat_item[key])
244
255
  if is_optional:
@@ -249,6 +260,15 @@ def __item_value_in_values(flat_item, key, values: list, flat_request_body: dict
249
260
  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."
250
261
  )
251
262
  return not negate_condition
263
+ else:
264
+ if key_of_relation:
265
+ if isinstance(item_value, list):
266
+ item_value = item_value[0]
267
+ item = get_item(StorageManager(), user_context.bag, {"id": item_value})
268
+ flat_item, _ = __get_flat_item_and_object_lists(item)
269
+ return __item_value_in_values(
270
+ flat_item, key_of_relation, values, flat_request_body, user_context
271
+ )
252
272
 
253
273
  expected_values = []
254
274
  for value in values:
@@ -280,3 +300,9 @@ def __get_element_from_object_list_of_item(
280
300
  if element[object_lists[object_list]] == key:
281
301
  return element
282
302
  return {}
303
+
304
+
305
+ def __get_flat_item_and_object_lists(item):
306
+ config = get_object_configuration_mapper().get(item["type"])
307
+ object_lists = config.document_info().get("object_lists", {})
308
+ return flatten_dict(object_lists, item), object_lists
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elody
3
- Version: 0.0.194
3
+ Version: 0.0.196
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