auditize 0.2.1__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.
- auditize/__init__.py +4 -0
- auditize/__main__.py +212 -0
- auditize/apikey/__init__.py +0 -0
- auditize/apikey/api.py +143 -0
- auditize/apikey/api_models.py +84 -0
- auditize/apikey/models.py +17 -0
- auditize/apikey/service.py +103 -0
- auditize/app/__init__.py +48 -0
- auditize/app/app_api.py +42 -0
- auditize/app/app_static.py +25 -0
- auditize/app/cors.py +15 -0
- auditize/auth/__init__.py +0 -0
- auditize/auth/api.py +83 -0
- auditize/auth/authorizer.py +204 -0
- auditize/auth/constants.py +2 -0
- auditize/auth/jwt.py +102 -0
- auditize/config.py +191 -0
- auditize/data/html/assets/favicon-CmRhlul4.png +0 -0
- auditize/data/html/assets/index-BBo2OJ_i.js +316 -0
- auditize/data/html/assets/index-XfLvnFsA.css +1 -0
- auditize/data/html/auditize-web-component.mjs +32947 -0
- auditize/data/html/index.html +17 -0
- auditize/database.py +105 -0
- auditize/exceptions.py +56 -0
- auditize/helpers/__init__.py +0 -0
- auditize/helpers/api/__init__.py +0 -0
- auditize/helpers/api/errors.py +155 -0
- auditize/helpers/api/validators.py +10 -0
- auditize/helpers/datetime.py +42 -0
- auditize/helpers/email.py +22 -0
- auditize/i18n/__init__.py +7 -0
- auditize/i18n/detection.py +24 -0
- auditize/i18n/lang.py +9 -0
- auditize/i18n/translations/en.json +45 -0
- auditize/i18n/translations/fr.json +45 -0
- auditize/i18n/translator.py +38 -0
- auditize/log/__init__.py +0 -0
- auditize/log/api.py +559 -0
- auditize/log/api_models.py +422 -0
- auditize/log/db.py +118 -0
- auditize/log/models.py +92 -0
- auditize/log/service/__init__.py +31 -0
- auditize/log/service/consolidation.py +515 -0
- auditize/log/service/csv.py +161 -0
- auditize/log/service/main.py +216 -0
- auditize/log_filter/__init__.py +0 -0
- auditize/log_filter/api.py +115 -0
- auditize/log_filter/api_models.py +187 -0
- auditize/log_filter/models.py +28 -0
- auditize/log_filter/service.py +92 -0
- auditize/log_i18n_profile/__init__.py +0 -0
- auditize/log_i18n_profile/api.py +137 -0
- auditize/log_i18n_profile/api_models.py +74 -0
- auditize/log_i18n_profile/models.py +74 -0
- auditize/log_i18n_profile/service.py +103 -0
- auditize/openapi.py +157 -0
- auditize/permissions/__init__.py +0 -0
- auditize/permissions/api_models.py +172 -0
- auditize/permissions/assertions.py +128 -0
- auditize/permissions/models.py +97 -0
- auditize/permissions/operations.py +269 -0
- auditize/permissions/service.py +12 -0
- auditize/repo/__init__.py +0 -0
- auditize/repo/api.py +253 -0
- auditize/repo/api_models.py +141 -0
- auditize/repo/models.py +45 -0
- auditize/repo/service.py +232 -0
- auditize/resource/__init__.py +0 -0
- auditize/resource/api_models.py +27 -0
- auditize/resource/models.py +13 -0
- auditize/resource/pagination/__init__.py +0 -0
- auditize/resource/pagination/cursor/__init__.py +0 -0
- auditize/resource/pagination/cursor/api_models.py +48 -0
- auditize/resource/pagination/cursor/service.py +93 -0
- auditize/resource/pagination/page/__init__.py +0 -0
- auditize/resource/pagination/page/api_models.py +60 -0
- auditize/resource/pagination/page/models.py +17 -0
- auditize/resource/pagination/page/service.py +29 -0
- auditize/resource/service.py +82 -0
- auditize/scheduler.py +15 -0
- auditize/user/__init__.py +0 -0
- auditize/user/api.py +224 -0
- auditize/user/api_models.py +156 -0
- auditize/user/models.py +32 -0
- auditize/user/service.py +220 -0
- auditize-0.2.1.dist-info/METADATA +100 -0
- auditize-0.2.1.dist-info/RECORD +90 -0
- auditize-0.2.1.dist-info/WHEEL +5 -0
- auditize-0.2.1.dist-info/entry_points.txt +2 -0
- auditize-0.2.1.dist-info/top_level.txt +1 -0
auditize/log/api.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Depends, Form, Path, Query, Request, Response, UploadFile
|
|
5
|
+
from fastapi.responses import StreamingResponse
|
|
6
|
+
|
|
7
|
+
from auditize.auth.authorizer import (
|
|
8
|
+
AuthorizedForLogRead,
|
|
9
|
+
AuthorizedForLogWrite,
|
|
10
|
+
)
|
|
11
|
+
from auditize.config import get_config
|
|
12
|
+
from auditize.exceptions import PayloadTooLarge, ValidationError
|
|
13
|
+
from auditize.helpers.api.errors import error_responses
|
|
14
|
+
from auditize.helpers.api.validators import (
|
|
15
|
+
IDENTIFIER_PATTERN_STRING,
|
|
16
|
+
)
|
|
17
|
+
from auditize.helpers.datetime import now
|
|
18
|
+
from auditize.i18n import get_request_lang
|
|
19
|
+
from auditize.log import service
|
|
20
|
+
from auditize.log.api_models import (
|
|
21
|
+
LogCreationRequest,
|
|
22
|
+
LogCreationResponse,
|
|
23
|
+
LogEntityListResponse,
|
|
24
|
+
LogEntityResponse,
|
|
25
|
+
LogReadingResponse,
|
|
26
|
+
LogSearchQueryParams,
|
|
27
|
+
LogsReadingResponse,
|
|
28
|
+
NameListResponse,
|
|
29
|
+
)
|
|
30
|
+
from auditize.log.db import get_log_db_for_reading
|
|
31
|
+
from auditize.log.models import Log, LogSearchParams
|
|
32
|
+
from auditize.resource.pagination.cursor.api_models import CursorPaginationParams
|
|
33
|
+
from auditize.resource.pagination.page.api_models import PagePaginationParams
|
|
34
|
+
|
|
35
|
+
router = APIRouter(
|
|
36
|
+
responses=error_responses(401, 403, 404),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _get_consolidated_data(
|
|
41
|
+
repo_id: UUID,
|
|
42
|
+
get_data_func,
|
|
43
|
+
page_params,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> NameListResponse:
|
|
46
|
+
data, pagination = await get_data_func(
|
|
47
|
+
repo_id,
|
|
48
|
+
page=page_params.page,
|
|
49
|
+
page_size=page_params.page_size,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
52
|
+
return NameListResponse.build(data, pagination)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get(
|
|
56
|
+
"/repos/{repo_id}/logs/actions/types",
|
|
57
|
+
summary="List log action types",
|
|
58
|
+
description="Requires `log:read` permission.",
|
|
59
|
+
operation_id="list_log_action_types",
|
|
60
|
+
responses=error_responses(401, 403, 404),
|
|
61
|
+
tags=["log"],
|
|
62
|
+
)
|
|
63
|
+
async def get_log_action_types(
|
|
64
|
+
authorized: AuthorizedForLogRead(),
|
|
65
|
+
repo_id: UUID,
|
|
66
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
67
|
+
category: str = None,
|
|
68
|
+
) -> NameListResponse:
|
|
69
|
+
return await _get_consolidated_data(
|
|
70
|
+
repo_id,
|
|
71
|
+
service.get_log_action_types,
|
|
72
|
+
page_params,
|
|
73
|
+
action_category=category,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get(
|
|
78
|
+
"/repos/{repo_id}/logs/actions/categories",
|
|
79
|
+
summary="List log action categories",
|
|
80
|
+
description="Requires `log:read` permission.",
|
|
81
|
+
operation_id="list_log_action_categories",
|
|
82
|
+
tags=["log"],
|
|
83
|
+
)
|
|
84
|
+
async def get_log_action_categories(
|
|
85
|
+
authorized: AuthorizedForLogRead(),
|
|
86
|
+
repo_id: UUID,
|
|
87
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
88
|
+
) -> NameListResponse:
|
|
89
|
+
return await _get_consolidated_data(
|
|
90
|
+
repo_id,
|
|
91
|
+
service.get_log_action_categories,
|
|
92
|
+
page_params,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.get(
|
|
97
|
+
"/repos/{repo_id}/logs/actors/types",
|
|
98
|
+
summary="List log actor types",
|
|
99
|
+
description="Requires `log:read` permission.",
|
|
100
|
+
operation_id="list_log_actor_types",
|
|
101
|
+
tags=["log"],
|
|
102
|
+
)
|
|
103
|
+
async def get_log_actor_types(
|
|
104
|
+
authorized: AuthorizedForLogRead(),
|
|
105
|
+
repo_id: UUID,
|
|
106
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
107
|
+
) -> NameListResponse:
|
|
108
|
+
return await _get_consolidated_data(
|
|
109
|
+
repo_id,
|
|
110
|
+
service.get_log_actor_types,
|
|
111
|
+
page_params,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.get(
|
|
116
|
+
"/repos/{repo_id}/logs/actors/extras",
|
|
117
|
+
summary="List log actor custom field names",
|
|
118
|
+
description="Requires `log:read` permission.",
|
|
119
|
+
operation_id="list_log_actor_extras",
|
|
120
|
+
tags=["log"],
|
|
121
|
+
response_model=NameListResponse,
|
|
122
|
+
)
|
|
123
|
+
async def get_log_actor_extras(
|
|
124
|
+
authorized: AuthorizedForLogRead(),
|
|
125
|
+
repo_id: UUID,
|
|
126
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
127
|
+
) -> NameListResponse:
|
|
128
|
+
return await _get_consolidated_data(
|
|
129
|
+
repo_id,
|
|
130
|
+
service.get_log_actor_extra_fields,
|
|
131
|
+
page_params,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get(
|
|
136
|
+
"/repos/{repo_id}/logs/resources/types",
|
|
137
|
+
summary="List log resource types",
|
|
138
|
+
description="Requires `log:read` permission.",
|
|
139
|
+
operation_id="list_log_resource_types",
|
|
140
|
+
tags=["log"],
|
|
141
|
+
)
|
|
142
|
+
async def get_log_resource_types(
|
|
143
|
+
authorized: AuthorizedForLogRead(),
|
|
144
|
+
repo_id: UUID,
|
|
145
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
146
|
+
) -> NameListResponse:
|
|
147
|
+
return await _get_consolidated_data(
|
|
148
|
+
repo_id,
|
|
149
|
+
service.get_log_resource_types,
|
|
150
|
+
page_params,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.get(
|
|
155
|
+
"/repos/{repo_id}/logs/resources/extras",
|
|
156
|
+
summary="List log resource custom field names",
|
|
157
|
+
description="Requires `log:read` permission.",
|
|
158
|
+
operation_id="list_log_resource_extras",
|
|
159
|
+
tags=["log"],
|
|
160
|
+
response_model=NameListResponse,
|
|
161
|
+
)
|
|
162
|
+
async def get_log_resource_extras(
|
|
163
|
+
authorized: AuthorizedForLogRead(),
|
|
164
|
+
repo_id: UUID,
|
|
165
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
166
|
+
) -> NameListResponse:
|
|
167
|
+
return await _get_consolidated_data(
|
|
168
|
+
repo_id,
|
|
169
|
+
service.get_log_resource_extra_fields,
|
|
170
|
+
page_params,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.get(
|
|
175
|
+
"/repos/{repo_id}/logs/tags/types",
|
|
176
|
+
summary="List log tag types",
|
|
177
|
+
description="Requires `log:read` permission.",
|
|
178
|
+
operation_id="list_log_tag_types",
|
|
179
|
+
tags=["log"],
|
|
180
|
+
)
|
|
181
|
+
async def get_log_tag_types(
|
|
182
|
+
authorized: AuthorizedForLogRead(),
|
|
183
|
+
repo_id: UUID,
|
|
184
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
185
|
+
) -> NameListResponse:
|
|
186
|
+
return await _get_consolidated_data(
|
|
187
|
+
repo_id,
|
|
188
|
+
service.get_log_tag_types,
|
|
189
|
+
page_params,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.get(
|
|
194
|
+
"/repos/{repo_id}/logs/sources",
|
|
195
|
+
summary="List log source field names",
|
|
196
|
+
description="Requires `log:read` permission.",
|
|
197
|
+
operation_id="list_log_source_fields",
|
|
198
|
+
tags=["log"],
|
|
199
|
+
response_model=NameListResponse,
|
|
200
|
+
)
|
|
201
|
+
async def get_log_source_fields(
|
|
202
|
+
authorized: AuthorizedForLogRead(),
|
|
203
|
+
repo_id: UUID,
|
|
204
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
205
|
+
) -> NameListResponse:
|
|
206
|
+
return await _get_consolidated_data(
|
|
207
|
+
repo_id,
|
|
208
|
+
service.get_log_source_fields,
|
|
209
|
+
page_params,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@router.get(
|
|
214
|
+
"/repos/{repo_id}/logs/details",
|
|
215
|
+
summary="List log detail field names",
|
|
216
|
+
description="Requires `log:read` permission.",
|
|
217
|
+
operation_id="list_log_detail_fields",
|
|
218
|
+
tags=["log"],
|
|
219
|
+
response_model=NameListResponse,
|
|
220
|
+
)
|
|
221
|
+
async def get_log_detail_fields(
|
|
222
|
+
authorized: AuthorizedForLogRead(),
|
|
223
|
+
repo_id: UUID,
|
|
224
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
225
|
+
) -> NameListResponse:
|
|
226
|
+
return await _get_consolidated_data(
|
|
227
|
+
repo_id,
|
|
228
|
+
service.get_log_detail_fields,
|
|
229
|
+
page_params,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.get(
|
|
234
|
+
"/repos/{repo_id}/logs/attachments/types",
|
|
235
|
+
summary="List log attachment types",
|
|
236
|
+
description="Requires `log:read` permission.",
|
|
237
|
+
operation_id="list_log_attachment_types",
|
|
238
|
+
tags=["log"],
|
|
239
|
+
response_model=NameListResponse,
|
|
240
|
+
)
|
|
241
|
+
async def get_log_attachment_types(
|
|
242
|
+
authorized: AuthorizedForLogRead(),
|
|
243
|
+
repo_id: UUID,
|
|
244
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
245
|
+
) -> NameListResponse:
|
|
246
|
+
return await _get_consolidated_data(
|
|
247
|
+
repo_id,
|
|
248
|
+
service.get_log_attachment_types,
|
|
249
|
+
page_params,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@router.get(
|
|
254
|
+
"/repos/{repo_id}/logs/attachments/mime-types",
|
|
255
|
+
summary="List log attachment MIME types",
|
|
256
|
+
description="Requires `log:read` permission.",
|
|
257
|
+
operation_id="list_log_attachment_mime_types",
|
|
258
|
+
tags=["log"],
|
|
259
|
+
response_model=NameListResponse,
|
|
260
|
+
)
|
|
261
|
+
async def get_log_attachment_mime_types(
|
|
262
|
+
authorized: AuthorizedForLogRead(),
|
|
263
|
+
repo_id: UUID,
|
|
264
|
+
page_params: Annotated[PagePaginationParams, Depends()],
|
|
265
|
+
) -> NameListResponse:
|
|
266
|
+
return await _get_consolidated_data(
|
|
267
|
+
repo_id,
|
|
268
|
+
service.get_log_attachment_mime_types,
|
|
269
|
+
page_params,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@router.get(
|
|
274
|
+
"/repos/{repo_id}/logs/entities",
|
|
275
|
+
summary="List log entities",
|
|
276
|
+
description="Requires `log:read` permission.",
|
|
277
|
+
operation_id="list_log_entities",
|
|
278
|
+
tags=["log"],
|
|
279
|
+
)
|
|
280
|
+
async def get_log_entities(
|
|
281
|
+
authorized: AuthorizedForLogRead(),
|
|
282
|
+
repo_id: UUID,
|
|
283
|
+
root: bool = False,
|
|
284
|
+
parent_entity_ref: str = None,
|
|
285
|
+
page_params: Annotated[PagePaginationParams, Depends()] = PagePaginationParams(),
|
|
286
|
+
) -> LogEntityListResponse:
|
|
287
|
+
if not (root ^ (parent_entity_ref is not None)):
|
|
288
|
+
raise ValidationError(
|
|
289
|
+
"Parameters 'root' and 'parent_entity_ref' are mutually exclusive and one of them must be provided"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if root:
|
|
293
|
+
filter_args = {"parent_entity_ref": None}
|
|
294
|
+
elif parent_entity_ref:
|
|
295
|
+
filter_args = {"parent_entity_ref": parent_entity_ref}
|
|
296
|
+
else:
|
|
297
|
+
filter_args = {}
|
|
298
|
+
|
|
299
|
+
entities, pagination = await service.get_log_entities(
|
|
300
|
+
repo_id,
|
|
301
|
+
authorized_entities=authorized.permissions.logs.get_repo_readable_entities(
|
|
302
|
+
repo_id
|
|
303
|
+
),
|
|
304
|
+
page=page_params.page,
|
|
305
|
+
page_size=page_params.page_size,
|
|
306
|
+
**filter_args,
|
|
307
|
+
)
|
|
308
|
+
return LogEntityListResponse.build(entities, pagination)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@router.get(
|
|
312
|
+
"/repos/{repo_id}/logs/entities/ref:{entity_ref}",
|
|
313
|
+
summary="Get log entity",
|
|
314
|
+
description="Requires `log:read` permission.",
|
|
315
|
+
operation_id="get_log_entity",
|
|
316
|
+
tags=["log"],
|
|
317
|
+
)
|
|
318
|
+
async def get_log_entity(
|
|
319
|
+
authorized: AuthorizedForLogRead(),
|
|
320
|
+
repo_id: UUID,
|
|
321
|
+
entity_ref: Annotated[str, Path(description="Entity ref")],
|
|
322
|
+
) -> LogEntityResponse:
|
|
323
|
+
entity = await service.get_log_entity(
|
|
324
|
+
repo_id,
|
|
325
|
+
entity_ref,
|
|
326
|
+
authorized.permissions.logs.get_repo_readable_entities(repo_id),
|
|
327
|
+
)
|
|
328
|
+
return LogEntityResponse.model_validate(entity.model_dump())
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@router.post(
|
|
332
|
+
"/repos/{repo_id}/logs",
|
|
333
|
+
status_code=201,
|
|
334
|
+
summary="Create a log",
|
|
335
|
+
description="Requires `log:write` permission.",
|
|
336
|
+
operation_id="create_log",
|
|
337
|
+
responses=error_responses(400),
|
|
338
|
+
tags=["log"],
|
|
339
|
+
)
|
|
340
|
+
async def create_log(
|
|
341
|
+
authorized: AuthorizedForLogWrite(),
|
|
342
|
+
repo_id: UUID,
|
|
343
|
+
log_req: LogCreationRequest,
|
|
344
|
+
) -> LogCreationResponse:
|
|
345
|
+
log_id = await service.save_log(repo_id, Log.model_validate(log_req.model_dump()))
|
|
346
|
+
return LogCreationResponse(id=log_id)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@router.post(
|
|
350
|
+
"/repos/{repo_id}/logs/{log_id}/attachments",
|
|
351
|
+
summary="Add a file attachment to a log",
|
|
352
|
+
description="Requires `log:write` permission.",
|
|
353
|
+
operation_id="add_log_attachment",
|
|
354
|
+
tags=["log"],
|
|
355
|
+
status_code=204,
|
|
356
|
+
response_class=Response,
|
|
357
|
+
responses=error_responses(400, 413),
|
|
358
|
+
)
|
|
359
|
+
async def add_attachment(
|
|
360
|
+
authorized: AuthorizedForLogWrite(),
|
|
361
|
+
repo_id: UUID,
|
|
362
|
+
log_id: Annotated[
|
|
363
|
+
UUID,
|
|
364
|
+
Path(description="The ID of the log to attach the file to"),
|
|
365
|
+
],
|
|
366
|
+
file: UploadFile,
|
|
367
|
+
type: Annotated[
|
|
368
|
+
str,
|
|
369
|
+
Form(
|
|
370
|
+
description="The 'functional' type of the attachment",
|
|
371
|
+
json_schema_extra={"example": "Configuration file"},
|
|
372
|
+
pattern=IDENTIFIER_PATTERN_STRING,
|
|
373
|
+
),
|
|
374
|
+
],
|
|
375
|
+
name: Annotated[
|
|
376
|
+
str,
|
|
377
|
+
Form(
|
|
378
|
+
description="The name of the attachment. If not provided, the name of the uploaded file will be used.",
|
|
379
|
+
json_schema_extra={"example": "config.json"},
|
|
380
|
+
),
|
|
381
|
+
] = None,
|
|
382
|
+
mime_type: Annotated[
|
|
383
|
+
str,
|
|
384
|
+
Form(
|
|
385
|
+
description="The MIME type of the attachment. If not provided, the MIME type of the uploaded "
|
|
386
|
+
"file will be used.",
|
|
387
|
+
json_schema_extra={"example": "application/json"},
|
|
388
|
+
),
|
|
389
|
+
] = None,
|
|
390
|
+
) -> None:
|
|
391
|
+
config = get_config()
|
|
392
|
+
data = await file.read(config.attachment_max_size + 1)
|
|
393
|
+
if len(data) > config.attachment_max_size:
|
|
394
|
+
raise PayloadTooLarge(
|
|
395
|
+
f"Attachment size exceeds the maximum allowed size ({config.attachment_max_size} bytes)"
|
|
396
|
+
)
|
|
397
|
+
await service.save_log_attachment(
|
|
398
|
+
repo_id,
|
|
399
|
+
log_id,
|
|
400
|
+
name=name or file.filename,
|
|
401
|
+
type=type,
|
|
402
|
+
mime_type=mime_type or file.content_type or "application/octet-stream",
|
|
403
|
+
data=data,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class _CsvResponse(Response):
|
|
408
|
+
media_type = "text/csv"
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
_COLUMNS_DESCRIPTION = f"""
|
|
412
|
+
Comma-separated list of columns to include in the CSV output. Available columns are:
|
|
413
|
+
{"\n".join(f"- `{col}`" for col in service.CSV_BUILTIN_COLUMNS)}
|
|
414
|
+
- `source.<custom-field>`
|
|
415
|
+
- `actor.<custom-field>`
|
|
416
|
+
- `resource.<custom-field>`
|
|
417
|
+
- `details.<custom-field>`
|
|
418
|
+
|
|
419
|
+
Example of column name if you have a "role" custom field for the actor: `actor.role`.
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
_CUSTOM_FIELDS_DESCRIPTION = (
|
|
424
|
+
"Requires `log:read` permission.\n"
|
|
425
|
+
"\n"
|
|
426
|
+
"This endpoint also accepts search on custom fields through the extra parameters:\n"
|
|
427
|
+
"- `source.<custom-field>`\n"
|
|
428
|
+
"- `actor.<custom-field>`\n"
|
|
429
|
+
"- `resource.<custom-field>`\n"
|
|
430
|
+
"- `details.<custom-field>`\n"
|
|
431
|
+
"\n"
|
|
432
|
+
"Example: `/repos/{repo_id}/logs?actor.role=admin`"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@router.get(
|
|
437
|
+
"/repos/{repo_id}/logs/csv",
|
|
438
|
+
summary="List logs as CSV file",
|
|
439
|
+
description=_CUSTOM_FIELDS_DESCRIPTION,
|
|
440
|
+
operation_id="list_logs_csv",
|
|
441
|
+
tags=["log"],
|
|
442
|
+
response_class=_CsvResponse,
|
|
443
|
+
)
|
|
444
|
+
async def get_logs_as_csv(
|
|
445
|
+
request: Request,
|
|
446
|
+
authorized: AuthorizedForLogRead(),
|
|
447
|
+
repo_id: UUID,
|
|
448
|
+
search_params: Annotated[LogSearchQueryParams, Depends()],
|
|
449
|
+
columns: Annotated[str, Query(description=_COLUMNS_DESCRIPTION)] = ",".join(
|
|
450
|
+
service.CSV_BUILTIN_COLUMNS
|
|
451
|
+
),
|
|
452
|
+
):
|
|
453
|
+
# NB: as we cannot properly handle an error in a StreamingResponse,
|
|
454
|
+
# we perform as much validation as possible before calling get_logs_as_csv
|
|
455
|
+
await get_log_db_for_reading(repo_id)
|
|
456
|
+
columns = columns.split(",") # convert columns string to a list
|
|
457
|
+
service.validate_csv_columns(columns)
|
|
458
|
+
|
|
459
|
+
filename = f"auditize-logs_{repo_id}_{now().strftime("%Y%m%d%H%M%S")}.csv"
|
|
460
|
+
|
|
461
|
+
return StreamingResponse(
|
|
462
|
+
service.get_logs_as_csv(
|
|
463
|
+
repo_id,
|
|
464
|
+
authorized_entities=authorized.permissions.logs.get_repo_readable_entities(
|
|
465
|
+
repo_id
|
|
466
|
+
),
|
|
467
|
+
search_params=LogSearchParams.model_validate(search_params.model_dump()),
|
|
468
|
+
columns=columns,
|
|
469
|
+
lang=get_request_lang(request),
|
|
470
|
+
),
|
|
471
|
+
media_type="text/csv",
|
|
472
|
+
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@router.get(
|
|
477
|
+
"/repos/{repo_id}/logs/{log_id}",
|
|
478
|
+
summary="Get log",
|
|
479
|
+
description="Requires `log:read` permission.",
|
|
480
|
+
operation_id="get_log",
|
|
481
|
+
tags=["log"],
|
|
482
|
+
status_code=200,
|
|
483
|
+
)
|
|
484
|
+
async def get_log(
|
|
485
|
+
authorized: AuthorizedForLogRead(),
|
|
486
|
+
repo_id: UUID,
|
|
487
|
+
log_id: Annotated[UUID, Path(description="Log ID")],
|
|
488
|
+
) -> LogReadingResponse:
|
|
489
|
+
log = await service.get_log(
|
|
490
|
+
repo_id,
|
|
491
|
+
log_id,
|
|
492
|
+
authorized_entities=authorized.permissions.logs.get_repo_readable_entities(
|
|
493
|
+
repo_id
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
return LogReadingResponse.model_validate(log.model_dump())
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@router.get(
|
|
500
|
+
"/repos/{repo_id}/logs/{log_id}/attachments/{attachment_idx}",
|
|
501
|
+
summary="Download a log attachment",
|
|
502
|
+
description="Requires `log:read` permission.",
|
|
503
|
+
operation_id="get_log_attachment",
|
|
504
|
+
tags=["log"],
|
|
505
|
+
response_class=Response,
|
|
506
|
+
responses={
|
|
507
|
+
200: {
|
|
508
|
+
"description": (
|
|
509
|
+
"Attachment content. The actual MIME type will be the MIME type "
|
|
510
|
+
"of the attachment when it was uploaded."
|
|
511
|
+
),
|
|
512
|
+
"content": {
|
|
513
|
+
"application/octet-stream": {
|
|
514
|
+
"schema": {"type": "string", "format": "binary", "example": None}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
async def get_log_attachment(
|
|
521
|
+
authorized: AuthorizedForLogRead(),
|
|
522
|
+
repo_id: UUID,
|
|
523
|
+
log_id: UUID = Path(description="Log ID"),
|
|
524
|
+
attachment_idx: int = Path(
|
|
525
|
+
description="The index of the attachment in the log's attachments list (starts from 0)",
|
|
526
|
+
),
|
|
527
|
+
):
|
|
528
|
+
attachment = await service.get_log_attachment(repo_id, log_id, attachment_idx)
|
|
529
|
+
return Response(
|
|
530
|
+
content=attachment.data,
|
|
531
|
+
media_type=attachment.mime_type,
|
|
532
|
+
headers={"Content-Disposition": f"attachment; filename={attachment.name}"},
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@router.get(
|
|
537
|
+
"/repos/{repo_id}/logs",
|
|
538
|
+
summary="List logs",
|
|
539
|
+
description=_CUSTOM_FIELDS_DESCRIPTION,
|
|
540
|
+
operation_id="list_logs",
|
|
541
|
+
tags=["log"],
|
|
542
|
+
)
|
|
543
|
+
async def get_logs(
|
|
544
|
+
authorized: AuthorizedForLogRead(),
|
|
545
|
+
repo_id: UUID,
|
|
546
|
+
search_params: Annotated[LogSearchQueryParams, Depends()],
|
|
547
|
+
page_params: Annotated[CursorPaginationParams, Depends()],
|
|
548
|
+
) -> LogsReadingResponse:
|
|
549
|
+
# FIXME: we must check that "until" is greater than "since"
|
|
550
|
+
logs, next_cursor = await service.get_logs(
|
|
551
|
+
repo_id,
|
|
552
|
+
authorized_entities=authorized.permissions.logs.get_repo_readable_entities(
|
|
553
|
+
repo_id
|
|
554
|
+
),
|
|
555
|
+
search_params=LogSearchParams.model_validate(search_params.model_dump()),
|
|
556
|
+
limit=page_params.limit,
|
|
557
|
+
pagination_cursor=page_params.cursor,
|
|
558
|
+
)
|
|
559
|
+
return LogsReadingResponse.build(logs, next_cursor)
|