GeneralManager 0.19.1__py3-none-any.whl → 0.20.0__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.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/_types/api.py +4 -4
- general_manager/_types/bucket.py +4 -4
- general_manager/_types/cache.py +6 -6
- general_manager/_types/factory.py +35 -35
- general_manager/_types/general_manager.py +11 -9
- general_manager/_types/interface.py +5 -5
- general_manager/_types/manager.py +4 -4
- general_manager/_types/measurement.py +1 -1
- general_manager/_types/permission.py +3 -3
- general_manager/_types/utils.py +12 -12
- general_manager/api/graphql.py +207 -98
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +120 -65
- general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
- general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
- general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
- general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
- general_manager/cache/{cacheDecorator.py → cache_decorator.py} +27 -6
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
- general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
- general_manager/cache/signals.py +1 -1
- general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
- general_manager/factory/factories.py +10 -13
- general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
- general_manager/interface/{baseInterface.py → base_interface.py} +30 -22
- general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
- general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
- general_manager/interface/{databaseInterface.py → database_interface.py} +21 -21
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
- general_manager/logging.py +133 -0
- general_manager/manager/{generalManager.py → general_manager.py} +75 -17
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +63 -17
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +55 -32
- general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
- general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
- general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
- general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
- general_manager/permission/utils.py +6 -6
- general_manager/public_api_registry.py +76 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +102 -11
- general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
- general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
- general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
- general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
- general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
- general_manager/utils/public_api.py +19 -0
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- generalmanager-0.20.0.dist-info/RECORD +78 -0
- generalmanager-0.19.1.dist-info/RECORD +0 -77
- /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
- /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
- /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
- /general_manager/utils/{formatString.py → format_string.py} +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
general_manager/manager/meta.py
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
-
from typing import Any,
|
|
7
|
-
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Type, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from general_manager.interface.base_interface import InterfaceBase
|
|
9
|
+
from general_manager.logging import get_logger
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
|
-
from general_manager.manager.
|
|
12
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
GeneralManagerType = TypeVar("GeneralManagerType", bound="GeneralManager")
|
|
14
16
|
|
|
17
|
+
logger = get_logger("manager.meta")
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class InvalidInterfaceTypeError(TypeError):
|
|
17
21
|
"""Raised when a GeneralManager is configured with an incompatible Interface class."""
|
|
@@ -78,7 +82,7 @@ class GeneralManagerMeta(type):
|
|
|
78
82
|
"""
|
|
79
83
|
Create a GeneralManager subclass, integrate any declared Interface hooks, and register the class for pending initialization and GraphQL processing.
|
|
80
84
|
|
|
81
|
-
If the class body defines an `Interface`, validates it is a subclass of `InterfaceBase`, invokes the interface's `
|
|
85
|
+
If the class body defines an `Interface`, validates it is a subclass of `InterfaceBase`, invokes the interface's `handle_interface()` pre-creation hook to allow modification of the class namespace, creates the class, then invokes the post-creation hook and registers the class for attribute initialization and global tracking. If `Interface` is not defined, creates the class directly. If `settings.AUTOCREATE_GRAPHQL` is true, registers the created class for GraphQL interface processing.
|
|
82
86
|
|
|
83
87
|
Parameters:
|
|
84
88
|
mcs (type): The metaclass creating the class.
|
|
@@ -89,8 +93,16 @@ class GeneralManagerMeta(type):
|
|
|
89
93
|
Returns:
|
|
90
94
|
type: The newly created subclass, possibly modified by Interface hooks.
|
|
91
95
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
logger.debug(
|
|
97
|
+
"creating manager class",
|
|
98
|
+
context={
|
|
99
|
+
"class_name": name,
|
|
100
|
+
"module": attrs.get("__module__"),
|
|
101
|
+
"has_interface": "Interface" in attrs,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def create_new_general_manager_class(
|
|
94
106
|
mcs: type["GeneralManagerMeta"],
|
|
95
107
|
name: str,
|
|
96
108
|
bases: tuple[type, ...],
|
|
@@ -103,23 +115,42 @@ class GeneralManagerMeta(type):
|
|
|
103
115
|
interface = attrs.pop("Interface")
|
|
104
116
|
if not issubclass(interface, InterfaceBase):
|
|
105
117
|
raise InvalidInterfaceTypeError(interface.__name__)
|
|
106
|
-
|
|
107
|
-
attrs, interface_cls, model =
|
|
108
|
-
new_class =
|
|
109
|
-
|
|
118
|
+
pre_creation, post_creation = interface.handle_interface()
|
|
119
|
+
attrs, interface_cls, model = pre_creation(name, attrs, interface)
|
|
120
|
+
new_class = create_new_general_manager_class(mcs, name, bases, attrs)
|
|
121
|
+
post_creation(new_class, interface_cls, model)
|
|
110
122
|
mcs.pending_attribute_initialization.append(new_class)
|
|
111
123
|
mcs.all_classes.append(new_class)
|
|
124
|
+
logger.debug(
|
|
125
|
+
"registered manager class with interface",
|
|
126
|
+
context={
|
|
127
|
+
"class_name": new_class.__name__,
|
|
128
|
+
"interface": interface_cls.__name__,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
112
131
|
|
|
113
132
|
else:
|
|
114
|
-
new_class =
|
|
133
|
+
new_class = create_new_general_manager_class(mcs, name, bases, attrs)
|
|
134
|
+
logger.debug(
|
|
135
|
+
"registered manager class without interface",
|
|
136
|
+
context={
|
|
137
|
+
"class_name": new_class.__name__,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
115
140
|
|
|
116
141
|
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
|
117
142
|
mcs.pending_graphql_interfaces.append(new_class)
|
|
143
|
+
logger.debug(
|
|
144
|
+
"queued manager for graphql generation",
|
|
145
|
+
context={
|
|
146
|
+
"class_name": new_class.__name__,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
118
149
|
|
|
119
150
|
return new_class
|
|
120
151
|
|
|
121
152
|
@staticmethod
|
|
122
|
-
def
|
|
153
|
+
def create_at_properties_for_attributes(
|
|
123
154
|
attributes: Iterable[str], new_class: Type[GeneralManager]
|
|
124
155
|
) -> None:
|
|
125
156
|
"""
|
|
@@ -132,14 +163,14 @@ class GeneralManagerMeta(type):
|
|
|
132
163
|
new_class (Type[GeneralManager]): Class that will receive the generated descriptor attributes.
|
|
133
164
|
"""
|
|
134
165
|
|
|
135
|
-
def
|
|
166
|
+
def descriptor_method(
|
|
136
167
|
attr_name: str,
|
|
137
168
|
new_class: type,
|
|
138
169
|
) -> object:
|
|
139
170
|
"""
|
|
140
171
|
Create a descriptor that provides attribute access backed by an instance's interface attributes.
|
|
141
172
|
|
|
142
|
-
When accessed on the class, the descriptor returns the field type by delegating to the class's `Interface.
|
|
173
|
+
When accessed on the class, the descriptor returns the field type by delegating to the class's `Interface.get_field_type` for the configured attribute name. When accessed on an instance, it returns the value stored in `instance._attributes[attr_name]`. If the stored value is callable, it is invoked with `instance._interface` and the resulting value is returned. If the attribute is not present on the instance, a `MissingAttributeError` is raised. If invoking a callable attribute raises an exception, that error is wrapped in `AttributeEvaluationError`.
|
|
143
174
|
|
|
144
175
|
Parameters:
|
|
145
176
|
attr_name (str): The name of the attribute the descriptor resolves.
|
|
@@ -164,7 +195,7 @@ class GeneralManagerMeta(type):
|
|
|
164
195
|
"""
|
|
165
196
|
Provide the class field type when accessed on the class, or resolve and return the stored attribute value for an instance.
|
|
166
197
|
|
|
167
|
-
When accessed on a class, returns the field type from the class's Interface via Interface.
|
|
198
|
+
When accessed on a class, returns the field type from the class's Interface via Interface.get_field_type.
|
|
168
199
|
When accessed on an instance, retrieves the value stored in instance._attributes for this descriptor's attribute name;
|
|
169
200
|
if the stored value is callable, it is invoked with instance._interface and the result is returned.
|
|
170
201
|
|
|
@@ -176,9 +207,16 @@ class GeneralManagerMeta(type):
|
|
|
176
207
|
AttributeEvaluationError: If calling a callable attribute raises an exception; the original exception is wrapped.
|
|
177
208
|
"""
|
|
178
209
|
if instance is None:
|
|
179
|
-
return self._class.Interface.
|
|
210
|
+
return self._class.Interface.get_field_type(self._attr_name)
|
|
180
211
|
attribute = instance._attributes.get(self._attr_name, _nonExistent)
|
|
181
212
|
if attribute is _nonExistent:
|
|
213
|
+
logger.warning(
|
|
214
|
+
"missing attribute on manager instance",
|
|
215
|
+
context={
|
|
216
|
+
"attribute": self._attr_name,
|
|
217
|
+
"manager": instance.__class__.__name__,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
182
220
|
raise MissingAttributeError(
|
|
183
221
|
self._attr_name, instance.__class__.__name__
|
|
184
222
|
)
|
|
@@ -186,10 +224,18 @@ class GeneralManagerMeta(type):
|
|
|
186
224
|
try:
|
|
187
225
|
attribute = attribute(instance._interface)
|
|
188
226
|
except Exception as e:
|
|
227
|
+
logger.exception(
|
|
228
|
+
"attribute evaluation failed",
|
|
229
|
+
context={
|
|
230
|
+
"attribute": self._attr_name,
|
|
231
|
+
"manager": instance.__class__.__name__,
|
|
232
|
+
"error": type(e).__name__,
|
|
233
|
+
},
|
|
234
|
+
)
|
|
189
235
|
raise AttributeEvaluationError(self._attr_name, e) from e
|
|
190
236
|
return attribute
|
|
191
237
|
|
|
192
238
|
return Descriptor(attr_name, cast(Type[Any], new_class))
|
|
193
239
|
|
|
194
240
|
for attr_name in attributes:
|
|
195
|
-
setattr(new_class, attr_name,
|
|
241
|
+
setattr(new_class, attr_name, descriptor_method(attr_name, new_class))
|
|
@@ -194,7 +194,7 @@ class Measurement:
|
|
|
194
194
|
raise InvalidMeasurementInitializationError() from error
|
|
195
195
|
if not isinstance(value, Decimal):
|
|
196
196
|
value = Decimal(str(value))
|
|
197
|
-
self.__quantity = ureg.Quantity(self.
|
|
197
|
+
self.__quantity = ureg.Quantity(self.format_decimal(value), unit)
|
|
198
198
|
|
|
199
199
|
def __getstate__(self) -> dict[str, str]:
|
|
200
200
|
"""
|
|
@@ -221,7 +221,7 @@ class Measurement:
|
|
|
221
221
|
"""
|
|
222
222
|
value = Decimal(state["magnitude"])
|
|
223
223
|
unit = state["unit"]
|
|
224
|
-
self.__quantity = ureg.Quantity(self.
|
|
224
|
+
self.__quantity = ureg.Quantity(self.format_decimal(value), unit)
|
|
225
225
|
|
|
226
226
|
@property
|
|
227
227
|
def quantity(self) -> PlainQuantity:
|
|
@@ -282,7 +282,7 @@ class Measurement:
|
|
|
282
282
|
return cls(value, unit)
|
|
283
283
|
|
|
284
284
|
@staticmethod
|
|
285
|
-
def
|
|
285
|
+
def format_decimal(value: Decimal) -> Decimal:
|
|
286
286
|
"""
|
|
287
287
|
Normalise decimals so integers have no fractional component.
|
|
288
288
|
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""Base permission contract used by GeneralManager instances."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
from abc import ABC, abstractmethod
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
|
|
6
|
-
from general_manager.permission.permissionChecks import permission_functions
|
|
7
7
|
|
|
8
|
-
from django.contrib.auth.models import
|
|
9
|
-
|
|
8
|
+
from django.contrib.auth.models import AbstractBaseUser, AbstractUser, AnonymousUser
|
|
9
|
+
|
|
10
|
+
from general_manager.logging import get_logger
|
|
11
|
+
from general_manager.permission.permission_checks import permission_functions
|
|
12
|
+
from general_manager.permission.permission_data_manager import PermissionDataManager
|
|
10
13
|
from general_manager.permission.utils import (
|
|
11
|
-
validatePermissionString,
|
|
12
14
|
PermissionNotFoundError,
|
|
15
|
+
validate_permission_string,
|
|
13
16
|
)
|
|
14
|
-
import logging
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
17
|
-
from general_manager.manager.
|
|
19
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
18
20
|
from general_manager.manager.meta import GeneralManagerMeta
|
|
19
21
|
|
|
20
|
-
logger =
|
|
22
|
+
logger = get_logger("permission.base")
|
|
21
23
|
|
|
22
24
|
UserLike: TypeAlias = AbstractBaseUser | AnonymousUser
|
|
23
25
|
|
|
@@ -63,7 +65,7 @@ class BasePermission(ABC):
|
|
|
63
65
|
return self._request_user
|
|
64
66
|
|
|
65
67
|
@classmethod
|
|
66
|
-
def
|
|
68
|
+
def check_create_permission(
|
|
67
69
|
cls,
|
|
68
70
|
data: dict[str, Any],
|
|
69
71
|
manager: type[GeneralManager],
|
|
@@ -82,22 +84,29 @@ class BasePermission(ABC):
|
|
|
82
84
|
Raises:
|
|
83
85
|
PermissionCheckError: If one or more attributes in `data` are denied for the resolved `request_user`.
|
|
84
86
|
"""
|
|
85
|
-
request_user = cls.
|
|
87
|
+
request_user = cls.get_user_with_id(request_user)
|
|
86
88
|
errors = []
|
|
87
89
|
permission_data = PermissionDataManager(permission_data=data, manager=manager)
|
|
88
90
|
Permission = cls(permission_data, request_user)
|
|
91
|
+
user_identifier = getattr(request_user, "id", None)
|
|
89
92
|
for key in data.keys():
|
|
90
|
-
is_allowed = Permission.
|
|
93
|
+
is_allowed = Permission.check_permission("create", key)
|
|
91
94
|
if not is_allowed:
|
|
92
|
-
logger.
|
|
93
|
-
|
|
95
|
+
logger.info(
|
|
96
|
+
"permission denied",
|
|
97
|
+
context={
|
|
98
|
+
"manager": manager.__name__,
|
|
99
|
+
"action": "create",
|
|
100
|
+
"attribute": key,
|
|
101
|
+
"user_id": user_identifier,
|
|
102
|
+
},
|
|
94
103
|
)
|
|
95
104
|
errors.append(f"Create permission denied for attribute '{key}'")
|
|
96
105
|
if errors:
|
|
97
106
|
raise PermissionCheckError(request_user, errors)
|
|
98
107
|
|
|
99
108
|
@classmethod
|
|
100
|
-
def
|
|
109
|
+
def check_update_permission(
|
|
101
110
|
cls,
|
|
102
111
|
data: dict[str, Any],
|
|
103
112
|
old_manager_instance: GeneralManager,
|
|
@@ -109,30 +118,37 @@ class BasePermission(ABC):
|
|
|
109
118
|
Parameters:
|
|
110
119
|
data (dict[str, Any]): Mapping of attribute names to new values to be applied.
|
|
111
120
|
old_manager_instance (GeneralManager): Existing manager instance whose current state is used to evaluate update permissions.
|
|
112
|
-
request_user (UserLike | Any): User instance or user id; non-user values will be resolved to a User or AnonymousUser via
|
|
121
|
+
request_user (UserLike | Any): User instance or user id; non-user values will be resolved to a User or AnonymousUser via get_user_with_id.
|
|
113
122
|
|
|
114
123
|
Raises:
|
|
115
124
|
PermissionCheckError: Raised with a list of error messages when one or more fields are not permitted to be updated.
|
|
116
125
|
"""
|
|
117
|
-
request_user = cls.
|
|
126
|
+
request_user = cls.get_user_with_id(request_user)
|
|
118
127
|
|
|
119
128
|
errors = []
|
|
120
|
-
permission_data = PermissionDataManager.
|
|
129
|
+
permission_data = PermissionDataManager.for_update(
|
|
121
130
|
base_data=old_manager_instance, update_data=data
|
|
122
131
|
)
|
|
123
132
|
Permission = cls(permission_data, request_user)
|
|
133
|
+
user_identifier = getattr(request_user, "id", None)
|
|
124
134
|
for key in data.keys():
|
|
125
|
-
is_allowed = Permission.
|
|
135
|
+
is_allowed = Permission.check_permission("update", key)
|
|
126
136
|
if not is_allowed:
|
|
127
|
-
logger.
|
|
128
|
-
|
|
137
|
+
logger.info(
|
|
138
|
+
"permission denied",
|
|
139
|
+
context={
|
|
140
|
+
"manager": old_manager_instance.__class__.__name__,
|
|
141
|
+
"action": "update",
|
|
142
|
+
"attribute": key,
|
|
143
|
+
"user_id": user_identifier,
|
|
144
|
+
},
|
|
129
145
|
)
|
|
130
146
|
errors.append(f"Update permission denied for attribute '{key}'")
|
|
131
147
|
if errors:
|
|
132
148
|
raise PermissionCheckError(request_user, errors)
|
|
133
149
|
|
|
134
150
|
@classmethod
|
|
135
|
-
def
|
|
151
|
+
def check_delete_permission(
|
|
136
152
|
cls,
|
|
137
153
|
manager_instance: GeneralManager,
|
|
138
154
|
request_user: UserLike | Any,
|
|
@@ -149,23 +165,30 @@ class BasePermission(ABC):
|
|
|
149
165
|
Raises:
|
|
150
166
|
PermissionCheckError: If one or more attributes are not permitted for deletion by request_user. The exception carries the user and the list of denial messages.
|
|
151
167
|
"""
|
|
152
|
-
request_user = cls.
|
|
168
|
+
request_user = cls.get_user_with_id(request_user)
|
|
153
169
|
|
|
154
170
|
errors = []
|
|
155
171
|
permission_data = PermissionDataManager(manager_instance)
|
|
156
172
|
Permission = cls(permission_data, request_user)
|
|
173
|
+
user_identifier = getattr(request_user, "id", None)
|
|
157
174
|
for key in manager_instance.__dict__.keys():
|
|
158
|
-
is_allowed = Permission.
|
|
175
|
+
is_allowed = Permission.check_permission("delete", key)
|
|
159
176
|
if not is_allowed:
|
|
160
|
-
logger.
|
|
161
|
-
|
|
177
|
+
logger.info(
|
|
178
|
+
"permission denied",
|
|
179
|
+
context={
|
|
180
|
+
"manager": manager_instance.__class__.__name__,
|
|
181
|
+
"action": "delete",
|
|
182
|
+
"attribute": key,
|
|
183
|
+
"user_id": user_identifier,
|
|
184
|
+
},
|
|
162
185
|
)
|
|
163
186
|
errors.append(f"Delete permission denied for attribute '{key}'")
|
|
164
187
|
if errors:
|
|
165
188
|
raise PermissionCheckError(request_user, errors)
|
|
166
189
|
|
|
167
190
|
@staticmethod
|
|
168
|
-
def
|
|
191
|
+
def get_user_with_id(
|
|
169
192
|
user: Any | UserLike,
|
|
170
193
|
) -> UserLike:
|
|
171
194
|
"""
|
|
@@ -185,12 +208,12 @@ class BasePermission(ABC):
|
|
|
185
208
|
if isinstance(user, (AbstractBaseUser, AnonymousUser)):
|
|
186
209
|
return user
|
|
187
210
|
try:
|
|
188
|
-
return User.objects.get(
|
|
189
|
-
except User.DoesNotExist:
|
|
211
|
+
return User.objects.get(pk=user)
|
|
212
|
+
except (User.DoesNotExist, ValueError, TypeError):
|
|
190
213
|
return AnonymousUser()
|
|
191
214
|
|
|
192
215
|
@abstractmethod
|
|
193
|
-
def
|
|
216
|
+
def check_permission(
|
|
194
217
|
self,
|
|
195
218
|
action: Literal["create", "read", "update", "delete"],
|
|
196
219
|
attribute: str,
|
|
@@ -207,13 +230,13 @@ class BasePermission(ABC):
|
|
|
207
230
|
"""
|
|
208
231
|
raise NotImplementedError
|
|
209
232
|
|
|
210
|
-
def
|
|
233
|
+
def get_permission_filter(
|
|
211
234
|
self,
|
|
212
235
|
) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
|
|
213
236
|
"""Return the filter/exclude constraints associated with this permission."""
|
|
214
237
|
raise NotImplementedError
|
|
215
238
|
|
|
216
|
-
def
|
|
239
|
+
def _get_permission_filter(
|
|
217
240
|
self, permission: str
|
|
218
241
|
) -> dict[Literal["filter", "exclude"], dict[str, str]]:
|
|
219
242
|
"""
|
|
@@ -241,7 +264,7 @@ class BasePermission(ABC):
|
|
|
241
264
|
return {"filter": {}, "exclude": {}}
|
|
242
265
|
return permission_filter
|
|
243
266
|
|
|
244
|
-
def
|
|
267
|
+
def validate_permission_string(
|
|
245
268
|
self,
|
|
246
269
|
permission: str,
|
|
247
270
|
) -> bool:
|
|
@@ -254,7 +277,7 @@ class BasePermission(ABC):
|
|
|
254
277
|
Returns:
|
|
255
278
|
bool: True when every sub-permission evaluates to True for the current user.
|
|
256
279
|
"""
|
|
257
|
-
return
|
|
280
|
+
return validate_permission_string(
|
|
258
281
|
permission,
|
|
259
282
|
self.instance,
|
|
260
283
|
cast(AbstractUser | AnonymousUser, self.request_user),
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from typing import TYPE_CHECKING, Literal, Optional, Dict
|
|
5
|
-
from general_manager.permission.
|
|
5
|
+
from general_manager.permission.base_permission import BasePermission
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
from general_manager.permission.
|
|
8
|
+
from general_manager.permission.permission_data_manager import (
|
|
9
9
|
PermissionDataManager,
|
|
10
10
|
)
|
|
11
|
-
from general_manager.manager.
|
|
11
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
12
12
|
from django.contrib.auth.models import AbstractUser
|
|
13
13
|
|
|
14
14
|
type permission_type = Literal[
|
|
@@ -86,10 +86,10 @@ class ManagerBasedPermission(BasePermission):
|
|
|
86
86
|
request_user (AbstractUser): User whose permissions are being checked.
|
|
87
87
|
"""
|
|
88
88
|
super().__init__(instance, request_user)
|
|
89
|
-
self.
|
|
89
|
+
self.__set_permissions()
|
|
90
90
|
|
|
91
|
-
self.__attribute_permissions = self.
|
|
92
|
-
self.__based_on_permission = self.
|
|
91
|
+
self.__attribute_permissions = self.__get_attribute_permissions()
|
|
92
|
+
self.__based_on_permission = self.__get_based_on_permission()
|
|
93
93
|
self.__overall_results: Dict[permission_type, Optional[bool]] = {
|
|
94
94
|
"create": None,
|
|
95
95
|
"read": None,
|
|
@@ -97,7 +97,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
97
97
|
"delete": None,
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
def
|
|
100
|
+
def __set_permissions(self, skip_based_on: bool = False) -> None:
|
|
101
101
|
"""Populate CRUD permissions using class-level defaults and overrides."""
|
|
102
102
|
default_read = ["public"]
|
|
103
103
|
default_write = ["isAuthenticated"]
|
|
@@ -111,7 +111,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
111
111
|
self.__update__ = getattr(self.__class__, "__update__", default_write)
|
|
112
112
|
self.__delete__ = getattr(self.__class__, "__delete__", default_write)
|
|
113
113
|
|
|
114
|
-
def
|
|
114
|
+
def __get_based_on_permission(self) -> Optional[BasePermission]:
|
|
115
115
|
"""
|
|
116
116
|
Resolve and return a BasePermission instance from the manager attribute named by the class-level `__based_on__` configuration.
|
|
117
117
|
|
|
@@ -124,7 +124,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
124
124
|
InvalidBasedOnConfigurationError: If the configured `__based_on__` attribute does not exist on the target instance.
|
|
125
125
|
InvalidBasedOnTypeError: If the configured attribute exists but does not resolve to a `GeneralManager` or subclass.
|
|
126
126
|
"""
|
|
127
|
-
from general_manager.manager.
|
|
127
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
128
128
|
|
|
129
129
|
__based_on__ = self.__based_on__
|
|
130
130
|
if __based_on__ is None:
|
|
@@ -134,7 +134,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
134
134
|
if basis_object is notExistent:
|
|
135
135
|
raise InvalidBasedOnConfigurationError(__based_on__)
|
|
136
136
|
if basis_object is None:
|
|
137
|
-
self.
|
|
137
|
+
self.__set_permissions(skip_based_on=True)
|
|
138
138
|
return None
|
|
139
139
|
if not isinstance(basis_object, GeneralManager) and not (
|
|
140
140
|
isinstance(basis_object, type) and issubclass(basis_object, GeneralManager)
|
|
@@ -154,7 +154,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
154
154
|
request_user=self.request_user,
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
-
def
|
|
157
|
+
def __get_attribute_permissions(
|
|
158
158
|
self,
|
|
159
159
|
) -> dict[str, dict[permission_type, list[str]]]:
|
|
160
160
|
"""Collect attribute-level permission overrides defined on the class."""
|
|
@@ -164,7 +164,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
164
164
|
attribute_permissions[attribute] = getattr(self, attribute)
|
|
165
165
|
return attribute_permissions
|
|
166
166
|
|
|
167
|
-
def
|
|
167
|
+
def check_permission(
|
|
168
168
|
self,
|
|
169
169
|
action: permission_type,
|
|
170
170
|
attribute: str,
|
|
@@ -184,7 +184,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
184
184
|
"""
|
|
185
185
|
if (
|
|
186
186
|
self.__based_on_permission
|
|
187
|
-
and not self.__based_on_permission.
|
|
187
|
+
and not self.__based_on_permission.check_permission(action, attribute)
|
|
188
188
|
):
|
|
189
189
|
return False
|
|
190
190
|
|
|
@@ -210,15 +210,15 @@ class ManagerBasedPermission(BasePermission):
|
|
|
210
210
|
return last_result
|
|
211
211
|
attribute_permission = True
|
|
212
212
|
else:
|
|
213
|
-
attribute_permission = self.
|
|
213
|
+
attribute_permission = self.__check_specific_permission(
|
|
214
214
|
self.__attribute_permissions[attribute][action]
|
|
215
215
|
)
|
|
216
216
|
|
|
217
|
-
permission = self.
|
|
217
|
+
permission = self.__check_specific_permission(permissions)
|
|
218
218
|
self.__overall_results[action] = permission
|
|
219
219
|
return permission and attribute_permission
|
|
220
220
|
|
|
221
|
-
def
|
|
221
|
+
def __check_specific_permission(
|
|
222
222
|
self,
|
|
223
223
|
permissions: list[str],
|
|
224
224
|
) -> bool:
|
|
@@ -226,17 +226,17 @@ class ManagerBasedPermission(BasePermission):
|
|
|
226
226
|
if not permissions:
|
|
227
227
|
return True
|
|
228
228
|
for permission in permissions:
|
|
229
|
-
if self.
|
|
229
|
+
if self.validate_permission_string(permission):
|
|
230
230
|
return True
|
|
231
231
|
return False
|
|
232
232
|
|
|
233
|
-
def
|
|
233
|
+
def get_permission_filter(
|
|
234
234
|
self,
|
|
235
235
|
) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
|
|
236
236
|
"""
|
|
237
237
|
Builds queryset filter and exclude mappings derived from this permission configuration.
|
|
238
238
|
|
|
239
|
-
If a based-on permission exists, its filters and excludes are included with each key prefixed by the name in __based_on__. Then appends filters produced from this class's read permissions via
|
|
239
|
+
If a based-on permission exists, its filters and excludes are included with each key prefixed by the name in __based_on__. Then appends filters produced from this class's read permissions via _get_permission_filter.
|
|
240
240
|
|
|
241
241
|
Returns:
|
|
242
242
|
list[dict[Literal["filter", "exclude"], dict[str, str]]]: A list of dictionaries each containing "filter" and "exclude" mappings where keys are queryset lookups and values are lookup values.
|
|
@@ -245,7 +245,7 @@ class ManagerBasedPermission(BasePermission):
|
|
|
245
245
|
filters: list[dict[Literal["filter", "exclude"], dict[str, str]]] = []
|
|
246
246
|
|
|
247
247
|
if self.__based_on_permission is not None:
|
|
248
|
-
base_permissions = self.__based_on_permission.
|
|
248
|
+
base_permissions = self.__based_on_permission.get_permission_filter()
|
|
249
249
|
for base_permission in base_permissions:
|
|
250
250
|
filter = base_permission.get("filter", {})
|
|
251
251
|
exclude = base_permission.get("exclude", {})
|
|
@@ -263,6 +263,6 @@ class ManagerBasedPermission(BasePermission):
|
|
|
263
263
|
)
|
|
264
264
|
|
|
265
265
|
for permission in self.__read__:
|
|
266
|
-
filters.append(self.
|
|
266
|
+
filters.append(self._get_permission_filter(permission))
|
|
267
267
|
|
|
268
268
|
return filters
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
|
5
5
|
from typing import Any
|
|
6
|
-
from general_manager.permission.
|
|
6
|
+
from general_manager.permission.base_permission import (
|
|
7
7
|
BasePermission,
|
|
8
8
|
PermissionCheckError,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
from general_manager.permission.
|
|
12
|
-
from general_manager.permission.utils import
|
|
11
|
+
from general_manager.permission.permission_data_manager import PermissionDataManager
|
|
12
|
+
from general_manager.permission.utils import validate_permission_string
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class MutationPermission:
|
|
@@ -29,7 +29,7 @@ class MutationPermission:
|
|
|
29
29
|
"""
|
|
30
30
|
self._data: PermissionDataManager = PermissionDataManager(data)
|
|
31
31
|
self._request_user = request_user
|
|
32
|
-
self.__attribute_permissions = self.
|
|
32
|
+
self.__attribute_permissions = self.__get_attribute_permissions()
|
|
33
33
|
|
|
34
34
|
self.__overall_result: bool | None = None
|
|
35
35
|
|
|
@@ -43,7 +43,7 @@ class MutationPermission:
|
|
|
43
43
|
"""Return the user whose permissions are being evaluated."""
|
|
44
44
|
return self._request_user
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def __get_attribute_permissions(
|
|
47
47
|
self,
|
|
48
48
|
) -> dict[str, list[str]]:
|
|
49
49
|
"""Collect attribute-specific permission expressions declared on the class."""
|
|
@@ -71,17 +71,17 @@ class MutationPermission:
|
|
|
71
71
|
"""
|
|
72
72
|
errors = []
|
|
73
73
|
if not isinstance(request_user, (AbstractUser, AnonymousUser)):
|
|
74
|
-
request_user = BasePermission.
|
|
74
|
+
request_user = BasePermission.get_user_with_id(request_user)
|
|
75
75
|
Permission = cls(data, request_user)
|
|
76
76
|
for key in data:
|
|
77
|
-
if not Permission.
|
|
77
|
+
if not Permission.check_permission(key):
|
|
78
78
|
errors.append(
|
|
79
79
|
f"Permission denied for {key} with value {data[key]} for user {request_user}"
|
|
80
80
|
)
|
|
81
81
|
if errors:
|
|
82
82
|
raise PermissionCheckError(request_user, errors)
|
|
83
83
|
|
|
84
|
-
def
|
|
84
|
+
def check_permission(
|
|
85
85
|
self,
|
|
86
86
|
attribute: str,
|
|
87
87
|
) -> bool:
|
|
@@ -105,20 +105,20 @@ class MutationPermission:
|
|
|
105
105
|
return last_result
|
|
106
106
|
attribute_permission = True
|
|
107
107
|
else:
|
|
108
|
-
attribute_permission = self.
|
|
108
|
+
attribute_permission = self.__check_specific_permission(
|
|
109
109
|
self.__attribute_permissions[attribute]
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
permission = self.
|
|
112
|
+
permission = self.__check_specific_permission(self.__mutate__)
|
|
113
113
|
self.__overall_result = permission
|
|
114
114
|
return permission and attribute_permission
|
|
115
115
|
|
|
116
|
-
def
|
|
116
|
+
def __check_specific_permission(
|
|
117
117
|
self,
|
|
118
118
|
permissions: list[str],
|
|
119
119
|
) -> bool:
|
|
120
120
|
"""Return True when any permission expression evaluates to True."""
|
|
121
121
|
for permission in permissions:
|
|
122
|
-
if
|
|
122
|
+
if validate_permission_string(permission, self.data, self.request_user):
|
|
123
123
|
return True
|
|
124
124
|
return False
|
|
@@ -4,10 +4,10 @@ from typing import Any, Callable, TYPE_CHECKING, TypedDict, Literal
|
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
|
7
|
-
from general_manager.permission.
|
|
7
|
+
from general_manager.permission.permission_data_manager import (
|
|
8
8
|
PermissionDataManager,
|
|
9
9
|
)
|
|
10
|
-
from general_manager.manager.
|
|
10
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
11
11
|
from general_manager.manager.meta import GeneralManagerMeta
|
|
12
12
|
|
|
13
13
|
|