elody 0.0.194__py3-none-any.whl → 0.0.196__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.
@@ -3,123 +3,130 @@ import re as regex
3
3
  from abc import ABC, abstractmethod
4
4
  from configuration import get_object_configuration_mapper # pyright: ignore
5
5
  from copy import deepcopy
6
- from elody.util import get_raw_id
7
6
  from inuits_policy_based_auth.contexts.user_context import ( # pyright: ignore
8
7
  UserContext,
9
8
  )
10
9
  from inuits_policy_based_auth.helpers.tenant import Tenant # pyright: ignore
11
- from storage.storagemanager import StorageManager # pyright: ignore
12
10
  from werkzeug.exceptions import Forbidden # pyright: ignore
13
11
 
14
12
 
15
13
  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
14
  @abstractmethod
22
- def get_user(self, id: str, user_context: UserContext) -> dict:
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)
23
30
  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
31
+ user_context.bag["user_metadata_key_for_global_roles"] = (
32
+ user_metadata_key_for_global_roles
34
33
  )
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
34
+ user_context.bag["user_tenant_relation_type"] = user_tenant_relation_type
35
+
36
+ return self.user
44
37
 
45
38
  @abstractmethod
46
39
  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)
40
+ self, request, user_context: UserContext
41
+ ) -> UserContext:
42
+ user_context = self.__build_user_context(request, user_context)
43
+ user_context.id = "anonymous"
51
44
  user_context.x_tenant = Tenant()
52
- user_context.x_tenant.id = self.super_tenant_id
45
+ user_context.x_tenant.id = ""
53
46
  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"]
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
60
58
 
61
59
  @abstractmethod
62
- def _determine_tenant_id(self, request, user) -> str:
60
+ def _determine_tenant_id(self, request, user_context: UserContext) -> str:
63
61
  pass
64
62
 
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", []))
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", []))
75
107
 
76
108
  if len(roles) == 0 and not regex.match(
77
109
  "(/[^/]+/v[0-9]+)?/tenants$", request.path
78
110
  ):
79
111
  raise Forbidden("User has no global roles, switch to a specific tenant.")
80
- return roles
81
112
 
82
- def __get_user_tenant_relation(self, x_tenant_id: str) -> dict:
113
+ return list(set(roles))
114
+
115
+ def __get_user_tenant_relation(
116
+ self, tenant_id: str, user_tenant_relation_type: str
117
+ ) -> dict:
83
118
  user_tenant_relation = None
84
119
  for relation in self.user.get("relations", []):
85
- if relation["key"] == x_tenant_id and relation["type"] == "hasTenant":
120
+ if (
121
+ relation["key"] == tenant_id
122
+ and relation["type"] == user_tenant_relation_type
123
+ ):
86
124
  user_tenant_relation = relation
87
125
  break
88
126
 
89
127
  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 {}
128
+ if tenant_id:
129
+ raise Forbidden(f"User is not a member of tenant {tenant_id}.")
130
+ return {}
94
131
 
95
132
  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,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)
elody/policies/helpers.py CHANGED
@@ -1,7 +1,25 @@
1
- from elody.error_codes import ErrorCode, get_error_code, get_read
2
1
  from configuration import get_object_configuration_mapper # pyright: ignore
3
- from flask_restful import abort # pyright: ignore
2
+ from elody.error_codes import ErrorCode, get_error_code, get_read
4
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
5
23
 
6
24
 
7
25
  def get_content(item, request, content):
@@ -15,23 +33,17 @@ def get_content(item, request, content):
15
33
  )
16
34
 
17
35
 
18
- def get_item(storage_manager, user_context_bag, view_args):
36
+ def get_item(storage_manager, user_context_bag, view_args) -> dict:
19
37
  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
- )
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
@@ -16,32 +16,31 @@ elody/object_configurations/base_object_configuration.py,sha256=8wyUq_zqRkGb4Mp1
16
16
  elody/object_configurations/elody_configuration.py,sha256=H3iqNXPhYX6scR7C57L_hCWQH2MnWKwnH3vegLg-UGw,7645
17
17
  elody/object_configurations/job_configuration.py,sha256=HMDxaRUyfqhIy0q3yQDDMH9uW5iCd7VCmqknQofXNt0,2039
18
18
  elody/policies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- elody/policies/helpers.py,sha256=AV3wtvthJafW6ueEYGxggB5kk5knWTWzh3zq29Y1-ws,1434
20
- elody/policies/permission_handler.py,sha256=C5wwgAplxmrkrseA-oNjX_Ff1PfVGP5NIvpx0F5PYho,9869
21
- elody/policies/tenant_id_resolver.py,sha256=BIl6lr9AH6Q_aZSsxF24B7ramkIZH-R-8bpLrTvYLtM,13796
19
+ elody/policies/helpers.py,sha256=I0ctnQ0ilcbzlIfDTcgzGzAWVrrH0GoLZsB1d6sAf6I,1766
20
+ elody/policies/permission_handler.py,sha256=a9kiHRINdQpkmDgrcLYZl4FplkpsURO42FMZ3Vzmdv4,10843
22
21
  elody/policies/authentication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- elody/policies/authentication/base_user_tenant_validation_policy.py,sha256=fxvrB4iG6fehBh6b4XS8AWewtNsCABgSooma5ljjZk4,5144
22
+ elody/policies/authentication/base_user_tenant_validation_policy.py,sha256=yyGiYy-MM63itQYwUc99FPeXt6TBW5B95FeLMfjMEok,5368
24
23
  elody/policies/authentication/multi_tenant_policy.py,sha256=g4ZYUQMmCjgLg09wj0-0lGKsJsRt7h4ppI25o1VdZHw,4039
25
24
  elody/policies/authorization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
25
  elody/policies/authorization/filter_generic_objects_policy.py,sha256=mF32moh8hRetBgG8vQW-rz4xjoRQD2yOxdI740SFSUo,6522
27
- elody/policies/authorization/filter_generic_objects_policy_v2.py,sha256=AUnnJpevVP7_XT7jbpt0KDJx5X96H12JKrWOkG3C-uE,6451
26
+ elody/policies/authorization/filter_generic_objects_policy_v2.py,sha256=_imNTxEa21DOxfn3KwCBsEIUUVkx-IAn25rpZd5gG58,6885
28
27
  elody/policies/authorization/generic_object_detail_policy.py,sha256=y6g1i3vdKMKY4xS4H2m0e1DRztrivMEomx6NkDqA0Pk,3672
29
28
  elody/policies/authorization/generic_object_mediafiles_policy.py,sha256=1-DMsV-FDkcrQCE4KL-SGlVHjTZSMPwYq1bWln2nXE4,2887
30
29
  elody/policies/authorization/generic_object_metadata_policy.py,sha256=xwtOVYmiCKgf75CydfWnV7DLI9X1yVfXPQ4-Ux0Htqk,2787
31
30
  elody/policies/authorization/generic_object_relations_policy.py,sha256=hRLeA5gXB44ufiVVaxWtAuwnXBRY1U4bLWgIadzKtmw,3712
32
31
  elody/policies/authorization/generic_object_request_policy.py,sha256=kuLczjnK5omMF2Gw5ViY_Z9MNPx_w6bNwexiMzvLiUU,4867
33
- elody/policies/authorization/generic_object_request_policy_v2.py,sha256=zaoCdV7KVAcMYNNXHYTi8Vtjkw_E8_w8aMqxJ62QqBE,5099
34
- elody/policies/authorization/mediafile_derivatives_policy.py,sha256=0JHg0gsj7pdHsGyS5ksIPYatowBjNEgyiqCU2MtV6TU,3165
35
- elody/policies/authorization/mediafile_download_policy.py,sha256=I9j-w_CrfC1vJ9AqJetKQW9ebs0zur-Wfshrb_cN8vU,2535
32
+ elody/policies/authorization/generic_object_request_policy_v2.py,sha256=bD7X9zFt0AysbyVljOcvbdmbJNRQsMZRGfqs3jjHuxY,5434
33
+ elody/policies/authorization/mediafile_derivatives_policy.py,sha256=OwNpbS8i7-LzcQDPddMWTrXk_Y4wqZxrB12Lw1dhkr0,2671
34
+ elody/policies/authorization/mediafile_download_policy.py,sha256=XMsKavBucmTh4W1kWOzpFWxJ_ZXgHVK1RS7JB4HjtQo,1979
36
35
  elody/policies/authorization/multi_tenant_policy.py,sha256=SA9H7SBjzuh8mY3gYN7pDG8TV7hdI3GEUtNeiZeNL3M,3164
37
36
  elody/policies/authorization/tenant_request_policy.py,sha256=dEgblwRAqwWVcE-O7Jn8hVL3OnwDlQhDEOcPlcElBrk,1185
38
- elody-0.0.194.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
37
+ elody-0.0.196.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
39
38
  tests/__init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
39
  tests/data.py,sha256=Q3oxduf-E3m-Z5G_p3fcs8jVy6g10I7zXKL1m94UVMI,2906
41
40
  tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
41
  tests/unit/test_csv.py,sha256=NQaOhehfQ4GuXku0Y1SA8DYjJeqqidbF50zEHAi8RZA,15923
43
42
  tests/unit/test_utils.py,sha256=g63szcEZyHhCOtrW4BnNbcgVca3oYPIOLjBdIzNwwN0,8784
44
- elody-0.0.194.dist-info/METADATA,sha256=cJy9fwyFS48f9SYqswyVJzWN3dtmATHzXkIH_XhXABg,23358
45
- elody-0.0.194.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
46
- elody-0.0.194.dist-info/top_level.txt,sha256=E0mImupLj0KmtUUCXRYEoLDRaSkuiGaOIIseAa0oQ-M,21
47
- elody-0.0.194.dist-info/RECORD,,
43
+ elody-0.0.196.dist-info/METADATA,sha256=1m1zQRz7y_tJ05tOOMfPCD69nzUNSFdW6ADucbpHITs,23358
44
+ elody-0.0.196.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ elody-0.0.196.dist-info/top_level.txt,sha256=E0mImupLj0KmtUUCXRYEoLDRaSkuiGaOIIseAa0oQ-M,21
46
+ elody-0.0.196.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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