GeneralManager 0.20.1__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.

@@ -14,6 +14,8 @@ __all__ = [
14
14
  "get_logger",
15
15
  "graph_ql_mutation",
16
16
  "graph_ql_property",
17
+ "permission_functions",
18
+ "register_permission",
17
19
  ]
18
20
 
19
21
  from general_manager.interface.calculation_interface import CalculationInterface
@@ -27,3 +29,5 @@ from general_manager.rule.rule import Rule
27
29
  from general_manager.logging import get_logger
28
30
  from general_manager.api.mutation import graph_ql_mutation
29
31
  from general_manager.api.property import graph_ql_property
32
+ from general_manager.permission.permission_checks import permission_functions
33
+ from general_manager.permission.permission_checks import register_permission
@@ -3,11 +3,27 @@ from __future__ import annotations
3
3
  """Type-only imports for public API re-exports."""
4
4
 
5
5
  __all__ = [
6
+ "AuditLogger",
6
7
  "BasePermission",
8
+ "DatabaseAuditLogger",
9
+ "FileAuditLogger",
7
10
  "ManagerBasedPermission",
8
11
  "MutationPermission",
12
+ "PermissionAuditEvent",
13
+ "configure_audit_logger",
14
+ "configure_audit_logger_from_settings",
15
+ "permission_functions",
16
+ "register_permission",
9
17
  ]
10
18
 
19
+ from general_manager.permission.audit import AuditLogger
11
20
  from general_manager.permission.base_permission import BasePermission
21
+ from general_manager.permission.audit import DatabaseAuditLogger
22
+ from general_manager.permission.audit import FileAuditLogger
12
23
  from general_manager.permission.manager_based_permission import ManagerBasedPermission
13
24
  from general_manager.permission.mutation_permission import MutationPermission
25
+ from general_manager.permission.audit import PermissionAuditEvent
26
+ from general_manager.permission.audit import configure_audit_logger
27
+ from general_manager.permission.audit import configure_audit_logger_from_settings
28
+ from general_manager.permission.permission_checks import permission_functions
29
+ from general_manager.permission.permission_checks import register_permission
general_manager/apps.py CHANGED
@@ -20,6 +20,7 @@ from general_manager.logging import get_logger
20
20
  from general_manager.manager.general_manager import GeneralManager
21
21
  from general_manager.manager.input import Input
22
22
  from general_manager.manager.meta import GeneralManagerMeta
23
+ from general_manager.permission.audit import configure_audit_logger_from_settings
23
24
 
24
25
 
25
26
  class MissingRootUrlconfError(RuntimeError):
@@ -66,6 +67,7 @@ class GeneralmanagerConfig(AppConfig):
66
67
  GeneralManagerMeta.pending_attribute_initialization,
67
68
  GeneralManagerMeta.all_classes,
68
69
  )
70
+ configure_audit_logger_from_settings(settings)
69
71
  if getattr(settings, "AUTOCREATE_GRAPHQL", False):
70
72
  self.handle_graph_ql(GeneralManagerMeta.pending_graphql_interfaces)
71
73
 
@@ -0,0 +1,380 @@
1
+ """Lightweight audit logging hooks for permission evaluations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import json
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from queue import Empty, SimpleQueue
10
+ from threading import Event, Thread, Lock
11
+ from typing import Any, Iterable, Mapping, Protocol, runtime_checkable, Literal
12
+
13
+ from django.apps import apps
14
+ from django.db import connections, models
15
+ from django.utils import timezone
16
+
17
+
18
+ AuditAction = Literal["create", "read", "update", "delete", "mutation"]
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class PermissionAuditEvent:
23
+ """
24
+ Payload describing a permission evaluation outcome.
25
+
26
+ Attributes:
27
+ action (AuditAction): CRUD or mutation action that was evaluated.
28
+ attributes (tuple[str, ...]): Collection of attribute names covered by this evaluation.
29
+ granted (bool): True when the action was permitted.
30
+ user (Any): User object involved in the evaluation; consumers may extract ids.
31
+ manager (str | None): Name of the manager class (when applicable).
32
+ permissions (tuple[str, ...]): Permission expressions that were considered.
33
+ bypassed (bool): True when the decision relied on a superuser bypass.
34
+ metadata (Mapping[str, Any] | None): Optional additional context.
35
+ """
36
+
37
+ action: AuditAction
38
+ attributes: tuple[str, ...]
39
+ granted: bool
40
+ user: Any
41
+ manager: str | None
42
+ permissions: tuple[str, ...] = ()
43
+ bypassed: bool = False
44
+ metadata: Mapping[str, Any] | None = None
45
+
46
+
47
+ @runtime_checkable
48
+ class AuditLogger(Protocol):
49
+ """Protocol describing the expected behaviour of an audit logger implementation."""
50
+
51
+ def record(self, event: PermissionAuditEvent) -> None:
52
+ """Persist or forward a permission audit event."""
53
+
54
+
55
+ class _NoOpAuditLogger:
56
+ """Fallback logger used when no audit logger is configured."""
57
+
58
+ __slots__ = ()
59
+
60
+ def record(self, _event: PermissionAuditEvent) -> None:
61
+ """Ignore the audit event."""
62
+ return
63
+
64
+
65
+ _NOOP_LOGGER = _NoOpAuditLogger()
66
+ _audit_logger: AuditLogger = _NOOP_LOGGER
67
+ _SETTINGS_KEY = "GENERAL_MANAGER"
68
+ _AUDIT_LOGGER_KEY = "AUDIT_LOGGER"
69
+
70
+
71
+ def configure_audit_logger(logger: AuditLogger | None) -> None:
72
+ """
73
+ Configure the audit logger used by permission checks.
74
+
75
+ Parameters:
76
+ logger (AuditLogger | None): Concrete logger implementation. Passing ``None``
77
+ resets the logger to a no-op implementation.
78
+ """
79
+ global _audit_logger
80
+ _audit_logger = logger or _NOOP_LOGGER
81
+
82
+
83
+ def get_audit_logger() -> AuditLogger:
84
+ """Return the currently configured audit logger."""
85
+ return _audit_logger
86
+
87
+
88
+ def audit_logging_enabled() -> bool:
89
+ """Return True when audit logging is active."""
90
+ return _audit_logger is not _NOOP_LOGGER
91
+
92
+
93
+ def emit_permission_audit_event(event: PermissionAuditEvent) -> None:
94
+ """
95
+ Forward an audit event to the configured logger when logging is enabled.
96
+
97
+ Parameters:
98
+ event (PermissionAuditEvent): Event payload to record.
99
+ """
100
+ if _audit_logger is _NOOP_LOGGER:
101
+ return
102
+ _audit_logger.record(event)
103
+
104
+
105
+ def _serialize_event(event: PermissionAuditEvent) -> dict[str, Any]:
106
+ """Convert an audit event into a JSON-serialisable mapping."""
107
+ user_pk = getattr(event.user, "pk", None)
108
+ user_id = None if user_pk is None else str(user_pk)
109
+ return {
110
+ "timestamp": timezone.now().isoformat(),
111
+ "action": event.action,
112
+ "attributes": list(event.attributes),
113
+ "granted": event.granted,
114
+ "bypassed": event.bypassed,
115
+ "manager": event.manager,
116
+ "user_id": user_id,
117
+ "user": None if user_id is not None else repr(event.user),
118
+ "permissions": list(event.permissions),
119
+ "metadata": event.metadata,
120
+ }
121
+
122
+
123
+ def _resolve_logger_reference(value: Any) -> AuditLogger | None:
124
+ """Resolve audit logger setting values into concrete logger instances."""
125
+ if value is None:
126
+ return None
127
+ if isinstance(value, str):
128
+ from django.utils.module_loading import import_string
129
+
130
+ resolved = import_string(value)
131
+ elif isinstance(value, Mapping):
132
+ from django.utils.module_loading import import_string
133
+
134
+ class_path = value.get("class")
135
+ options = value.get("options", {})
136
+ if class_path is None:
137
+ return None
138
+ resolved = (
139
+ import_string(class_path) if isinstance(class_path, str) else class_path
140
+ )
141
+ if isinstance(resolved, type):
142
+ resolved = resolved(**options)
143
+ elif callable(resolved):
144
+ resolved = resolved(**options)
145
+ return resolved if hasattr(resolved, "record") else None
146
+ else:
147
+ resolved = value
148
+
149
+ if isinstance(resolved, type):
150
+ resolved = resolved()
151
+ elif callable(resolved) and not hasattr(resolved, "record"):
152
+ resolved = resolved()
153
+
154
+ if resolved is None or not hasattr(resolved, "record"):
155
+ return None
156
+ return resolved # type: ignore[return-value]
157
+
158
+
159
+ def configure_audit_logger_from_settings(django_settings: Any) -> None:
160
+ """
161
+ Configure the audit logger based on Django settings.
162
+
163
+ Expects either ``settings.GENERAL_MANAGER['AUDIT_LOGGER']`` or a top-level
164
+ ``settings.AUDIT_LOGGER`` value pointing to an audit logger implementation
165
+ (instance, callable, or dotted import path).
166
+ """
167
+ config: Mapping[str, Any] | None = getattr(django_settings, _SETTINGS_KEY, None)
168
+ logger_setting: Any = None
169
+ if isinstance(config, Mapping):
170
+ logger_setting = config.get(_AUDIT_LOGGER_KEY)
171
+ if logger_setting is None:
172
+ logger_setting = getattr(django_settings, _AUDIT_LOGGER_KEY, None)
173
+
174
+ logger_instance = _resolve_logger_reference(logger_setting)
175
+ configure_audit_logger(logger_instance)
176
+
177
+
178
+ _MODEL_CACHE: dict[str, type[models.Model]] = {}
179
+ _MODEL_CACHE_LOCK = Lock()
180
+
181
+
182
+ def _build_field_definitions() -> dict[str, models.Field[Any, Any]]:
183
+ return {
184
+ "created_at": models.DateTimeField(auto_now_add=True),
185
+ "action": models.CharField(max_length=32),
186
+ "attributes": models.JSONField(default=list),
187
+ "granted": models.BooleanField(),
188
+ "bypassed": models.BooleanField(),
189
+ "manager": models.CharField(max_length=255, null=True, blank=True),
190
+ "user_id": models.CharField(max_length=255, null=True, blank=True),
191
+ "user_repr": models.TextField(null=True, blank=True),
192
+ "permissions": models.JSONField(default=list),
193
+ "metadata": models.JSONField(null=True, blank=True),
194
+ }
195
+
196
+
197
+ def _get_audit_model(table_name: str) -> type[models.Model]:
198
+ """Return (and register) a concrete audit model for the given table."""
199
+ with _MODEL_CACHE_LOCK:
200
+ cached = _MODEL_CACHE.get(table_name)
201
+ if cached is not None:
202
+ return cached
203
+
204
+ app_config = apps.get_app_config("general_manager")
205
+ for model in app_config.get_models():
206
+ if model._meta.db_table == table_name:
207
+ _MODEL_CACHE[table_name] = model
208
+ return model
209
+
210
+ attrs: dict[str, Any] = _build_field_definitions()
211
+ attrs["__module__"] = __name__
212
+ attrs["Meta"] = type(
213
+ "Meta",
214
+ (),
215
+ {"db_table": table_name, "app_label": "general_manager"},
216
+ )
217
+ model_name = f"PermissionAuditEntry_{abs(hash(table_name))}"
218
+ model = type(model_name, (models.Model,), attrs)
219
+ registry_key = model.__name__.lower()
220
+ if registry_key not in app_config.models:
221
+ apps.register_model("general_manager", model)
222
+ _MODEL_CACHE[table_name] = model
223
+ return model
224
+
225
+
226
+ class _BufferedAuditLogger:
227
+ """Base class implementing a background worker that processes audit events in batches."""
228
+
229
+ _SENTINEL = object()
230
+
231
+ def __init__(
232
+ self,
233
+ *,
234
+ batch_size: int = 100,
235
+ flush_interval: float = 0.5,
236
+ use_worker: bool = True,
237
+ ) -> None:
238
+ self._batch_size = max(batch_size, 1)
239
+ self._flush_interval = flush_interval
240
+ self._use_worker = use_worker
241
+ self._closed = Event()
242
+ if self._use_worker:
243
+ self._queue: SimpleQueue[PermissionAuditEvent | object] = SimpleQueue()
244
+ self._worker = Thread(target=self._worker_loop, daemon=True)
245
+ self._worker.start()
246
+ atexit.register(self.close)
247
+ else:
248
+ self._queue = None # type: ignore[assignment]
249
+ self._worker = None # type: ignore[assignment]
250
+
251
+ def record(self, event: PermissionAuditEvent) -> None:
252
+ if self._closed.is_set():
253
+ return
254
+ if not self._use_worker:
255
+ self._handle_batch((event,))
256
+ return
257
+ self._queue.put(event)
258
+
259
+ def close(self) -> None:
260
+ if self._closed.is_set() or not self._use_worker:
261
+ return
262
+ self._closed.set()
263
+ self._queue.put(self._SENTINEL)
264
+ self._worker.join(timeout=2.0)
265
+
266
+ def flush(self) -> None:
267
+ """Block until all queued events are processed."""
268
+ if self._use_worker:
269
+ self.close()
270
+
271
+ def _worker_loop(self) -> None:
272
+ pending: list[PermissionAuditEvent] = []
273
+ while True:
274
+ try:
275
+ item = self._queue.get(timeout=self._flush_interval)
276
+ except Empty:
277
+ item = None
278
+ if item is self._SENTINEL:
279
+ break
280
+ if isinstance(item, PermissionAuditEvent):
281
+ pending.append(item)
282
+ if len(pending) >= self._batch_size or (item is None and pending):
283
+ self._handle_batch(pending)
284
+ pending = []
285
+ if pending:
286
+ self._handle_batch(pending)
287
+
288
+ def _handle_batch(self, events: Iterable[PermissionAuditEvent]) -> None:
289
+ raise NotImplementedError
290
+
291
+
292
+ class FileAuditLogger(_BufferedAuditLogger):
293
+ """Persist audit events as newline-delimited JSON records in a file."""
294
+
295
+ def __init__(
296
+ self,
297
+ path: str | Path,
298
+ *,
299
+ batch_size: int = 100,
300
+ flush_interval: float = 0.5,
301
+ ) -> None:
302
+ self._path = Path(path)
303
+ self._path.parent.mkdir(parents=True, exist_ok=True)
304
+ super().__init__(batch_size=batch_size, flush_interval=flush_interval)
305
+
306
+ def _handle_batch(self, events: Iterable[PermissionAuditEvent]) -> None:
307
+ records = [json.dumps(_serialize_event(event)) for event in events]
308
+ if not records:
309
+ return
310
+ data = "\n".join(records) + "\n"
311
+ with self._path.open("a", encoding="utf-8") as handle:
312
+ handle.write(data)
313
+
314
+
315
+ class DatabaseAuditLogger(_BufferedAuditLogger):
316
+ """Store audit events inside a dedicated database table using Django connections."""
317
+
318
+ def __init__(
319
+ self,
320
+ *,
321
+ using: str = "default",
322
+ table_name: str = "general_manager_permissionauditlog",
323
+ batch_size: int = 100,
324
+ flush_interval: float = 0.5,
325
+ ) -> None:
326
+ self._using = using
327
+ self.table_name = table_name
328
+ self.model = _get_audit_model(table_name)
329
+ connection = connections[self._using]
330
+ use_worker = connection.vendor != "sqlite"
331
+ super().__init__(
332
+ batch_size=batch_size,
333
+ flush_interval=flush_interval,
334
+ use_worker=use_worker,
335
+ )
336
+ self._ensure_table()
337
+
338
+ def _ensure_table(self) -> None:
339
+ connection = connections[self._using]
340
+ table_names = connection.introspection.table_names()
341
+ if self.model._meta.db_table in table_names:
342
+ return
343
+ with connection.schema_editor(atomic=False) as editor:
344
+ editor.create_model(self.model)
345
+
346
+ def _handle_batch(self, events: Iterable[PermissionAuditEvent]) -> None:
347
+ entries = []
348
+ for event in events:
349
+ serialized = _serialize_event(event)
350
+ entries.append(
351
+ self.model(
352
+ action=event.action,
353
+ attributes=serialized["attributes"],
354
+ granted=event.granted,
355
+ bypassed=event.bypassed,
356
+ manager=event.manager,
357
+ user_id=serialized["user_id"],
358
+ user_repr=serialized["user"],
359
+ permissions=serialized["permissions"],
360
+ metadata=serialized["metadata"],
361
+ )
362
+ )
363
+ if not entries:
364
+ return
365
+ self.model.objects.using(self._using).bulk_create(
366
+ entries, batch_size=self._batch_size
367
+ )
368
+
369
+
370
+ __all__ = [
371
+ "AuditLogger",
372
+ "DatabaseAuditLogger",
373
+ "FileAuditLogger",
374
+ "PermissionAuditEvent",
375
+ "audit_logging_enabled",
376
+ "configure_audit_logger",
377
+ "configure_audit_logger_from_settings",
378
+ "emit_permission_audit_event",
379
+ "get_audit_logger",
380
+ ]
@@ -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
 
@@ -1,25 +1,26 @@
1
1
  """Registry of reusable permission checks and their queryset filters."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from typing import Any, Callable, TYPE_CHECKING, TypedDict, Literal
4
6
 
5
7
  if TYPE_CHECKING:
6
- from django.contrib.auth.models import AbstractUser, AnonymousUser
8
+ from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
7
9
  from general_manager.permission.permission_data_manager import (
8
10
  PermissionDataManager,
9
11
  )
10
12
  from general_manager.manager.general_manager import GeneralManager
11
13
  from general_manager.manager.meta import GeneralManagerMeta
12
14
 
13
-
14
15
  type permission_filter = Callable[
15
- [AbstractUser | AnonymousUser, list[str]],
16
- dict[Literal["filter", "exclude"], dict[str, str]] | None,
16
+ [AbstractBaseUser | AnonymousUser, list[str]],
17
+ dict[Literal["filter", "exclude"], dict[str, Any]] | None,
17
18
  ]
18
19
 
19
20
  type permission_method = Callable[
20
21
  [
21
22
  PermissionDataManager | GeneralManager | GeneralManagerMeta,
22
- AbstractUser | AnonymousUser,
23
+ AbstractBaseUser | AnonymousUser,
23
24
  list[str],
24
25
  ],
25
26
  bool,
@@ -33,36 +34,205 @@ class PermissionDict(TypedDict):
33
34
  permission_filter: permission_filter
34
35
 
35
36
 
36
- permission_functions: dict[str, PermissionDict] = {
37
- "public": {
38
- "permission_method": lambda _instance, _user, _config: True,
39
- "permission_filter": lambda _user, _config: None,
40
- },
41
- "matches": {
42
- "permission_method": lambda instance, _user, config: getattr(
43
- instance, config[0]
44
- )
45
- == config[1],
46
- "permission_filter": lambda _user, config: {"filter": {config[0]: config[1]}},
47
- },
48
- "ends_with": {
49
- "permission_method": lambda instance, _user, config: getattr(
50
- instance, config[0]
51
- ).endswith(config[1]),
52
- "permission_filter": lambda _user, config: {
53
- "filter": {f"{config[0]}__endswith": config[1]}
54
- },
55
- },
56
- "isAdmin": {
57
- "permission_method": lambda _instance, user, _config: user.is_staff,
58
- "permission_filter": lambda _user, _config: None,
59
- },
60
- "isSelf": {
61
- "permission_method": lambda instance, user, _config: instance.creator == user, # type: ignore
62
- "permission_filter": lambda user, _config: {"filter": {"creator_id": user.id}}, # type: ignore
63
- },
64
- "isAuthenticated": {
65
- "permission_method": lambda _instance, user, _config: user.is_authenticated,
66
- "permission_filter": lambda _user, _config: None,
67
- },
68
- }
37
+ permission_functions: dict[str, PermissionDict] = {}
38
+
39
+ __all__ = ["permission_functions", "register_permission"]
40
+
41
+ _PERMISSION_ALREADY_REGISTERED_MESSAGE = "Permission function is already registered."
42
+
43
+
44
+ def _default_permission_filter(
45
+ _user: AbstractBaseUser | AnonymousUser, _config: list[str]
46
+ ) -> dict[Literal["filter", "exclude"], dict[str, Any]] | None:
47
+ return None
48
+
49
+
50
+ def register_permission(
51
+ name: str, *, permission_filter: permission_filter | None = None
52
+ ) -> Callable[[permission_method], permission_method]:
53
+ """
54
+ Register a permission function in the global registry.
55
+
56
+ Parameters:
57
+ name (str): Identifier used in permission expressions.
58
+ permission_filter (permission_filter | None): Optional callable that
59
+ provides queryset filters corresponding to the permission.
60
+
61
+ Returns:
62
+ Callable[[permission_method], permission_method]: Decorator that
63
+ registers the decorated function and returns it unchanged.
64
+
65
+ Raises:
66
+ ValueError: If another permission with the same name has already been
67
+ registered.
68
+ """
69
+
70
+ def decorator(func: permission_method) -> permission_method:
71
+ if name in permission_functions:
72
+ raise ValueError(_PERMISSION_ALREADY_REGISTERED_MESSAGE)
73
+ filter_callable = permission_filter or _default_permission_filter
74
+ permission_functions[name] = {
75
+ "permission_method": func,
76
+ "permission_filter": filter_callable,
77
+ }
78
+ return func
79
+
80
+ return decorator
81
+
82
+
83
+ @register_permission("public")
84
+ def _permission_public(
85
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
86
+ _user: AbstractBaseUser | AnonymousUser,
87
+ _config: list[str],
88
+ ) -> bool:
89
+ return True
90
+
91
+
92
+ def _matches_permission_filter(
93
+ _user: AbstractBaseUser | AnonymousUser, config: list[str]
94
+ ) -> dict[Literal["filter", "exclude"], dict[str, Any]] | None:
95
+ if len(config) < 2:
96
+ return None
97
+ return {"filter": {config[0]: config[1]}}
98
+
99
+
100
+ @register_permission("matches", permission_filter=_matches_permission_filter)
101
+ def _permission_matches(
102
+ instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
103
+ _user: AbstractBaseUser | AnonymousUser,
104
+ config: list[str],
105
+ ) -> bool:
106
+ return bool(
107
+ len(config) >= 2 and str(getattr(instance, config[0], None)) == config[1]
108
+ )
109
+
110
+
111
+ @register_permission("isAdmin")
112
+ def _permission_is_admin(
113
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
114
+ user: AbstractBaseUser | AnonymousUser,
115
+ _config: list[str],
116
+ ) -> bool:
117
+ return bool(getattr(user, "is_staff", False))
118
+
119
+
120
+ def _is_self_permission_filter(
121
+ user: AbstractBaseUser | AnonymousUser,
122
+ _config: list[str],
123
+ ) -> dict[Literal["filter", "exclude"], dict[str, Any]] | None:
124
+ return {"filter": {"creator_id": getattr(user, "id", None)}}
125
+
126
+
127
+ @register_permission("isSelf", permission_filter=_is_self_permission_filter)
128
+ def _permission_is_self(
129
+ instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
130
+ user: AbstractBaseUser | AnonymousUser,
131
+ _config: list[str],
132
+ ) -> bool:
133
+ return bool(instance.creator == user) # type: ignore[union-attr]
134
+
135
+
136
+ @register_permission("isAuthenticated")
137
+ def _permission_is_authenticated(
138
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
139
+ user: AbstractBaseUser | AnonymousUser,
140
+ _config: list[str],
141
+ ) -> bool:
142
+ return bool(getattr(user, "is_authenticated", False))
143
+
144
+
145
+ @register_permission("isActive")
146
+ def _permission_is_active(
147
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
148
+ user: AbstractBaseUser | AnonymousUser,
149
+ _config: list[str],
150
+ ) -> bool:
151
+ return bool(getattr(user, "is_active", False))
152
+
153
+
154
+ @register_permission("hasPermission")
155
+ def _permission_has_permission(
156
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
157
+ user: AbstractBaseUser | AnonymousUser,
158
+ config: list[str],
159
+ ) -> bool:
160
+ if not config:
161
+ return False
162
+ has_perm = getattr(user, "has_perm", None)
163
+ if not callable(has_perm):
164
+ return False
165
+ return bool(has_perm(config[0]))
166
+
167
+
168
+ @register_permission("inGroup")
169
+ def _permission_in_group(
170
+ _instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
171
+ user: AbstractBaseUser | AnonymousUser,
172
+ config: list[str],
173
+ ) -> bool:
174
+ if not config:
175
+ return False
176
+ group_manager = getattr(user, "groups", None)
177
+ if group_manager is None or not hasattr(group_manager, "filter"):
178
+ return False
179
+ filtered = group_manager.filter(name=config[0]) # type: ignore[attr-defined]
180
+ return bool(hasattr(filtered, "exists") and filtered.exists()) # type: ignore[call-arg]
181
+
182
+
183
+ def _related_user_field_permission_filter(
184
+ user: AbstractBaseUser | AnonymousUser, config: list[str]
185
+ ) -> dict[Literal["filter", "exclude"], dict[str, Any]] | None:
186
+ if not config:
187
+ return None
188
+ user_id = getattr(user, "id", None)
189
+ if user_id is None:
190
+ return None
191
+ return {"filter": {f"{config[0]}_id": user_id}}
192
+
193
+
194
+ @register_permission(
195
+ "relatedUserField",
196
+ permission_filter=_related_user_field_permission_filter,
197
+ )
198
+ def _permission_related_user_field(
199
+ instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
200
+ user: AbstractBaseUser | AnonymousUser,
201
+ config: list[str],
202
+ ) -> bool:
203
+ if not config:
204
+ return False
205
+ related_object = getattr(instance, config[0], None)
206
+ return bool(related_object == user) # type: ignore[arg-type]
207
+
208
+
209
+ def _many_to_many_contains_user_permission_filter(
210
+ user: AbstractBaseUser | AnonymousUser, config: list[str]
211
+ ) -> dict[Literal["filter", "exclude"], dict[str, Any]] | None:
212
+ if not config:
213
+ return None
214
+ user_id = getattr(user, "id", None)
215
+ if user_id is None:
216
+ return None
217
+ return {"filter": {f"{config[0]}__id": user_id}}
218
+
219
+
220
+ @register_permission(
221
+ "manyToManyContainsUser",
222
+ permission_filter=_many_to_many_contains_user_permission_filter,
223
+ )
224
+ def _permission_many_to_many_contains_user(
225
+ instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
226
+ user: AbstractBaseUser | AnonymousUser,
227
+ config: list[str],
228
+ ) -> bool:
229
+ if not config:
230
+ return False
231
+ related_manager = getattr(instance, config[0], None)
232
+ if related_manager is None or not hasattr(related_manager, "filter"):
233
+ return False
234
+ user_pk = getattr(user, "pk", None)
235
+ if user_pk is None:
236
+ return False
237
+ filtered = related_manager.filter(pk=user_pk) # type: ignore[attr-defined]
238
+ return bool(hasattr(filtered, "exists") and filtered.exists()) # type: ignore[call-arg]
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from typing import Callable, Optional, TypeVar, Generic, cast
5
- from django.contrib.auth.models import AbstractUser
6
5
 
7
6
  from general_manager.manager.general_manager import GeneralManager
8
7
 
@@ -4,7 +4,7 @@ from general_manager.permission.permission_checks import (
4
4
  permission_functions,
5
5
  )
6
6
  from general_manager.permission.permission_data_manager import PermissionDataManager
7
- from django.contrib.auth.models import AbstractUser, AnonymousUser
7
+ from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
8
8
 
9
9
  from general_manager.manager.general_manager import GeneralManager
10
10
  from general_manager.manager.meta import GeneralManagerMeta
@@ -26,7 +26,7 @@ class PermissionNotFoundError(ValueError):
26
26
  def validate_permission_string(
27
27
  permission: str,
28
28
  data: PermissionDataManager | GeneralManager | GeneralManagerMeta,
29
- request_user: AbstractUser | AnonymousUser,
29
+ request_user: AbstractBaseUser | AnonymousUser,
30
30
  ) -> bool:
31
31
  """
32
32
  Evaluate a compound permission expression joined by '&' operators.
@@ -34,7 +34,7 @@ def validate_permission_string(
34
34
  Parameters:
35
35
  permission (str): Permission expression where sub-permissions are joined with '&'. Individual sub-permissions may include ':'-separated configuration parts (for example, "isAuthenticated&admin:level").
36
36
  data (PermissionDataManager | GeneralManager | GeneralManagerMeta): Object passed to each permission function.
37
- request_user (AbstractUser | AnonymousUser): User for whom permissions are evaluated.
37
+ request_user (AbstractBaseUser | AnonymousUser): User for whom permissions are evaluated.
38
38
 
39
39
  Returns:
40
40
  `true` if every sub-permission evaluates to True, `false` otherwise.
@@ -35,6 +35,14 @@ GENERAL_MANAGER_EXPORTS: LazyExportMap = {
35
35
  "general_manager.permission.manager_based_permission",
36
36
  "ManagerBasedPermission",
37
37
  ),
38
+ "register_permission": (
39
+ "general_manager.permission.permission_checks",
40
+ "register_permission",
41
+ ),
42
+ "permission_functions": (
43
+ "general_manager.permission.permission_checks",
44
+ "permission_functions",
45
+ ),
38
46
  "Rule": ("general_manager.rule.rule", "Rule"),
39
47
  }
40
48
 
@@ -124,6 +132,35 @@ PERMISSION_EXPORTS: LazyExportMap = {
124
132
  "general_manager.permission.mutation_permission",
125
133
  "MutationPermission",
126
134
  ),
135
+ "register_permission": (
136
+ "general_manager.permission.permission_checks",
137
+ "register_permission",
138
+ ),
139
+ "permission_functions": (
140
+ "general_manager.permission.permission_checks",
141
+ "permission_functions",
142
+ ),
143
+ "configure_audit_logger": (
144
+ "general_manager.permission.audit",
145
+ "configure_audit_logger",
146
+ ),
147
+ "configure_audit_logger_from_settings": (
148
+ "general_manager.permission.audit",
149
+ "configure_audit_logger_from_settings",
150
+ ),
151
+ "FileAuditLogger": (
152
+ "general_manager.permission.audit",
153
+ "FileAuditLogger",
154
+ ),
155
+ "DatabaseAuditLogger": (
156
+ "general_manager.permission.audit",
157
+ "DatabaseAuditLogger",
158
+ ),
159
+ "PermissionAuditEvent": (
160
+ "general_manager.permission.audit",
161
+ "PermissionAuditEvent",
162
+ ),
163
+ "AuditLogger": ("general_manager.permission.audit", "AuditLogger"),
127
164
  }
128
165
 
129
166
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.20.1
3
+ Version: 0.21.0
4
4
  Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: MIT License
@@ -1,18 +1,18 @@
1
1
  general_manager/__init__.py,sha256=Xy4_fFjChIYtlBPDv7K3JEHOulq9_IFXkCc4CElS32Q,1035
2
- general_manager/apps.py,sha256=bz_A2IfgVba2b8lZC5aeQqe40IH7GaavpTEg828Ltm4,27343
2
+ general_manager/apps.py,sha256=1wzMof8IKn_Zw4QyD3WwD7zVedAvsZKhCwsa0ajx6U8,27480
3
3
  general_manager/logging.py,sha256=bL2dEdNnxo74qRmhOCG06lbPtVCOozfF3GS133T9HH8,4250
4
- general_manager/public_api_registry.py,sha256=mDpKIE3fmFAMTbQnQdLAZGc_CKNNA6p22Jrql9R6Y4w,7817
4
+ general_manager/public_api_registry.py,sha256=zAYG6UPfbv5NDSM59kvLxUZBJce5NF8lzEB4op8LnPM,8974
5
5
  general_manager/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  general_manager/_types/__init__.py,sha256=RmS0Ok-1-CwWOZL_socLWksEk9MfsshL9eQx879I4mU,60
7
7
  general_manager/_types/api.py,sha256=i5fi7O3C3frI74oqPT-tAoztDplgNDOShyvPNxhw7CY,496
8
8
  general_manager/_types/bucket.py,sha256=DZ3GC-1iR4RiNBKydsMhp_R4GTaYMfsonwe7MCADVP4,435
9
9
  general_manager/_types/cache.py,sha256=jMbIEXDq1pUvVxTN8hvD8rSH7QRjE3ewrMx2gEhYP3w,660
10
10
  general_manager/_types/factory.py,sha256=cPG_td05KCWv2j6_h_vYCKETPol3k_nXkEMWCcUdNHM,1726
11
- general_manager/_types/general_manager.py,sha256=Izs47-pOnYJqPmMF7m5DRodSISEztXKSxtBGIHgN8sA,1031
11
+ general_manager/_types/general_manager.py,sha256=m-VA1968DYEFiunWyA72fUI1EicMLKarYJ5bkSyKJ7I,1241
12
12
  general_manager/_types/interface.py,sha256=JlPn3G3ompn1PRfFQfsrIJv2Pf0I3n1RL2ki5_fDX54,605
13
13
  general_manager/_types/manager.py,sha256=rXM6NrQaPdfE9CQoRV_jhbI3SK23rvdpvyBpybVgWi8,506
14
14
  general_manager/_types/measurement.py,sha256=Unkpf3H7TGaUKfkjmFDR7QWBU4lPp5I2A4GlK8pps-Y,443
15
- general_manager/_types/permission.py,sha256=HJBEOIDYbkYSZkEbT4_6qRDyUk2Ek153K_RchmeYd5I,416
15
+ general_manager/_types/permission.py,sha256=olbtZNPkYoI1mbtzs6FDuVlKyGiP6VfGwufe8G56CQQ,1196
16
16
  general_manager/_types/rule.py,sha256=aNbx3jlVinAYQGJlmFwW8rOwTjf19F6khq5xbGTdD3U,238
17
17
  general_manager/_types/utils.py,sha256=8VyD6EKkcRIzdVs6FANP5Ri71eT4BT4c3dTUeyjDr40,1043
18
18
  general_manager/api/__init__.py,sha256=ChV1UPCUjW1Lci0BajmXVtE-3C-WLQTvjO058BMu1Bs,1020
@@ -51,13 +51,13 @@ general_manager/measurement/__init__.py,sha256=38-6kTERbw7EeOQAw3eZ9DDEu6hwwqkGJ
51
51
  general_manager/measurement/measurement.py,sha256=ArEnntLu3ffHID71uFbXwiB5e5tpsBT2_TbBPJfCWJI,26070
52
52
  general_manager/measurement/measurement_field.py,sha256=zQbyLZT7E9Lx4f3u_wQ-rEoLPqH0FWhfWyOd1BaW--M,13950
53
53
  general_manager/permission/__init__.py,sha256=s4Ka11uYBrymM5O-n5kIxOjwUprZ394jTuzGf5prO_8,981
54
- general_manager/permission/base_permission.py,sha256=IKi5n8t1sKUP_F2eZnzTC8lg7HI8OVo5p1jxCBpyCgw,11961
55
- general_manager/permission/file_based_permission.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- general_manager/permission/manager_based_permission.py,sha256=OdDGKwutkHTT13bhSeGGycd3xr7wGhuRXsYym3Z9j2A,10613
57
- general_manager/permission/mutation_permission.py,sha256=5JV6pluIFPGzJPQ_pr-44zwFg_TgzNwDTHcdpwHettU,4606
58
- general_manager/permission/permission_checks.py,sha256=4OVQz0QVnmHN-KPge4pz2Zm_oMxo2PUytW7JoDoHjL0,2297
59
- general_manager/permission/permission_data_manager.py,sha256=vNWbPrcw1NUZ-oTRjc3PME479ubbOqxlKx020GY5wzQ,3905
60
- general_manager/permission/utils.py,sha256=yojrcF1kD6KjKugrGyiaf8XOyS_tUA4yB5_RideKJeg,2905
54
+ general_manager/permission/audit.py,sha256=mOT4Dj5LnAMbHWXKf6JCM01L-jd7GsMSmAIBTR1hTS0,12802
55
+ general_manager/permission/base_permission.py,sha256=69mgDS7iuxEA1V8lDanDBAH20BY3yc4vp8E0sjrqzmE,16045
56
+ general_manager/permission/manager_based_permission.py,sha256=SfqEvC3RbXjWxxm6JOgEJI9li_6uiB4-avihhgfjynw,11902
57
+ general_manager/permission/mutation_permission.py,sha256=QG-cH1T8anIS4DYRdUHwHOilpop9UzkwMUTiak-sG8s,6871
58
+ general_manager/permission/permission_checks.py,sha256=S0agZzG-tdZWLKWAF_1jdrvsLVbhVk1k1MrBdqNYc8o,7710
59
+ general_manager/permission/permission_data_manager.py,sha256=aqmj5rpslOgCT0GdXnsBtjpFoRerFOa3xG84Jus17b8,3853
60
+ general_manager/permission/utils.py,sha256=Q3E9aq3jUOhBXOdLwWSQsXgNMuV6Bp7R84xM_JLB6Y0,2917
61
61
  general_manager/rule/__init__.py,sha256=dFQLJNr0qpkpuzuBbMY-74jz_Y033YyB9X_OfeJYhSs,1026
62
62
  general_manager/rule/handler.py,sha256=qSBXmXvl7fRgjxUo4wyUOzdZm14a8Umd2Jec4zPXJQc,16309
63
63
  general_manager/rule/rule.py,sha256=41VXFTSWkBEOj8045vZTezm38VEGimjrk22J4YRl144,22691
@@ -71,8 +71,8 @@ general_manager/utils/none_to_zero.py,sha256=Z2KVXBKCz02V2ZBs-7tIs_32n7YDxiXyood
71
71
  general_manager/utils/path_mapping.py,sha256=f3sd3OFh9aJgyF-ayqr22mHnCo-uoIXqio5DRQmziGM,9931
72
72
  general_manager/utils/public_api.py,sha256=bas4SKchJUg2dI2SLRQwkF_CoXJKKwkzQx_JOjMJ2VY,3272
73
73
  general_manager/utils/testing.py,sha256=_V4Y5SXvu-ocoq-AntPFPd140igclUJUT2xVXxhRrSs,14954
74
- generalmanager-0.20.1.dist-info/licenses/LICENSE,sha256=OcRwUgM4iiESN1oNELgyuwuah39XX-EPZiPDDHndNVI,1070
75
- generalmanager-0.20.1.dist-info/METADATA,sha256=aZ08I7nii3gn6zicyZnhavmaZlbQHZIlb8PEaYuT5is,8266
76
- generalmanager-0.20.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- generalmanager-0.20.1.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
78
- generalmanager-0.20.1.dist-info/RECORD,,
74
+ generalmanager-0.21.0.dist-info/licenses/LICENSE,sha256=OcRwUgM4iiESN1oNELgyuwuah39XX-EPZiPDDHndNVI,1070
75
+ generalmanager-0.21.0.dist-info/METADATA,sha256=ubnYLTNd5AzlLYju1ykPewMtGIMF-e7lVKl4Hqo2Os8,8266
76
+ generalmanager-0.21.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
+ generalmanager-0.21.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
78
+ generalmanager-0.21.0.dist-info/RECORD,,
File without changes