GeneralManager 0.20.0__py3-none-any.whl → 0.21.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.

@@ -3,11 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
6
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias
7
7
 
8
- from django.contrib.auth.models import AbstractBaseUser, AbstractUser, AnonymousUser
8
+ from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
9
9
 
10
10
  from general_manager.logging import get_logger
11
+ from general_manager.permission.audit import (
12
+ PermissionAuditEvent,
13
+ audit_logging_enabled,
14
+ emit_permission_audit_event,
15
+ )
11
16
  from general_manager.permission.permission_checks import permission_functions
12
17
  from general_manager.permission.permission_data_manager import PermissionDataManager
13
18
  from general_manager.permission.utils import (
@@ -64,6 +69,18 @@ class BasePermission(ABC):
64
69
  """Return the user being evaluated for permission checks."""
65
70
  return self._request_user
66
71
 
72
+ def describe_permissions(
73
+ self,
74
+ action: Literal["create", "read", "update", "delete"],
75
+ attribute: str,
76
+ ) -> tuple[str, ...]:
77
+ """Return permission expressions associated with an action/attribute pair."""
78
+ return ()
79
+
80
+ def _is_superuser(self) -> bool:
81
+ """Return True when the current request user bypasses permission checks."""
82
+ return bool(getattr(self.request_user, "is_superuser", False))
83
+
67
84
  @classmethod
68
85
  def check_create_permission(
69
86
  cls,
@@ -85,17 +102,45 @@ class BasePermission(ABC):
85
102
  PermissionCheckError: If one or more attributes in `data` are denied for the resolved `request_user`.
86
103
  """
87
104
  request_user = cls.get_user_with_id(request_user)
88
- errors = []
89
105
  permission_data = PermissionDataManager(permission_data=data, manager=manager)
90
106
  Permission = cls(permission_data, request_user)
107
+ manager_name = manager.__name__ if manager is not None else None
108
+ if Permission._is_superuser():
109
+ if audit_logging_enabled():
110
+ for key in data.keys():
111
+ emit_permission_audit_event(
112
+ PermissionAuditEvent(
113
+ action="create",
114
+ attributes=(key,),
115
+ granted=True,
116
+ user=request_user,
117
+ manager=manager_name,
118
+ permissions=Permission.describe_permissions("create", key),
119
+ bypassed=True,
120
+ )
121
+ )
122
+ return
123
+
124
+ errors: list[str] = []
91
125
  user_identifier = getattr(request_user, "id", None)
92
126
  for key in data.keys():
93
127
  is_allowed = Permission.check_permission("create", key)
128
+ if audit_logging_enabled():
129
+ emit_permission_audit_event(
130
+ PermissionAuditEvent(
131
+ action="create",
132
+ attributes=(key,),
133
+ granted=is_allowed,
134
+ user=request_user,
135
+ manager=manager_name,
136
+ permissions=Permission.describe_permissions("create", key),
137
+ )
138
+ )
94
139
  if not is_allowed:
95
140
  logger.info(
96
141
  "permission denied",
97
142
  context={
98
- "manager": manager.__name__,
143
+ "manager": manager_name,
99
144
  "action": "create",
100
145
  "attribute": key,
101
146
  "user_id": user_identifier,
@@ -124,20 +169,47 @@ class BasePermission(ABC):
124
169
  PermissionCheckError: Raised with a list of error messages when one or more fields are not permitted to be updated.
125
170
  """
126
171
  request_user = cls.get_user_with_id(request_user)
127
-
128
- errors = []
129
172
  permission_data = PermissionDataManager.for_update(
130
173
  base_data=old_manager_instance, update_data=data
131
174
  )
132
175
  Permission = cls(permission_data, request_user)
176
+ manager_name = old_manager_instance.__class__.__name__
177
+ if Permission._is_superuser():
178
+ if audit_logging_enabled():
179
+ for key in data.keys():
180
+ emit_permission_audit_event(
181
+ PermissionAuditEvent(
182
+ action="update",
183
+ attributes=(key,),
184
+ granted=True,
185
+ user=request_user,
186
+ manager=manager_name,
187
+ permissions=Permission.describe_permissions("update", key),
188
+ bypassed=True,
189
+ )
190
+ )
191
+ return
192
+
193
+ errors: list[str] = []
133
194
  user_identifier = getattr(request_user, "id", None)
134
195
  for key in data.keys():
135
196
  is_allowed = Permission.check_permission("update", key)
197
+ if audit_logging_enabled():
198
+ emit_permission_audit_event(
199
+ PermissionAuditEvent(
200
+ action="update",
201
+ attributes=(key,),
202
+ granted=is_allowed,
203
+ user=request_user,
204
+ manager=manager_name,
205
+ permissions=Permission.describe_permissions("update", key),
206
+ )
207
+ )
136
208
  if not is_allowed:
137
209
  logger.info(
138
210
  "permission denied",
139
211
  context={
140
- "manager": old_manager_instance.__class__.__name__,
212
+ "manager": manager_name,
141
213
  "action": "update",
142
214
  "attribute": key,
143
215
  "user_id": user_identifier,
@@ -166,18 +238,45 @@ class BasePermission(ABC):
166
238
  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.
167
239
  """
168
240
  request_user = cls.get_user_with_id(request_user)
169
-
170
- errors = []
171
241
  permission_data = PermissionDataManager(manager_instance)
172
242
  Permission = cls(permission_data, request_user)
243
+ manager_name = manager_instance.__class__.__name__
244
+ if Permission._is_superuser():
245
+ if audit_logging_enabled():
246
+ for key in manager_instance.__dict__.keys():
247
+ emit_permission_audit_event(
248
+ PermissionAuditEvent(
249
+ action="delete",
250
+ attributes=(key,),
251
+ granted=True,
252
+ user=request_user,
253
+ manager=manager_name,
254
+ permissions=Permission.describe_permissions("delete", key),
255
+ bypassed=True,
256
+ )
257
+ )
258
+ return
259
+
260
+ errors: list[str] = []
173
261
  user_identifier = getattr(request_user, "id", None)
174
262
  for key in manager_instance.__dict__.keys():
175
263
  is_allowed = Permission.check_permission("delete", key)
264
+ if audit_logging_enabled():
265
+ emit_permission_audit_event(
266
+ PermissionAuditEvent(
267
+ action="delete",
268
+ attributes=(key,),
269
+ granted=is_allowed,
270
+ user=request_user,
271
+ manager=manager_name,
272
+ permissions=Permission.describe_permissions("delete", key),
273
+ )
274
+ )
176
275
  if not is_allowed:
177
276
  logger.info(
178
277
  "permission denied",
179
278
  context={
180
- "manager": manager_instance.__class__.__name__,
279
+ "manager": manager_name,
181
280
  "action": "delete",
182
281
  "attribute": key,
183
282
  "user_id": user_identifier,
@@ -251,15 +350,14 @@ class BasePermission(ABC):
251
350
  Raises:
252
351
  PermissionNotFoundError: If no permission function matches the leading name in `permission`.
253
352
  """
353
+ if self._is_superuser():
354
+ return {"filter": {}, "exclude": {}}
254
355
  permission_function, *config = permission.split(":")
255
356
  if permission_function not in permission_functions:
256
357
  raise PermissionNotFoundError(permission)
257
358
  permission_filter = permission_functions[permission_function][
258
359
  "permission_filter"
259
- ](
260
- cast(AbstractUser | AnonymousUser, self.request_user),
261
- config,
262
- )
360
+ ](self.request_user, config)
263
361
  if permission_filter is None:
264
362
  return {"filter": {}, "exclude": {}}
265
363
  return permission_filter
@@ -277,8 +375,6 @@ class BasePermission(ABC):
277
375
  Returns:
278
376
  bool: True when every sub-permission evaluates to True for the current user.
279
377
  """
280
- return validate_permission_string(
281
- permission,
282
- self.instance,
283
- cast(AbstractUser | AnonymousUser, self.request_user),
284
- )
378
+ if self._is_superuser():
379
+ return True
380
+ return validate_permission_string(permission, self.instance, self.request_user)
@@ -2,14 +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.base_permission import BasePermission
5
+ from general_manager.permission.base_permission import BasePermission, UserLike
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from general_manager.permission.permission_data_manager import (
9
9
  PermissionDataManager,
10
10
  )
11
11
  from general_manager.manager.general_manager import GeneralManager
12
- from django.contrib.auth.models import AbstractUser
13
12
 
14
13
  type permission_type = Literal[
15
14
  "create",
@@ -76,14 +75,14 @@ class ManagerBasedPermission(BasePermission):
76
75
  def __init__(
77
76
  self,
78
77
  instance: PermissionDataManager | GeneralManager,
79
- request_user: AbstractUser,
78
+ request_user: UserLike,
80
79
  ) -> None:
81
80
  """
82
81
  Initialise the permission object and gather default and attribute-level rules.
83
82
 
84
83
  Parameters:
85
84
  instance (PermissionDataManager | GeneralManager): Target data used for permission evaluation.
86
- request_user (AbstractUser): User whose permissions are being checked.
85
+ request_user (UserLike): User whose permissions are being checked.
87
86
  """
88
87
  super().__init__(instance, request_user)
89
88
  self.__set_permissions()
@@ -182,6 +181,9 @@ class ManagerBasedPermission(BasePermission):
182
181
  Raises:
183
182
  UnknownPermissionActionError: If `action` is not one of "create", "read", "update", or "delete".
184
183
  """
184
+ if self._is_superuser():
185
+ self.__overall_results[action] = True
186
+ return True
185
187
  if (
186
188
  self.__based_on_permission
187
189
  and not self.__based_on_permission.check_permission(action, attribute)
@@ -241,6 +243,8 @@ class ManagerBasedPermission(BasePermission):
241
243
  Returns:
242
244
  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.
243
245
  """
246
+ if self._is_superuser():
247
+ return [{"filter": {}, "exclude": {}}]
244
248
  __based_on__ = self.__based_on__
245
249
  filters: list[dict[Literal["filter", "exclude"], dict[str, str]]] = []
246
250
 
@@ -266,3 +270,32 @@ class ManagerBasedPermission(BasePermission):
266
270
  filters.append(self._get_permission_filter(permission))
267
271
 
268
272
  return filters
273
+
274
+ def describe_permissions(
275
+ self,
276
+ action: permission_type,
277
+ attribute: str,
278
+ ) -> tuple[str, ...]:
279
+ """Return permission expressions considered for the given action/attribute."""
280
+ if action == "create":
281
+ base_permissions: tuple[str, ...] = tuple(self.__create__)
282
+ elif action == "read":
283
+ base_permissions = tuple(self.__read__)
284
+ elif action == "update":
285
+ base_permissions = tuple(self.__update__)
286
+ elif action == "delete":
287
+ base_permissions = tuple(self.__delete__)
288
+ else:
289
+ raise UnknownPermissionActionError(action)
290
+
291
+ attribute_source = self.__attribute_permissions.get(attribute)
292
+ if isinstance(attribute_source, dict):
293
+ attribute_permissions = tuple(attribute_source.get(action, []))
294
+ else:
295
+ attribute_permissions = tuple()
296
+ combined = base_permissions + attribute_permissions
297
+ if self.__based_on_permission is not None:
298
+ combined += self.__based_on_permission.describe_permissions(
299
+ action, attribute
300
+ )
301
+ return combined
@@ -1,15 +1,26 @@
1
1
  """Permission helper for GraphQL mutations."""
2
2
 
3
3
  from __future__ import annotations
4
- from django.contrib.auth.models import AbstractUser, AnonymousUser
4
+
5
5
  from typing import Any
6
+
7
+ from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
8
+
9
+ from general_manager.permission.audit import (
10
+ PermissionAuditEvent,
11
+ audit_logging_enabled,
12
+ emit_permission_audit_event,
13
+ )
6
14
  from general_manager.permission.base_permission import (
7
15
  BasePermission,
8
16
  PermissionCheckError,
9
17
  )
10
-
11
18
  from general_manager.permission.permission_data_manager import PermissionDataManager
12
19
  from general_manager.permission.utils import validate_permission_string
20
+ from general_manager.logging import get_logger
21
+
22
+
23
+ logger = get_logger("permission.mutation")
13
24
 
14
25
 
15
26
  class MutationPermission:
@@ -18,18 +29,19 @@ class MutationPermission:
18
29
  __mutate__: list[str]
19
30
 
20
31
  def __init__(
21
- self, data: dict[str, Any], request_user: AbstractUser | AnonymousUser
32
+ self, data: dict[str, Any], request_user: AbstractBaseUser | AnonymousUser
22
33
  ) -> None:
23
34
  """
24
35
  Create a mutation permission context for the given data and user.
25
36
 
26
37
  Parameters:
27
38
  data (dict[str, Any]): Input payload for the mutation.
28
- request_user (AbstractUser | AnonymousUser): User attempting the mutation.
39
+ request_user (AbstractBaseUser | AnonymousUser): User attempting the mutation.
29
40
  """
30
41
  self._data: PermissionDataManager = PermissionDataManager(data)
31
42
  self._request_user = request_user
32
43
  self.__attribute_permissions = self.__get_attribute_permissions()
44
+ self._mutate_permissions: list[str] = getattr(self.__class__, "__mutate__", [])
33
45
 
34
46
  self.__overall_result: bool | None = None
35
47
 
@@ -39,7 +51,7 @@ class MutationPermission:
39
51
  return self._data
40
52
 
41
53
  @property
42
- def request_user(self) -> AbstractUser | AnonymousUser:
54
+ def request_user(self) -> AbstractBaseUser | AnonymousUser:
43
55
  """Return the user whose permissions are being evaluated."""
44
56
  return self._request_user
45
57
 
@@ -53,31 +65,74 @@ class MutationPermission:
53
65
  attribute_permissions[attribute] = getattr(self.__class__, attribute)
54
66
  return attribute_permissions
55
67
 
68
+ def describe_permissions(self, attribute: str) -> tuple[str, ...]:
69
+ """Return mutate-level and attribute-specific permissions for the field."""
70
+ base_permissions = tuple(self._mutate_permissions)
71
+ attribute_permissions = tuple(self.__attribute_permissions.get(attribute, []))
72
+ return base_permissions + attribute_permissions
73
+
56
74
  @classmethod
57
75
  def check(
58
76
  cls,
59
77
  data: dict[str, Any],
60
- request_user: AbstractUser | AnonymousUser | Any,
78
+ request_user: AbstractBaseUser | AnonymousUser | Any,
61
79
  ) -> None:
62
80
  """
63
81
  Validate that the given user is authorized to perform the mutation described by `data`.
64
82
 
65
83
  Parameters:
66
84
  data (dict[str, Any]): Mutation payload mapping field names to values.
67
- request_user (AbstractUser | AnonymousUser | Any): A user object or a user identifier; if an identifier is provided it will be resolved to a user.
85
+ request_user (AbstractBaseUser | AnonymousUser | Any): A user object or a user identifier; if an identifier is provided it will be resolved to a user.
68
86
 
69
87
  Raises:
70
88
  PermissionCheckError: Raised with the `request_user` and a list of field-level error messages when one or more fields fail their permission checks.
71
89
  """
72
- errors = []
73
- if not isinstance(request_user, (AbstractUser, AnonymousUser)):
90
+ errors: list[str] = []
91
+ if not isinstance(request_user, (AbstractBaseUser, AnonymousUser)):
74
92
  request_user = BasePermission.get_user_with_id(request_user)
75
93
  Permission = cls(data, request_user)
94
+ class_name = cls.__name__
95
+ is_audit_enabled = audit_logging_enabled()
96
+ if getattr(request_user, "is_superuser", False):
97
+ if is_audit_enabled:
98
+ for key in data:
99
+ emit_permission_audit_event(
100
+ PermissionAuditEvent(
101
+ action="mutation",
102
+ attributes=(key,),
103
+ granted=True,
104
+ user=request_user,
105
+ manager=class_name,
106
+ permissions=Permission.describe_permissions(key),
107
+ bypassed=True,
108
+ )
109
+ )
110
+ return
76
111
  for key in data:
77
- if not Permission.check_permission(key):
78
- errors.append(
79
- f"Permission denied for {key} with value {data[key]} for user {request_user}"
112
+ is_allowed = Permission.check_permission(key)
113
+ if is_audit_enabled:
114
+ emit_permission_audit_event(
115
+ PermissionAuditEvent(
116
+ action="mutation",
117
+ attributes=(key,),
118
+ granted=is_allowed,
119
+ user=request_user,
120
+ manager=class_name,
121
+ permissions=Permission.describe_permissions(key),
122
+ )
123
+ )
124
+ if not is_allowed:
125
+ user_identifier = getattr(request_user, "id", None)
126
+ logger.info(
127
+ "permission denied",
128
+ context={
129
+ "mutation": class_name,
130
+ "action": "mutation",
131
+ "attribute": key,
132
+ "user_id": user_identifier,
133
+ },
80
134
  )
135
+ errors.append(f"Mutation permission denied for attribute '{key}'")
81
136
  if errors:
82
137
  raise PermissionCheckError(request_user, errors)
83
138
 
@@ -109,7 +164,7 @@ class MutationPermission:
109
164
  self.__attribute_permissions[attribute]
110
165
  )
111
166
 
112
- permission = self.__check_specific_permission(self.__mutate__)
167
+ permission = self.__check_specific_permission(self._mutate_permissions)
113
168
  self.__overall_result = permission
114
169
  return permission and attribute_permission
115
170