GeneralManager 0.20.1__tar.gz → 0.21.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- {generalmanager-0.20.1/src/GeneralManager.egg-info → generalmanager-0.21.0}/PKG-INFO +1 -1
- {generalmanager-0.20.1 → generalmanager-0.21.0}/pyproject.toml +1 -1
- {generalmanager-0.20.1 → generalmanager-0.21.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/GeneralManager.egg-info/SOURCES.txt +1 -1
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/general_manager.py +4 -0
- generalmanager-0.21.0/src/general_manager/_types/permission.py +29 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/apps.py +2 -0
- generalmanager-0.21.0/src/general_manager/permission/audit.py +380 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/base_permission.py +115 -19
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/manager_based_permission.py +37 -4
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/mutation_permission.py +68 -13
- generalmanager-0.21.0/src/general_manager/permission/permission_checks.py +238 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/permission_data_manager.py +0 -1
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/utils.py +3 -3
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/public_api_registry.py +37 -0
- generalmanager-0.20.1/src/general_manager/_types/permission.py +0 -13
- generalmanager-0.20.1/src/general_manager/permission/file_based_permission.py +0 -0
- generalmanager-0.20.1/src/general_manager/permission/permission_checks.py +0 -68
- {generalmanager-0.20.1 → generalmanager-0.21.0}/LICENSE +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/README.md +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/setup.cfg +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/api.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/bucket.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/cache.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/factory.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/manager.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/measurement.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/rule.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/utils.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/api/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/bucket/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/bucket/base_bucket.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/bucket/database_bucket.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/bucket/group_bucket.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/cache_decorator.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/cache_tracker.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/dependency_index.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/factory/auto_factory.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/factory/factory_methods.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/base_interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/calculation_interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/database_based_interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/database_interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/models.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/interface/read_only_interface.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/logging.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/manager/general_manager.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/manager/group_manager.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/measurement/measurement_field.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/py.typed +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/__init__.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/filter_parser.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/format_string.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/json_encoder.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/make_cache_key.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/none_to_zero.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/path_mapping.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/public_api.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/utils/testing.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/tests/test_settings.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/tests/test_urls.py +0 -0
- {generalmanager-0.20.1 → generalmanager-0.21.0}/tests/testing_asgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GeneralManager
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "GeneralManager"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.21.0"
|
|
8
8
|
description = "Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Tim Kleindick", email = "tkleindick@yahoo.de" }]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GeneralManager
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -59,8 +59,8 @@ src/general_manager/measurement/__init__.py
|
|
|
59
59
|
src/general_manager/measurement/measurement.py
|
|
60
60
|
src/general_manager/measurement/measurement_field.py
|
|
61
61
|
src/general_manager/permission/__init__.py
|
|
62
|
+
src/general_manager/permission/audit.py
|
|
62
63
|
src/general_manager/permission/base_permission.py
|
|
63
|
-
src/general_manager/permission/file_based_permission.py
|
|
64
64
|
src/general_manager/permission/manager_based_permission.py
|
|
65
65
|
src/general_manager/permission/mutation_permission.py
|
|
66
66
|
src/general_manager/permission/permission_checks.py
|
{generalmanager-0.20.1 → generalmanager-0.21.0}/src/general_manager/_types/general_manager.py
RENAMED
|
@@ -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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Type-only imports for public API re-exports."""
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AuditLogger",
|
|
7
|
+
"BasePermission",
|
|
8
|
+
"DatabaseAuditLogger",
|
|
9
|
+
"FileAuditLogger",
|
|
10
|
+
"ManagerBasedPermission",
|
|
11
|
+
"MutationPermission",
|
|
12
|
+
"PermissionAuditEvent",
|
|
13
|
+
"configure_audit_logger",
|
|
14
|
+
"configure_audit_logger_from_settings",
|
|
15
|
+
"permission_functions",
|
|
16
|
+
"register_permission",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
from general_manager.permission.audit import AuditLogger
|
|
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
|
|
23
|
+
from general_manager.permission.manager_based_permission import ManagerBasedPermission
|
|
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
|
|
@@ -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
|
+
]
|