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.
Files changed (90) hide show
  1. auditize/__init__.py +4 -0
  2. auditize/__main__.py +212 -0
  3. auditize/apikey/__init__.py +0 -0
  4. auditize/apikey/api.py +143 -0
  5. auditize/apikey/api_models.py +84 -0
  6. auditize/apikey/models.py +17 -0
  7. auditize/apikey/service.py +103 -0
  8. auditize/app/__init__.py +48 -0
  9. auditize/app/app_api.py +42 -0
  10. auditize/app/app_static.py +25 -0
  11. auditize/app/cors.py +15 -0
  12. auditize/auth/__init__.py +0 -0
  13. auditize/auth/api.py +83 -0
  14. auditize/auth/authorizer.py +204 -0
  15. auditize/auth/constants.py +2 -0
  16. auditize/auth/jwt.py +102 -0
  17. auditize/config.py +191 -0
  18. auditize/data/html/assets/favicon-CmRhlul4.png +0 -0
  19. auditize/data/html/assets/index-BBo2OJ_i.js +316 -0
  20. auditize/data/html/assets/index-XfLvnFsA.css +1 -0
  21. auditize/data/html/auditize-web-component.mjs +32947 -0
  22. auditize/data/html/index.html +17 -0
  23. auditize/database.py +105 -0
  24. auditize/exceptions.py +56 -0
  25. auditize/helpers/__init__.py +0 -0
  26. auditize/helpers/api/__init__.py +0 -0
  27. auditize/helpers/api/errors.py +155 -0
  28. auditize/helpers/api/validators.py +10 -0
  29. auditize/helpers/datetime.py +42 -0
  30. auditize/helpers/email.py +22 -0
  31. auditize/i18n/__init__.py +7 -0
  32. auditize/i18n/detection.py +24 -0
  33. auditize/i18n/lang.py +9 -0
  34. auditize/i18n/translations/en.json +45 -0
  35. auditize/i18n/translations/fr.json +45 -0
  36. auditize/i18n/translator.py +38 -0
  37. auditize/log/__init__.py +0 -0
  38. auditize/log/api.py +559 -0
  39. auditize/log/api_models.py +422 -0
  40. auditize/log/db.py +118 -0
  41. auditize/log/models.py +92 -0
  42. auditize/log/service/__init__.py +31 -0
  43. auditize/log/service/consolidation.py +515 -0
  44. auditize/log/service/csv.py +161 -0
  45. auditize/log/service/main.py +216 -0
  46. auditize/log_filter/__init__.py +0 -0
  47. auditize/log_filter/api.py +115 -0
  48. auditize/log_filter/api_models.py +187 -0
  49. auditize/log_filter/models.py +28 -0
  50. auditize/log_filter/service.py +92 -0
  51. auditize/log_i18n_profile/__init__.py +0 -0
  52. auditize/log_i18n_profile/api.py +137 -0
  53. auditize/log_i18n_profile/api_models.py +74 -0
  54. auditize/log_i18n_profile/models.py +74 -0
  55. auditize/log_i18n_profile/service.py +103 -0
  56. auditize/openapi.py +157 -0
  57. auditize/permissions/__init__.py +0 -0
  58. auditize/permissions/api_models.py +172 -0
  59. auditize/permissions/assertions.py +128 -0
  60. auditize/permissions/models.py +97 -0
  61. auditize/permissions/operations.py +269 -0
  62. auditize/permissions/service.py +12 -0
  63. auditize/repo/__init__.py +0 -0
  64. auditize/repo/api.py +253 -0
  65. auditize/repo/api_models.py +141 -0
  66. auditize/repo/models.py +45 -0
  67. auditize/repo/service.py +232 -0
  68. auditize/resource/__init__.py +0 -0
  69. auditize/resource/api_models.py +27 -0
  70. auditize/resource/models.py +13 -0
  71. auditize/resource/pagination/__init__.py +0 -0
  72. auditize/resource/pagination/cursor/__init__.py +0 -0
  73. auditize/resource/pagination/cursor/api_models.py +48 -0
  74. auditize/resource/pagination/cursor/service.py +93 -0
  75. auditize/resource/pagination/page/__init__.py +0 -0
  76. auditize/resource/pagination/page/api_models.py +60 -0
  77. auditize/resource/pagination/page/models.py +17 -0
  78. auditize/resource/pagination/page/service.py +29 -0
  79. auditize/resource/service.py +82 -0
  80. auditize/scheduler.py +15 -0
  81. auditize/user/__init__.py +0 -0
  82. auditize/user/api.py +224 -0
  83. auditize/user/api_models.py +156 -0
  84. auditize/user/models.py +32 -0
  85. auditize/user/service.py +220 -0
  86. auditize-0.2.1.dist-info/METADATA +100 -0
  87. auditize-0.2.1.dist-info/RECORD +90 -0
  88. auditize-0.2.1.dist-info/WHEEL +5 -0
  89. auditize-0.2.1.dist-info/entry_points.txt +2 -0
  90. auditize-0.2.1.dist-info/top_level.txt +1 -0
auditize/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # Shortcut for ASGI server, the entrypoint is: auditize:asgi
2
+ from .app import app_factory as asgi
3
+
4
+ __version__ = "0.2.1"
auditize/__main__.py ADDED
@@ -0,0 +1,212 @@
1
+ import argparse
2
+ import asyncio
3
+ import getpass
4
+ import json
5
+ import platform
6
+ import sys
7
+
8
+ import uvicorn
9
+
10
+ from auditize import __version__
11
+ from auditize.app import build_api_app, build_app
12
+ from auditize.config import get_config, init_config
13
+ from auditize.database import init_dbm
14
+ from auditize.exceptions import (
15
+ ConfigAlreadyInitialized,
16
+ ConfigError,
17
+ ConstraintViolation,
18
+ )
19
+ from auditize.openapi import get_customized_openapi_schema
20
+ from auditize.permissions.models import Permissions
21
+ from auditize.scheduler import build_scheduler
22
+ from auditize.user.models import User
23
+ from auditize.user.service import (
24
+ get_users,
25
+ hash_user_password,
26
+ save_user,
27
+ )
28
+
29
+
30
+ def _lazy_init(*, skip_dbm_init=False):
31
+ try:
32
+ init_config()
33
+ except ConfigAlreadyInitialized:
34
+ # this case corresponds to tests where config and dbm are already initialized
35
+ return
36
+ except ConfigError as exc:
37
+ sys.exit("ERROR: " + str(exc))
38
+
39
+ if not skip_dbm_init:
40
+ init_dbm()
41
+
42
+
43
+ def _get_password() -> str:
44
+ password = getpass.getpass("Password: ")
45
+ confirm = getpass.getpass("Confirm password: ")
46
+
47
+ if password != confirm:
48
+ print("Passwords do not match, please try again.", file=sys.stderr)
49
+ print("", file=sys.stderr)
50
+ return _get_password()
51
+
52
+ return password
53
+
54
+
55
+ async def _bootstrap_superadmin(
56
+ email: str, first_name: str, last_name: str, password: str
57
+ ):
58
+ await save_user(
59
+ User(
60
+ first_name=first_name,
61
+ last_name=last_name,
62
+ email=email,
63
+ password_hash=hash_user_password(password),
64
+ permissions=Permissions(is_superadmin=True),
65
+ ),
66
+ )
67
+
68
+
69
+ async def bootstrap_superadmin(email: str, first_name: str, last_name: str):
70
+ _lazy_init()
71
+
72
+ password = _get_password()
73
+
74
+ try:
75
+ await _bootstrap_superadmin(email, first_name, last_name, password)
76
+ except ConstraintViolation:
77
+ sys.exit(f"Error: user with email {email} already exists")
78
+ print(f"User with email {email} has been successfully created")
79
+
80
+
81
+ async def bootstrap_default_superadmin():
82
+ _lazy_init()
83
+
84
+ users, _ = await get_users(query=None, page=1, page_size=1)
85
+ if not users:
86
+ await _bootstrap_superadmin(
87
+ "super.admin@example.net", "Super", "Admin", "auditize"
88
+ )
89
+
90
+
91
+ async def serve(host: str, port: int):
92
+ _lazy_init()
93
+ app = build_app()
94
+ config = uvicorn.Config(app, host=host, port=port)
95
+ server = uvicorn.Server(config)
96
+ await server.serve()
97
+
98
+
99
+ async def schedule():
100
+ _lazy_init()
101
+ scheduler = build_scheduler()
102
+ scheduler.start()
103
+ print("Scheduler started")
104
+ try:
105
+ while True:
106
+ await asyncio.sleep(10)
107
+ except asyncio.CancelledError:
108
+ scheduler.shutdown()
109
+
110
+
111
+ async def dump_config():
112
+ _lazy_init(skip_dbm_init=True)
113
+ config = get_config()
114
+ print(json.dumps(config.to_dict(), ensure_ascii=False, indent=4))
115
+
116
+
117
+ async def dump_openapi():
118
+ print(
119
+ json.dumps(
120
+ get_customized_openapi_schema(
121
+ build_api_app(cors_allow_origins=[], online_doc=False),
122
+ include_internal_routes=False,
123
+ ),
124
+ ensure_ascii=False,
125
+ indent=4,
126
+ )
127
+ )
128
+
129
+
130
+ async def version():
131
+ print(
132
+ "auditize version %s (using Python %s - %s)"
133
+ % (__version__, platform.python_version(), sys.executable)
134
+ )
135
+
136
+
137
+ async def async_main(args=None):
138
+ parser = argparse.ArgumentParser()
139
+ parser.add_argument(
140
+ "--version", "-v", action="store_true", help="Print version information"
141
+ )
142
+ sub_parsers = parser.add_subparsers()
143
+
144
+ # CMD bootstrap-default-superadmin
145
+ bootstrap_default_superadmin_parser = sub_parsers.add_parser(
146
+ "bootstrap-default-superadmin",
147
+ help="Bootstrap a default superadmin user if no users exist",
148
+ )
149
+ bootstrap_default_superadmin_parser.set_defaults(
150
+ func=lambda _: bootstrap_default_superadmin()
151
+ )
152
+
153
+ # CMD bootstrap-superadmin
154
+ bootstrap_superadmin_parser = sub_parsers.add_parser(
155
+ "bootstrap-superadmin", help="Create a superadmin user"
156
+ )
157
+ bootstrap_superadmin_parser.add_argument("email")
158
+ bootstrap_superadmin_parser.add_argument("first_name")
159
+ bootstrap_superadmin_parser.add_argument("last_name")
160
+ bootstrap_superadmin_parser.set_defaults(
161
+ func=lambda cmd_args: bootstrap_superadmin(
162
+ cmd_args.email, cmd_args.first_name, cmd_args.last_name
163
+ )
164
+ )
165
+
166
+ # CMD serve
167
+ serve_parser = sub_parsers.add_parser("serve", help="Serve the application")
168
+ serve_parser.add_argument("--host", default="127.0.0.1")
169
+ serve_parser.add_argument("--port", default=8000, type=int)
170
+ serve_parser.set_defaults(func=lambda cmd_args: serve(cmd_args.host, cmd_args.port))
171
+
172
+ # CMD schedule
173
+ schedule_parser = sub_parsers.add_parser(
174
+ "schedule", help="Schedule Auditize periodic tasks"
175
+ )
176
+ schedule_parser.set_defaults(func=lambda _: schedule())
177
+
178
+ # CMD config
179
+ config_parser = sub_parsers.add_parser(
180
+ "config", help="Dump the Auditize configuration as JSON"
181
+ )
182
+ config_parser.set_defaults(func=lambda _: dump_config())
183
+
184
+ # CMD openapi
185
+ openapi_parser = sub_parsers.add_parser("openapi", help="Dump the OpenAPI schema")
186
+ openapi_parser.set_defaults(func=lambda _: dump_openapi())
187
+
188
+ # CMD version
189
+ version_parser = sub_parsers.add_parser("version", help="Print version information")
190
+ version_parser.set_defaults(func=lambda _: version())
191
+
192
+ parsed_args = parser.parse_args(args)
193
+
194
+ if parsed_args.version:
195
+ await version()
196
+ return 0
197
+
198
+ if not hasattr(parsed_args, "func"):
199
+ parser.print_help()
200
+ return 1
201
+
202
+ await parsed_args.func(parsed_args)
203
+
204
+ return 0
205
+
206
+
207
+ def main(args=None):
208
+ return asyncio.run(async_main(args))
209
+
210
+
211
+ if __name__ == "__main__":
212
+ sys.exit(main(sys.argv[1:]))
File without changes
auditize/apikey/api.py ADDED
@@ -0,0 +1,143 @@
1
+ from typing import Annotated
2
+ from uuid import UUID
3
+
4
+ from fastapi import APIRouter, Depends
5
+
6
+ from auditize.apikey import service
7
+ from auditize.apikey.api_models import (
8
+ ApikeyCreationRequest,
9
+ ApikeyCreationResponse,
10
+ ApikeyListResponse,
11
+ ApikeyReadingResponse,
12
+ ApikeyRegenerationResponse,
13
+ ApikeyUpdateRequest,
14
+ )
15
+ from auditize.apikey.models import Apikey, ApikeyUpdate
16
+ from auditize.auth.authorizer import Authenticated, Authorized
17
+ from auditize.exceptions import PermissionDenied
18
+ from auditize.helpers.api.errors import error_responses
19
+ from auditize.permissions.assertions import (
20
+ can_read_apikey,
21
+ can_write_apikey,
22
+ )
23
+ from auditize.permissions.operations import authorize_grant
24
+ from auditize.resource.api_models import ResourceSearchParams
25
+ from auditize.resource.pagination.page.api_models import PagePaginationParams
26
+
27
+ router = APIRouter(responses=error_responses(401, 403))
28
+
29
+
30
+ def _ensure_cannot_alter_own_apikey(authorized: Authenticated, apikey_id: UUID):
31
+ if authorized.apikey and authorized.apikey.id == apikey_id:
32
+ raise PermissionDenied("Cannot alter own apikey")
33
+
34
+
35
+ @router.post(
36
+ "/apikeys",
37
+ summary="Create API key",
38
+ description="Requires `apikey:write` permission.",
39
+ operation_id="create_apikey",
40
+ tags=["apikey"],
41
+ status_code=201,
42
+ responses=error_responses(400, 409),
43
+ )
44
+ async def create_apikey(
45
+ authorized: Authorized(can_write_apikey()),
46
+ apikey: ApikeyCreationRequest,
47
+ ) -> ApikeyCreationResponse:
48
+ apikey_model = Apikey.model_validate(apikey.model_dump())
49
+ authorize_grant(authorized.permissions, apikey_model.permissions)
50
+ apikey_id, key = await service.create_apikey(apikey_model)
51
+ return ApikeyCreationResponse(id=apikey_id, key=key)
52
+
53
+
54
+ @router.patch(
55
+ "/apikeys/{apikey_id}",
56
+ summary="Update API key",
57
+ description="Requires `apikey:write` permission.",
58
+ operation_id="update_apikey",
59
+ tags=["apikey"],
60
+ status_code=204,
61
+ responses=error_responses(400, 404, 409),
62
+ )
63
+ async def update_apikey(
64
+ authorized: Authorized(can_write_apikey()),
65
+ apikey_id: UUID,
66
+ apikey: ApikeyUpdateRequest,
67
+ ):
68
+ _ensure_cannot_alter_own_apikey(authorized, apikey_id)
69
+ apikey_model = ApikeyUpdate.model_validate(apikey.model_dump(exclude_unset=True))
70
+ if apikey_model.permissions:
71
+ authorize_grant(authorized.permissions, apikey_model.permissions)
72
+ await service.update_apikey(apikey_id, apikey_model)
73
+
74
+
75
+ @router.get(
76
+ "/apikeys/{apikey_id}",
77
+ summary="Get API key",
78
+ description="Requires `apikey:read` permission.",
79
+ operation_id="get_apikey",
80
+ tags=["apikey"],
81
+ responses=error_responses(404),
82
+ )
83
+ async def get_repo(
84
+ authorized: Authorized(can_read_apikey()),
85
+ apikey_id: UUID,
86
+ ) -> ApikeyReadingResponse:
87
+ apikey = await service.get_apikey(apikey_id)
88
+ return ApikeyReadingResponse.model_validate(apikey.model_dump())
89
+
90
+
91
+ @router.get(
92
+ "/apikeys",
93
+ summary="List API keys",
94
+ description="Requires `apikey:read` permission.",
95
+ operation_id="list_apikeys",
96
+ tags=["apikey"],
97
+ )
98
+ async def list_apikeys(
99
+ authorized: Authorized(can_read_apikey()),
100
+ search_params: Annotated[ResourceSearchParams, Depends()],
101
+ page_params: Annotated[PagePaginationParams, Depends()],
102
+ ) -> ApikeyListResponse:
103
+ apikeys, page_info = await service.get_apikeys(
104
+ query=search_params.query,
105
+ page=page_params.page,
106
+ page_size=page_params.page_size,
107
+ )
108
+ return ApikeyListResponse.build(apikeys, page_info)
109
+
110
+
111
+ @router.delete(
112
+ "/apikeys/{apikey_id}",
113
+ summary="Delete API key",
114
+ description="Requires `apikey:write` permission.",
115
+ operation_id="delete_apikey",
116
+ tags=["apikey"],
117
+ status_code=204,
118
+ responses=error_responses(404),
119
+ )
120
+ async def delete_apikey(
121
+ authorized: Authorized(can_write_apikey()),
122
+ apikey_id: UUID,
123
+ ):
124
+ _ensure_cannot_alter_own_apikey(authorized, apikey_id)
125
+ await service.delete_apikey(apikey_id)
126
+
127
+
128
+ @router.post(
129
+ "/apikeys/{apikey_id}/key",
130
+ summary="Re-generate API key secret",
131
+ description="Requires `apikey:write` permission.",
132
+ operation_id="generate_apikey_new_secret",
133
+ tags=["apikey"],
134
+ status_code=200,
135
+ responses=error_responses(404),
136
+ )
137
+ async def regenerate_apikey(
138
+ authorized: Authorized(can_write_apikey()),
139
+ apikey_id: UUID,
140
+ ) -> ApikeyRegenerationResponse:
141
+ _ensure_cannot_alter_own_apikey(authorized, apikey_id)
142
+ key = await service.regenerate_apikey(apikey_id)
143
+ return ApikeyRegenerationResponse(key=key)
@@ -0,0 +1,84 @@
1
+ from datetime import datetime
2
+ from uuid import UUID
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from auditize.apikey.models import Apikey
7
+ from auditize.permissions.api_models import PermissionsInputData, PermissionsOutputData
8
+ from auditize.resource.api_models import HasDatetimeSerialization, IdField
9
+ from auditize.resource.pagination.page.api_models import PagePaginatedResponse
10
+
11
+
12
+ def _ApikeyNameField(**kwargs): # noqa
13
+ return Field(
14
+ description="The API key name",
15
+ json_schema_extra={"example": "Integration API key"},
16
+ **kwargs,
17
+ )
18
+
19
+
20
+ def _ApikeyIdField(): # noqa
21
+ return IdField(description="API key ID")
22
+
23
+
24
+ def _ApikeyKeyField(description="The API key secret", **kwargs): # noqa
25
+ return Field(
26
+ description=description,
27
+ json_schema_extra={
28
+ "example": "aak-_euGzb85ZisAZtwx8d78NtC1ohK5suU7-u_--jIENlU"
29
+ },
30
+ **kwargs,
31
+ )
32
+
33
+
34
+ def _ApikeyPermissionsField(**kwargs): # noqa
35
+ return Field(
36
+ description="The API key permissions",
37
+ **kwargs,
38
+ )
39
+
40
+
41
+ class ApikeyCreationRequest(BaseModel):
42
+ name: str = _ApikeyNameField()
43
+ permissions: PermissionsInputData = _ApikeyPermissionsField(
44
+ default_factory=PermissionsInputData
45
+ )
46
+
47
+
48
+ class ApikeyUpdateRequest(BaseModel):
49
+ name: str = _ApikeyNameField(default=None)
50
+ permissions: PermissionsInputData = _ApikeyPermissionsField(default=None)
51
+
52
+
53
+ class ApikeyCreationResponse(BaseModel):
54
+ id: UUID = _ApikeyIdField()
55
+ key: str = _ApikeyKeyField()
56
+
57
+
58
+ class ApikeyReadingResponse(BaseModel):
59
+ id: UUID = _ApikeyIdField()
60
+ name: str = _ApikeyNameField()
61
+ permissions: PermissionsOutputData = _ApikeyPermissionsField()
62
+
63
+
64
+ class ApikeyListResponse(PagePaginatedResponse[Apikey, ApikeyReadingResponse]):
65
+ @classmethod
66
+ def build_item(cls, apikey: Apikey) -> ApikeyReadingResponse:
67
+ return ApikeyReadingResponse.model_validate(apikey.model_dump())
68
+
69
+
70
+ class ApikeyRegenerationResponse(BaseModel):
71
+ key: str = _ApikeyKeyField(description="The new API key secret")
72
+
73
+
74
+ class AccessTokenRequest(BaseModel):
75
+ permissions: PermissionsInputData = _ApikeyPermissionsField()
76
+
77
+
78
+ class AccessTokenResponse(BaseModel, HasDatetimeSerialization):
79
+ access_token: str = Field(
80
+ description="The access token",
81
+ )
82
+ expires_at: datetime = Field(
83
+ description="The access token expiration time",
84
+ )
@@ -0,0 +1,17 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from auditize.permissions.models import Permissions
6
+ from auditize.resource.models import HasCreatedAt, HasId
7
+
8
+
9
+ class Apikey(BaseModel, HasId, HasCreatedAt):
10
+ name: str
11
+ key_hash: Optional[str] = Field(default=None)
12
+ permissions: Permissions = Field(default_factory=Permissions)
13
+
14
+
15
+ class ApikeyUpdate(BaseModel):
16
+ name: str = None
17
+ permissions: Permissions = None
@@ -0,0 +1,103 @@
1
+ import hashlib
2
+ import secrets
3
+ from uuid import UUID
4
+
5
+ from auditize.apikey.models import Apikey, ApikeyUpdate
6
+ from auditize.auth.constants import APIKEY_SECRET_PREFIX
7
+ from auditize.database import get_dbm
8
+ from auditize.exceptions import enhance_constraint_violation_exception
9
+ from auditize.permissions.operations import normalize_permissions, update_permissions
10
+ from auditize.permissions.service import remove_repo_from_permissions
11
+ from auditize.repo.service import ensure_repos_in_permissions_exist
12
+ from auditize.resource.pagination.page.models import PagePaginationInfo
13
+ from auditize.resource.pagination.page.service import find_paginated_by_page
14
+ from auditize.resource.service import (
15
+ create_resource_document,
16
+ delete_resource_document,
17
+ get_resource_document,
18
+ update_resource_document,
19
+ )
20
+
21
+
22
+ def _hash_key(key: str) -> str:
23
+ # Generate a non-salted hash of the key, so it can be looked up afterward
24
+ return hashlib.sha256(key.encode()).hexdigest()
25
+
26
+
27
+ def _generate_key() -> tuple[str, str]:
28
+ value = APIKEY_SECRET_PREFIX + secrets.token_urlsafe(32)
29
+ return value, _hash_key(value)
30
+
31
+
32
+ async def create_apikey(apikey: Apikey) -> tuple[UUID, str]:
33
+ await ensure_repos_in_permissions_exist(apikey.permissions)
34
+ key, key_hash = _generate_key()
35
+ with enhance_constraint_violation_exception("error.constraint_violation.apikey"):
36
+ apikey_id = await create_resource_document(
37
+ get_dbm().core_db.apikeys,
38
+ {
39
+ **apikey.model_dump(exclude={"id", "key_hash", "permissions"}),
40
+ "key_hash": key_hash,
41
+ "permissions": normalize_permissions(apikey.permissions).model_dump(),
42
+ },
43
+ )
44
+ return apikey_id, key
45
+
46
+
47
+ async def update_apikey(apikey_id: UUID, update: ApikeyUpdate):
48
+ doc_update = update.model_dump(exclude_unset=True, exclude={"permissions"})
49
+ if update.permissions:
50
+ apikey = await get_apikey(apikey_id)
51
+ apikey_permissions = update_permissions(apikey.permissions, update.permissions)
52
+ await ensure_repos_in_permissions_exist(apikey_permissions)
53
+ doc_update["permissions"] = apikey_permissions.model_dump()
54
+
55
+ with enhance_constraint_violation_exception("error.constraint_violation.apikey"):
56
+ await update_resource_document(get_dbm().core_db.apikeys, apikey_id, doc_update)
57
+
58
+
59
+ async def regenerate_apikey(apikey_id: UUID) -> str:
60
+ dbm = get_dbm()
61
+ key, key_hash = _generate_key()
62
+ await update_resource_document(
63
+ dbm.core_db.apikeys, apikey_id, {"key_hash": key_hash}
64
+ )
65
+ return key
66
+
67
+
68
+ async def _get_apikey(filter: any) -> Apikey:
69
+ dbm = get_dbm()
70
+ result = await get_resource_document(dbm.core_db.apikeys, filter)
71
+ return Apikey.model_validate(result)
72
+
73
+
74
+ async def get_apikey(apikey_id: UUID) -> Apikey:
75
+ return await _get_apikey(apikey_id)
76
+
77
+
78
+ async def get_apikey_by_key(key: str) -> Apikey:
79
+ return await _get_apikey({"key_hash": _hash_key(key)})
80
+
81
+
82
+ async def get_apikeys(
83
+ query: str, page: int, page_size: int
84
+ ) -> tuple[list[Apikey], PagePaginationInfo]:
85
+ dbm = get_dbm()
86
+ results, page_info = await find_paginated_by_page(
87
+ dbm.core_db.apikeys,
88
+ filter={"$text": {"$search": query}} if query else None,
89
+ sort=[("name", 1)],
90
+ page=page,
91
+ page_size=page_size,
92
+ )
93
+ return [Apikey.model_validate(result) async for result in results], page_info
94
+
95
+
96
+ async def delete_apikey(apikey_id: UUID):
97
+ dbm = get_dbm()
98
+ await delete_resource_document(dbm.core_db.apikeys, apikey_id)
99
+
100
+
101
+ async def remove_repo_from_apikeys_permissions(repo_id: UUID):
102
+ dbm = get_dbm()
103
+ await remove_repo_from_permissions(dbm.core_db.apikeys, repo_id)
@@ -0,0 +1,48 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from auditize.app.app_api import build_app as build_api_app
6
+ from auditize.app.app_static import build_app as build_static_app
7
+ from auditize.app.cors import setup_cors
8
+ from auditize.config import get_config, init_config
9
+ from auditize.database import init_dbm
10
+
11
+ __all__ = ("build_app", "build_api_app", "app_factory")
12
+
13
+
14
+ @asynccontextmanager
15
+ async def _setup_app(_):
16
+ dbm = init_dbm()
17
+ await dbm.setup()
18
+ yield
19
+
20
+
21
+ def build_app():
22
+ # This function is intended to be used in a context where
23
+ # config and dbm have already been initialized
24
+ app = FastAPI(openapi_url=None)
25
+ config = get_config()
26
+ app.mount(
27
+ "/api",
28
+ build_api_app(
29
+ cors_allow_origins=config.cors_allow_origins, online_doc=config.online_doc
30
+ ),
31
+ )
32
+ app.mount("/", build_static_app(cors_allow_origins=config.cors_allow_origins))
33
+ return app
34
+
35
+
36
+ def app_factory():
37
+ # This function is intended to be used with
38
+ # uvicorn auditize.app:app_factory --factory
39
+ config = init_config()
40
+ app = FastAPI(lifespan=_setup_app, openapi_url=None)
41
+ app.mount(
42
+ "/api",
43
+ build_api_app(
44
+ cors_allow_origins=config.cors_allow_origins, online_doc=config.online_doc
45
+ ),
46
+ )
47
+ app.mount("/", build_static_app(cors_allow_origins=config.cors_allow_origins))
48
+ return app
@@ -0,0 +1,42 @@
1
+ from fastapi import APIRouter, FastAPI
2
+ from fastapi.exceptions import RequestValidationError
3
+
4
+ from auditize.apikey.api import router as apikey_api_router
5
+ from auditize.app.cors import setup_cors
6
+ from auditize.auth.api import router as auth_api_router
7
+ from auditize.exceptions import AuditizeException
8
+ from auditize.helpers.api.errors import (
9
+ make_response_from_exception,
10
+ )
11
+ from auditize.i18n import get_request_lang
12
+ from auditize.log.api import router as log_api_router
13
+ from auditize.log_filter.api import router as log_filter_api_router
14
+ from auditize.log_i18n_profile.api import router as log_i18n_profile_api_router
15
+ from auditize.openapi import customize_openapi
16
+ from auditize.repo.api import router as repo_api_router
17
+ from auditize.user.api import router as user_api_router
18
+
19
+
20
+ def _exception_handler(request, exc):
21
+ return make_response_from_exception(exc, get_request_lang(request))
22
+
23
+
24
+ def build_app(*, cors_allow_origins: list[str], online_doc: bool):
25
+ if online_doc:
26
+ app = FastAPI()
27
+ else:
28
+ app = FastAPI(openapi_url=None)
29
+ app.add_exception_handler(AuditizeException, _exception_handler)
30
+ app.add_exception_handler(RequestValidationError, _exception_handler)
31
+ customize_openapi(app)
32
+ router = APIRouter()
33
+ router.include_router(auth_api_router)
34
+ router.include_router(log_api_router)
35
+ router.include_router(repo_api_router)
36
+ router.include_router(user_api_router)
37
+ router.include_router(apikey_api_router)
38
+ router.include_router(log_i18n_profile_api_router)
39
+ router.include_router(log_filter_api_router)
40
+ app.include_router(router)
41
+ setup_cors(app, cors_allow_origins=cors_allow_origins)
42
+ return app
@@ -0,0 +1,25 @@
1
+ import os.path as osp
2
+
3
+ from fastapi import FastAPI
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+ from starlette.responses import FileResponse
6
+ from starlette.staticfiles import StaticFiles
7
+
8
+ from auditize.app.cors import setup_cors
9
+
10
+ _STATIC_DIR = osp.join(osp.dirname(__file__), osp.pardir, "data", "html")
11
+
12
+
13
+ async def _index_html_redirection(request, call_next):
14
+ response = await call_next(request)
15
+ if response.status_code == 404:
16
+ return FileResponse(osp.join(_STATIC_DIR, "index.html"))
17
+ return response
18
+
19
+
20
+ def build_app(*, cors_allow_origins: list[str]):
21
+ app = FastAPI(openapi_url=None)
22
+ app.mount("", StaticFiles(directory=_STATIC_DIR))
23
+ app.add_middleware(BaseHTTPMiddleware, dispatch=_index_html_redirection)
24
+ setup_cors(app, cors_allow_origins=cors_allow_origins)
25
+ return app
auditize/app/cors.py ADDED
@@ -0,0 +1,15 @@
1
+ from fastapi import FastAPI
2
+ from starlette.middleware.cors import CORSMiddleware
3
+
4
+
5
+ def setup_cors(app: FastAPI, *, cors_allow_origins: list[str]):
6
+ if not cors_allow_origins:
7
+ return
8
+
9
+ app.add_middleware(
10
+ CORSMiddleware,
11
+ allow_origins=cors_allow_origins,
12
+ allow_credentials=True,
13
+ allow_methods=["*"],
14
+ allow_headers=["*"],
15
+ )