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/__init__.py
ADDED
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)
|
auditize/app/__init__.py
ADDED
|
@@ -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
|
auditize/app/app_api.py
ADDED
|
@@ -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
|
+
)
|