fastapi-audit-recorder 0.1.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.
- fastapi_audit_recorder-0.1.0/.gitignore +6 -0
- fastapi_audit_recorder-0.1.0/.gitlab-ci.yml +58 -0
- fastapi_audit_recorder-0.1.0/LICENSE +21 -0
- fastapi_audit_recorder-0.1.0/MANIFEST.in +5 -0
- fastapi_audit_recorder-0.1.0/PKG-INFO +248 -0
- fastapi_audit_recorder-0.1.0/README.md +208 -0
- fastapi_audit_recorder-0.1.0/dev_server.py +123 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/__init__.py +17 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/audit_decorator.py +207 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/context.py +59 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/models.py +57 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/resolver.py +32 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/result.py +25 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/service.py +113 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder/transactional_decorator.py +37 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder.egg-info/PKG-INFO +248 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder.egg-info/SOURCES.txt +27 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder.egg-info/dependency_links.txt +1 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder.egg-info/requires.txt +8 -0
- fastapi_audit_recorder-0.1.0/fastapi_audit_recorder.egg-info/top_level.txt +1 -0
- fastapi_audit_recorder-0.1.0/pyproject.toml +60 -0
- fastapi_audit_recorder-0.1.0/setup.cfg +4 -0
- fastapi_audit_recorder-0.1.0/tests/__init__.py +0 -0
- fastapi_audit_recorder-0.1.0/tests/test_audit_decorator.py +257 -0
- fastapi_audit_recorder-0.1.0/tests/test_context.py +51 -0
- fastapi_audit_recorder-0.1.0/tests/test_resolver.py +78 -0
- fastapi_audit_recorder-0.1.0/tests/test_result.py +43 -0
- fastapi_audit_recorder-0.1.0/tests/test_service.py +209 -0
- fastapi_audit_recorder-0.1.0/tests/test_transactional_decorator.py +89 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- lint
|
|
3
|
+
- test
|
|
4
|
+
- build
|
|
5
|
+
- publish
|
|
6
|
+
|
|
7
|
+
variables:
|
|
8
|
+
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
|
9
|
+
|
|
10
|
+
default:
|
|
11
|
+
cache:
|
|
12
|
+
paths:
|
|
13
|
+
- .cache/pip
|
|
14
|
+
|
|
15
|
+
ruff:
|
|
16
|
+
stage: lint
|
|
17
|
+
image: python:3.11
|
|
18
|
+
before_script:
|
|
19
|
+
- pip install -e .[dev]
|
|
20
|
+
script:
|
|
21
|
+
- ruff check .
|
|
22
|
+
allow_failure: true
|
|
23
|
+
|
|
24
|
+
pytest:
|
|
25
|
+
stage: test
|
|
26
|
+
image: python:3.11
|
|
27
|
+
script:
|
|
28
|
+
- pip install -e .[dev]
|
|
29
|
+
- pytest
|
|
30
|
+
|
|
31
|
+
build:
|
|
32
|
+
stage: build
|
|
33
|
+
image: python:3.11
|
|
34
|
+
before_script:
|
|
35
|
+
- pip install build
|
|
36
|
+
script:
|
|
37
|
+
- export SETUPTOOLS_SCM_PRETEND_VERSION=${CI_COMMIT_TAG#v}
|
|
38
|
+
- python -m build
|
|
39
|
+
artifacts:
|
|
40
|
+
paths:
|
|
41
|
+
- dist/
|
|
42
|
+
rules:
|
|
43
|
+
- if: '$CI_COMMIT_TAG'
|
|
44
|
+
|
|
45
|
+
publish:
|
|
46
|
+
stage: publish
|
|
47
|
+
image: python:3.11
|
|
48
|
+
before_script:
|
|
49
|
+
- pip install twine
|
|
50
|
+
script:
|
|
51
|
+
- export TWINE_USERNAME="__token__"
|
|
52
|
+
- export TWINE_PASSWORD="$PYPI_TOKEN"
|
|
53
|
+
- twine upload dist/* --verbose
|
|
54
|
+
dependencies:
|
|
55
|
+
- build
|
|
56
|
+
rules:
|
|
57
|
+
- if: '$CI_COMMIT_TAG'
|
|
58
|
+
when: manual
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fastapi-audit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-audit-recorder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic audit library for FastAPI applications
|
|
5
|
+
Author: fastapi-audit-recorder contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 fastapi-audit contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: fastapi,audit,sqlalchemy,logging,trail
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
33
|
+
Requires-Dist: pydantic>=2.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: ruff; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-anyio; extra == "dev"
|
|
38
|
+
Requires-Dist: anyio[trio]; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Cette librairie permet d'auditer les actions sur un projet et mets également à disposition une UI intégrée.
|
|
43
|
+
|
|
44
|
+
# Structure complète du projet
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
project-audit/
|
|
48
|
+
├── fastapi_audit_recorder/
|
|
49
|
+
│ ├── __init__.py
|
|
50
|
+
│ ├── context.py
|
|
51
|
+
│ ├── audit_decorator.py
|
|
52
|
+
│ ├── transactional_decorator.py
|
|
53
|
+
│ ├── service.py
|
|
54
|
+
│ ├── models.py
|
|
55
|
+
│ ├── resolver.py
|
|
56
|
+
│ └── result.py
|
|
57
|
+
├── tests/
|
|
58
|
+
├── LICENSE
|
|
59
|
+
├── README.md
|
|
60
|
+
└── pyproject.toml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
# Formatting with ruff
|
|
64
|
+
|
|
65
|
+
ruff check . --fix
|
|
66
|
+
ruff format .
|
|
67
|
+
|
|
68
|
+
# Tests
|
|
69
|
+
|
|
70
|
+
pytest
|
|
71
|
+
|
|
72
|
+
# Builder la lib
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m pip install --upgrade build
|
|
76
|
+
python -m build
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Le répertoire dist contiendra un fichier .whl et un fichier .tar.gz.
|
|
80
|
+
|
|
81
|
+
# Installer la distribution sur un projet (en local)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install /path/to/project-audit/dist/fastapi_audit_recorder-0.1.0-py3-none-any.whl
|
|
85
|
+
pip show fastapi-audit-recorder
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
# Installer la distribution sur un projet (en mode dev)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install -e /home/you/dev/project-audit
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
# Executer les tests unitaires
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pytest -v
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Intégration de la base de données (sqlalchemy et alembic)
|
|
101
|
+
|
|
102
|
+
Importer et ajouter la metadata dans le fichier de configuration :
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from fastapi_audit_recorder import models as fastapi_audit_models
|
|
106
|
+
|
|
107
|
+
target_metadata = [app.models.model.Base.metadata, fastapi_audit_models.Base.metadata]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Générer et executer le script de mise à jour, sous Alembic :
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
alembic revision --autogenerate -m "create_audit_logs_from_fastapi_audit_recorder"
|
|
114
|
+
alembic upgrade head
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Intégration du service
|
|
118
|
+
|
|
119
|
+
### Configuration
|
|
120
|
+
|
|
121
|
+
Créer un fichier audit_setup.py.
|
|
122
|
+
Celui-ci condiendra les requêtes qui seront executées avant et après chaque action, afin d'historiser les données et les changements.
|
|
123
|
+
Les actions et les types de resources sont à la charge de chaque projet. TODO : héritage
|
|
124
|
+
|
|
125
|
+
Créer autant de query et de register que vous avez de type de resource. Exemple :
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from fastapi_audit_recorder.resolver import resolver
|
|
129
|
+
from sqlalchemy.orm import Session
|
|
130
|
+
from app.models.model import Compte
|
|
131
|
+
|
|
132
|
+
class AuditResourceEnum(enum.Enum):
|
|
133
|
+
COMPTE = "COMPTE"
|
|
134
|
+
|
|
135
|
+
class AuditActionEnum(enum.Enum):
|
|
136
|
+
APPROVE = "APPROVE"
|
|
137
|
+
REFUSE = "REFUSE"
|
|
138
|
+
|
|
139
|
+
def get_compte(session: Session, id: int) -> Compte | None:
|
|
140
|
+
return session.query(Compte).get(id)
|
|
141
|
+
|
|
142
|
+
def audit_setup():
|
|
143
|
+
resolver.register(AuditResourceEnum.COMPTE.name, get_compte, Compte.id)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
La méthode `register` accepte trois paramètres :
|
|
147
|
+
- `resource_type` : le nom de la ressource
|
|
148
|
+
- `loader` : la fonction de chargement de la ressource, signature `(session: Session, resource_id: Any) -> Any`
|
|
149
|
+
- `identifier` : le champ SQLAlchemy utilisé comme identifiant de la ressource (ex. `Compte.id`, `Compte.uuid`)
|
|
150
|
+
|
|
151
|
+
L'`identifier` sert à extraire automatiquement l'id depuis l'objet retourné par la fonction décorée lorsque `id_param` n'est pas renseigné, notamment dans le cas d'une création.
|
|
152
|
+
|
|
153
|
+
Appeler la méthode audit_setup() dans le context manager :
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from app.audit_setup import audit_setup
|
|
157
|
+
|
|
158
|
+
@asynccontextmanager
|
|
159
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
160
|
+
...
|
|
161
|
+
audit_setup()
|
|
162
|
+
yield
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Décorateur @audit
|
|
166
|
+
|
|
167
|
+
La lib mets à disposition un décorateur afin d'auditer une action.
|
|
168
|
+
Exemple d'utilisation :
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from fastapi_audit_recorder.decorators.audit_decorator import audit
|
|
172
|
+
|
|
173
|
+
@audit(action="UPDATE", resource_type="COMPTE", id_param="compteDTO.id")
|
|
174
|
+
async def update_compte(compteDTO: CompteDTO, id_token: KeycloakIDToken, session: Session) -> CompteDTO:
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Si `id_param` n'est pas renseigné, l'identifiant sera extrait automatiquement depuis l'objet retourné par la fonction,
|
|
178
|
+
en utilisant l'`identifier` enregistré dans le resolver (via `resolver.register`). Exemple :
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
@audit(action="CREATE", resource_type="COMPTE") # pas d'id_param
|
|
182
|
+
async def create_compte(compteDTO: CompteDTO, id_token: KeycloakIDToken, session: Session) -> Compte:
|
|
183
|
+
...
|
|
184
|
+
return db_compte # db_compte.id sera extrait via Compte.id déclaré dans le resolver
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Important:
|
|
188
|
+
- **Tous les paramètres du décorateur `@audit` doivent être passés en nommés** (`action=`, `resource_type=`, `id_param=`)
|
|
189
|
+
- **Tous les arguments de la fonction décorée doivent également être passés en nommés** lors de l'appel
|
|
190
|
+
- `id_token: KeycloakIDToken` est obligatoire afin de pouvoir récupérer les informations sur l'utilisateur
|
|
191
|
+
- `session: Session` est obligatoire afin que les logs d'audit soient ajoutées dans la même session
|
|
192
|
+
|
|
193
|
+
### Gestion de la transaction - décorateur @transactional
|
|
194
|
+
|
|
195
|
+
Le décorateur @audit ne réalise aucun commit, c'est action à la charge de la méthode appelante.
|
|
196
|
+
Un décorateur @transactional est mis à disposition afin d'automatiser les commits, exemple :
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from fastapi_audit_recorder.decorators.audit_decorator import audit
|
|
200
|
+
from fastapi_audit_recorder.decorators.transactional_decorator import transactional
|
|
201
|
+
|
|
202
|
+
@transactional
|
|
203
|
+
@audit(action=AuditActionEnum.CREATE.name, resource_type=AuditResourceEnum.COMPTE.name, id_param="db_compte.id")
|
|
204
|
+
def approve_demande_creation(...)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A noter: l'ordre est important, le l'annotation @transactional doit précéder l'annotation @audit pour la persitance des données.
|
|
208
|
+
|
|
209
|
+
### Contexte
|
|
210
|
+
|
|
211
|
+
Dans certain cas, on souhaite auditer une action en fonction de l'appelant.
|
|
212
|
+
Par exemple, auditer une approbation manuelle mais ne pas auditer une autoapprobation.
|
|
213
|
+
|
|
214
|
+
Pour cela il est possible de définir un contexte afin de désactiver un ou plusieurs audits, exemple :
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from fastapi_audit_recorder.context import skip_audit
|
|
218
|
+
|
|
219
|
+
with skip_audit(AuditActionEnum.APPROVE, AuditActionEnum.REJECT)
|
|
220
|
+
with skip_audit():
|
|
221
|
+
service.approve(...)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Ignorer l'audit
|
|
225
|
+
|
|
226
|
+
L'audit est automatiquement réalisée après l'execution de la méthode annotée, sauf si celle-ci lance une exception.
|
|
227
|
+
Il est possible d'ignorer explicitement l'audit. Exemple :
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fastapi_audit_recorder.result import AuditResult
|
|
231
|
+
|
|
232
|
+
def approve(...) {
|
|
233
|
+
if is_already_approve:
|
|
234
|
+
return AuditResult.skip(db_compte)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Services
|
|
240
|
+
|
|
241
|
+
La lib mets à disposition des services afin de récupérer des données filtrées et paginées :
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from fastapi_audit_recorder import service
|
|
245
|
+
|
|
246
|
+
service.query_logs(...)
|
|
247
|
+
service.query_log_by_id(...)
|
|
248
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
|
|
2
|
+
Cette librairie permet d'auditer les actions sur un projet et mets également à disposition une UI intégrée.
|
|
3
|
+
|
|
4
|
+
# Structure complète du projet
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
project-audit/
|
|
8
|
+
├── fastapi_audit_recorder/
|
|
9
|
+
│ ├── __init__.py
|
|
10
|
+
│ ├── context.py
|
|
11
|
+
│ ├── audit_decorator.py
|
|
12
|
+
│ ├── transactional_decorator.py
|
|
13
|
+
│ ├── service.py
|
|
14
|
+
│ ├── models.py
|
|
15
|
+
│ ├── resolver.py
|
|
16
|
+
│ └── result.py
|
|
17
|
+
├── tests/
|
|
18
|
+
├── LICENSE
|
|
19
|
+
├── README.md
|
|
20
|
+
└── pyproject.toml
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
# Formatting with ruff
|
|
24
|
+
|
|
25
|
+
ruff check . --fix
|
|
26
|
+
ruff format .
|
|
27
|
+
|
|
28
|
+
# Tests
|
|
29
|
+
|
|
30
|
+
pytest
|
|
31
|
+
|
|
32
|
+
# Builder la lib
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install --upgrade build
|
|
36
|
+
python -m build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Le répertoire dist contiendra un fichier .whl et un fichier .tar.gz.
|
|
40
|
+
|
|
41
|
+
# Installer la distribution sur un projet (en local)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install /path/to/project-audit/dist/fastapi_audit_recorder-0.1.0-py3-none-any.whl
|
|
45
|
+
pip show fastapi-audit-recorder
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
# Installer la distribution sur un projet (en mode dev)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e /home/you/dev/project-audit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
# Executer les tests unitaires
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pytest -v
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Intégration de la base de données (sqlalchemy et alembic)
|
|
61
|
+
|
|
62
|
+
Importer et ajouter la metadata dans le fichier de configuration :
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from fastapi_audit_recorder import models as fastapi_audit_models
|
|
66
|
+
|
|
67
|
+
target_metadata = [app.models.model.Base.metadata, fastapi_audit_models.Base.metadata]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Générer et executer le script de mise à jour, sous Alembic :
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
alembic revision --autogenerate -m "create_audit_logs_from_fastapi_audit_recorder"
|
|
74
|
+
alembic upgrade head
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Intégration du service
|
|
78
|
+
|
|
79
|
+
### Configuration
|
|
80
|
+
|
|
81
|
+
Créer un fichier audit_setup.py.
|
|
82
|
+
Celui-ci condiendra les requêtes qui seront executées avant et après chaque action, afin d'historiser les données et les changements.
|
|
83
|
+
Les actions et les types de resources sont à la charge de chaque projet. TODO : héritage
|
|
84
|
+
|
|
85
|
+
Créer autant de query et de register que vous avez de type de resource. Exemple :
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from fastapi_audit_recorder.resolver import resolver
|
|
89
|
+
from sqlalchemy.orm import Session
|
|
90
|
+
from app.models.model import Compte
|
|
91
|
+
|
|
92
|
+
class AuditResourceEnum(enum.Enum):
|
|
93
|
+
COMPTE = "COMPTE"
|
|
94
|
+
|
|
95
|
+
class AuditActionEnum(enum.Enum):
|
|
96
|
+
APPROVE = "APPROVE"
|
|
97
|
+
REFUSE = "REFUSE"
|
|
98
|
+
|
|
99
|
+
def get_compte(session: Session, id: int) -> Compte | None:
|
|
100
|
+
return session.query(Compte).get(id)
|
|
101
|
+
|
|
102
|
+
def audit_setup():
|
|
103
|
+
resolver.register(AuditResourceEnum.COMPTE.name, get_compte, Compte.id)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
La méthode `register` accepte trois paramètres :
|
|
107
|
+
- `resource_type` : le nom de la ressource
|
|
108
|
+
- `loader` : la fonction de chargement de la ressource, signature `(session: Session, resource_id: Any) -> Any`
|
|
109
|
+
- `identifier` : le champ SQLAlchemy utilisé comme identifiant de la ressource (ex. `Compte.id`, `Compte.uuid`)
|
|
110
|
+
|
|
111
|
+
L'`identifier` sert à extraire automatiquement l'id depuis l'objet retourné par la fonction décorée lorsque `id_param` n'est pas renseigné, notamment dans le cas d'une création.
|
|
112
|
+
|
|
113
|
+
Appeler la méthode audit_setup() dans le context manager :
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from app.audit_setup import audit_setup
|
|
117
|
+
|
|
118
|
+
@asynccontextmanager
|
|
119
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
120
|
+
...
|
|
121
|
+
audit_setup()
|
|
122
|
+
yield
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Décorateur @audit
|
|
126
|
+
|
|
127
|
+
La lib mets à disposition un décorateur afin d'auditer une action.
|
|
128
|
+
Exemple d'utilisation :
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from fastapi_audit_recorder.decorators.audit_decorator import audit
|
|
132
|
+
|
|
133
|
+
@audit(action="UPDATE", resource_type="COMPTE", id_param="compteDTO.id")
|
|
134
|
+
async def update_compte(compteDTO: CompteDTO, id_token: KeycloakIDToken, session: Session) -> CompteDTO:
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Si `id_param` n'est pas renseigné, l'identifiant sera extrait automatiquement depuis l'objet retourné par la fonction,
|
|
138
|
+
en utilisant l'`identifier` enregistré dans le resolver (via `resolver.register`). Exemple :
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
@audit(action="CREATE", resource_type="COMPTE") # pas d'id_param
|
|
142
|
+
async def create_compte(compteDTO: CompteDTO, id_token: KeycloakIDToken, session: Session) -> Compte:
|
|
143
|
+
...
|
|
144
|
+
return db_compte # db_compte.id sera extrait via Compte.id déclaré dans le resolver
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Important:
|
|
148
|
+
- **Tous les paramètres du décorateur `@audit` doivent être passés en nommés** (`action=`, `resource_type=`, `id_param=`)
|
|
149
|
+
- **Tous les arguments de la fonction décorée doivent également être passés en nommés** lors de l'appel
|
|
150
|
+
- `id_token: KeycloakIDToken` est obligatoire afin de pouvoir récupérer les informations sur l'utilisateur
|
|
151
|
+
- `session: Session` est obligatoire afin que les logs d'audit soient ajoutées dans la même session
|
|
152
|
+
|
|
153
|
+
### Gestion de la transaction - décorateur @transactional
|
|
154
|
+
|
|
155
|
+
Le décorateur @audit ne réalise aucun commit, c'est action à la charge de la méthode appelante.
|
|
156
|
+
Un décorateur @transactional est mis à disposition afin d'automatiser les commits, exemple :
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from fastapi_audit_recorder.decorators.audit_decorator import audit
|
|
160
|
+
from fastapi_audit_recorder.decorators.transactional_decorator import transactional
|
|
161
|
+
|
|
162
|
+
@transactional
|
|
163
|
+
@audit(action=AuditActionEnum.CREATE.name, resource_type=AuditResourceEnum.COMPTE.name, id_param="db_compte.id")
|
|
164
|
+
def approve_demande_creation(...)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A noter: l'ordre est important, le l'annotation @transactional doit précéder l'annotation @audit pour la persitance des données.
|
|
168
|
+
|
|
169
|
+
### Contexte
|
|
170
|
+
|
|
171
|
+
Dans certain cas, on souhaite auditer une action en fonction de l'appelant.
|
|
172
|
+
Par exemple, auditer une approbation manuelle mais ne pas auditer une autoapprobation.
|
|
173
|
+
|
|
174
|
+
Pour cela il est possible de définir un contexte afin de désactiver un ou plusieurs audits, exemple :
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from fastapi_audit_recorder.context import skip_audit
|
|
178
|
+
|
|
179
|
+
with skip_audit(AuditActionEnum.APPROVE, AuditActionEnum.REJECT)
|
|
180
|
+
with skip_audit():
|
|
181
|
+
service.approve(...)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Ignorer l'audit
|
|
185
|
+
|
|
186
|
+
L'audit est automatiquement réalisée après l'execution de la méthode annotée, sauf si celle-ci lance une exception.
|
|
187
|
+
Il est possible d'ignorer explicitement l'audit. Exemple :
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from fastapi_audit_recorder.result import AuditResult
|
|
191
|
+
|
|
192
|
+
def approve(...) {
|
|
193
|
+
if is_already_approve:
|
|
194
|
+
return AuditResult.skip(db_compte)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Services
|
|
200
|
+
|
|
201
|
+
La lib mets à disposition des services afin de récupérer des données filtrées et paginées :
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from fastapi_audit_recorder import service
|
|
205
|
+
|
|
206
|
+
service.query_logs(...)
|
|
207
|
+
service.query_log_by_id(...)
|
|
208
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serveur de développement local avec données de test.
|
|
3
|
+
Accessible depuis : http://localhost:8000/audit/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, Request
|
|
9
|
+
from fastapi_audit.models import AuditLog, Base
|
|
10
|
+
from fastapi_audit.routers.display_router import get_display_router
|
|
11
|
+
from fastapi_audit.search_router import get_search_router
|
|
12
|
+
from sqlalchemy import create_engine
|
|
13
|
+
from sqlalchemy.orm import sessionmaker
|
|
14
|
+
|
|
15
|
+
# --- DB SQLite en mémoire ---
|
|
16
|
+
engine = create_engine('sqlite:///:memory:', connect_args={'check_same_thread': False})
|
|
17
|
+
SessionLocal = sessionmaker(bind=engine)
|
|
18
|
+
|
|
19
|
+
Base.metadata.create_all(bind=engine)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_db():
|
|
23
|
+
db = SessionLocal()
|
|
24
|
+
try:
|
|
25
|
+
yield db
|
|
26
|
+
finally:
|
|
27
|
+
db.close()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- Données de test ---
|
|
31
|
+
def seed(db):
|
|
32
|
+
entries = [
|
|
33
|
+
AuditLog(
|
|
34
|
+
id='1',
|
|
35
|
+
action='CREATE',
|
|
36
|
+
resource_type='User',
|
|
37
|
+
resource_id='101',
|
|
38
|
+
user_id='42',
|
|
39
|
+
user_email='alice@example.com',
|
|
40
|
+
old_values=None,
|
|
41
|
+
new_values={'name': 'Alice', 'email': 'alice@example.com'},
|
|
42
|
+
changes={'name': {'old': None, 'new': 'Alice'}},
|
|
43
|
+
created_at=datetime(2026, 3, 1, 9, 0, 0, tzinfo=timezone.utc),
|
|
44
|
+
),
|
|
45
|
+
AuditLog(
|
|
46
|
+
id='2',
|
|
47
|
+
action='UPDATE',
|
|
48
|
+
resource_type='User',
|
|
49
|
+
resource_id='101',
|
|
50
|
+
user_id='42',
|
|
51
|
+
user_email='alice@example.com',
|
|
52
|
+
old_values={'name': 'Alice'},
|
|
53
|
+
new_values={'name': 'Alice Dupont'},
|
|
54
|
+
changes={'name': {'old': 'Alice', 'new': 'Alice Dupont'}},
|
|
55
|
+
created_at=datetime(2026, 3, 10, 14, 30, 0, tzinfo=timezone.utc),
|
|
56
|
+
),
|
|
57
|
+
AuditLog(
|
|
58
|
+
id='3',
|
|
59
|
+
action='DELETE',
|
|
60
|
+
resource_type='Order',
|
|
61
|
+
resource_id='55',
|
|
62
|
+
user_id='7',
|
|
63
|
+
user_email='bob@example.com',
|
|
64
|
+
old_values={'status': 'pending', 'amount': 99.9},
|
|
65
|
+
new_values=None,
|
|
66
|
+
changes={'status': {'old': 'pending', 'new': None}},
|
|
67
|
+
created_at=datetime(2026, 3, 15, 8, 0, 0, tzinfo=timezone.utc),
|
|
68
|
+
),
|
|
69
|
+
AuditLog(
|
|
70
|
+
id='4',
|
|
71
|
+
action='CREATE',
|
|
72
|
+
resource_type='Order',
|
|
73
|
+
resource_id='56',
|
|
74
|
+
user_id='7',
|
|
75
|
+
user_email='bob@example.com',
|
|
76
|
+
old_values=None,
|
|
77
|
+
new_values={'status': 'confirmed', 'amount': 149.0},
|
|
78
|
+
changes={'status': {'old': None, 'new': 'confirmed'}},
|
|
79
|
+
created_at=datetime(2026, 3, 18, 11, 0, 0, tzinfo=timezone.utc),
|
|
80
|
+
),
|
|
81
|
+
AuditLog(
|
|
82
|
+
id='5',
|
|
83
|
+
action='APPROVE',
|
|
84
|
+
resource_type='Invoice',
|
|
85
|
+
resource_id='9',
|
|
86
|
+
user_id='42',
|
|
87
|
+
user_email='alice@example.com',
|
|
88
|
+
old_values={'approved': False},
|
|
89
|
+
new_values={'approved': True},
|
|
90
|
+
changes={'approved': {'old': False, 'new': True}},
|
|
91
|
+
created_at=datetime(2026, 3, 19, 10, 0, 0, tzinfo=timezone.utc),
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
db.add_all(entries)
|
|
95
|
+
db.commit()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
with SessionLocal() as db:
|
|
99
|
+
seed(db)
|
|
100
|
+
|
|
101
|
+
# --- App ---
|
|
102
|
+
app = FastAPI(title='Audit Dev Server')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.middleware('http')
|
|
106
|
+
async def inject_dev_cookie(request: Request, call_next):
|
|
107
|
+
"""Injecte un cookie kc_token factice sur toutes les requêtes en dev."""
|
|
108
|
+
headers = dict(request.scope['headers'])
|
|
109
|
+
cookie_header = headers.get(b'cookie', b'')
|
|
110
|
+
if b'kc_token' not in cookie_header:
|
|
111
|
+
sep = b'; ' if cookie_header else b''
|
|
112
|
+
headers[b'cookie'] = cookie_header + sep + b'kc_token=dev-token'
|
|
113
|
+
request.scope['headers'] = list(headers.items())
|
|
114
|
+
return await call_next(request)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
app.include_router(get_search_router(get_db), prefix='/audit')
|
|
118
|
+
app.include_router(get_display_router(), prefix='/audit')
|
|
119
|
+
|
|
120
|
+
if __name__ == '__main__':
|
|
121
|
+
import uvicorn
|
|
122
|
+
|
|
123
|
+
uvicorn.run('dev_server:app', host='0.0.0.0', port=8000, reload=True)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .audit_decorator import audit
|
|
2
|
+
from .context import skip_audit
|
|
3
|
+
from .models import AuditLog, AuditLogDTO, PaginatedAuditLogs
|
|
4
|
+
from .resolver import ResourceResolver
|
|
5
|
+
from .result import AuditResult
|
|
6
|
+
from .transactional_decorator import transactional
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'AuditLog',
|
|
10
|
+
'AuditLogDTO',
|
|
11
|
+
'AuditResult',
|
|
12
|
+
'PaginatedAuditLogs',
|
|
13
|
+
'ResourceResolver',
|
|
14
|
+
'audit',
|
|
15
|
+
'transactional',
|
|
16
|
+
'skip_audit',
|
|
17
|
+
]
|