fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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.
- fastapi_rtk/__init__.py +39 -35
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +476 -221
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/generic/__init__.py +6 -0
- fastapi_rtk/backends/generic/column.py +21 -12
- fastapi_rtk/backends/generic/db.py +42 -7
- fastapi_rtk/backends/generic/filters.py +21 -16
- fastapi_rtk/backends/generic/interface.py +14 -8
- fastapi_rtk/backends/generic/model.py +19 -11
- fastapi_rtk/backends/sqla/__init__.py +1 -0
- fastapi_rtk/backends/sqla/db.py +77 -17
- fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
- fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
- fastapi_rtk/backends/sqla/filters.py +50 -21
- fastapi_rtk/backends/sqla/interface.py +96 -34
- fastapi_rtk/backends/sqla/model.py +56 -39
- fastapi_rtk/bases/__init__.py +20 -0
- fastapi_rtk/bases/db.py +94 -7
- fastapi_rtk/bases/file_manager.py +47 -3
- fastapi_rtk/bases/filter.py +22 -0
- fastapi_rtk/bases/interface.py +49 -5
- fastapi_rtk/bases/model.py +3 -0
- fastapi_rtk/bases/session.py +2 -0
- fastapi_rtk/cli/cli.py +62 -9
- fastapi_rtk/cli/commands/__init__.py +23 -0
- fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
- fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
- fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
- fastapi_rtk/cli/commands/translate.py +299 -0
- fastapi_rtk/cli/decorators.py +9 -4
- fastapi_rtk/cli/utils.py +46 -0
- fastapi_rtk/config.py +41 -1
- fastapi_rtk/const.py +29 -1
- fastapi_rtk/db.py +76 -40
- fastapi_rtk/decorators.py +1 -1
- fastapi_rtk/dependencies.py +134 -62
- fastapi_rtk/exceptions.py +51 -1
- fastapi_rtk/fastapi_react_toolkit.py +186 -171
- fastapi_rtk/file_managers/file_manager.py +8 -6
- fastapi_rtk/file_managers/s3_file_manager.py +69 -33
- fastapi_rtk/globals.py +22 -12
- fastapi_rtk/lang/__init__.py +3 -0
- fastapi_rtk/lang/babel/__init__.py +4 -0
- fastapi_rtk/lang/babel/cli.py +40 -0
- fastapi_rtk/lang/babel/config.py +17 -0
- fastapi_rtk/lang/babel.cfg +1 -0
- fastapi_rtk/lang/lazy_text.py +120 -0
- fastapi_rtk/lang/messages.pot +238 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
- fastapi_rtk/manager.py +355 -37
- fastapi_rtk/mixins.py +12 -0
- fastapi_rtk/routers.py +208 -72
- fastapi_rtk/schemas.py +142 -39
- fastapi_rtk/security/sqla/apis.py +39 -13
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +369 -11
- fastapi_rtk/setting.py +446 -88
- fastapi_rtk/types.py +94 -27
- fastapi_rtk/utils/__init__.py +8 -0
- fastapi_rtk/utils/async_task_runner.py +286 -61
- fastapi_rtk/utils/csv_json_converter.py +243 -40
- fastapi_rtk/utils/hooks.py +34 -0
- fastapi_rtk/utils/merge_schema.py +3 -3
- fastapi_rtk/utils/multiple_async_contexts.py +21 -0
- fastapi_rtk/utils/pydantic.py +46 -1
- fastapi_rtk/utils/run_utils.py +31 -1
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/utils/use_default_when_none.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
- fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
- fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
- fastapi_rtk/backends/gremlinpython/column.py +0 -208
- fastapi_rtk/backends/gremlinpython/db.py +0 -228
- fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
- fastapi_rtk/backends/gremlinpython/filters.py +0 -461
- fastapi_rtk/backends/gremlinpython/interface.py +0 -734
- fastapi_rtk/backends/gremlinpython/model.py +0 -364
- fastapi_rtk/backends/gremlinpython/session.py +0 -23
- fastapi_rtk/cli/commands.py +0 -295
- fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
- fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
- fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/db.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
3
|
import contextvars
|
|
4
|
+
import typing
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
Callable,
|
|
7
8
|
Dict,
|
|
8
|
-
Literal,
|
|
9
9
|
Optional,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
import sqlalchemy.orm
|
|
12
13
|
from fastapi import Depends
|
|
13
14
|
from fastapi_users.db import SQLAlchemyUserDatabase
|
|
14
15
|
from fastapi_users.models import ID, OAP, UP
|
|
@@ -39,7 +40,8 @@ from sqlalchemy.orm import (
|
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
from .backends.sqla.model import Table, metadata, metadatas
|
|
42
|
-
from .const import FASTAPI_RTK_TABLES
|
|
43
|
+
from .const import DEFAULT_METADATA_KEY, FASTAPI_RTK_TABLES, logger
|
|
44
|
+
from .exceptions import FastAPIReactToolkitException
|
|
43
45
|
from .security.sqla.models import OAuthAccount, User
|
|
44
46
|
from .utils import T, lazy_self, safe_call, smart_run, smartdefaultdict
|
|
45
47
|
|
|
@@ -90,6 +92,7 @@ class UserDatabase(SQLAlchemyUserDatabase[UP, ID]):
|
|
|
90
92
|
|
|
91
93
|
statement = (
|
|
92
94
|
select(self.user_table)
|
|
95
|
+
.options(sqlalchemy.orm.selectinload(self.user_table.oauth_accounts))
|
|
93
96
|
.join(self.oauth_account_table)
|
|
94
97
|
.where(self.oauth_account_table.oauth_name == oauth)
|
|
95
98
|
.where(self.oauth_account_table.account_id == account_id)
|
|
@@ -120,6 +123,7 @@ class UserDatabase(SQLAlchemyUserDatabase[UP, ID]):
|
|
|
120
123
|
raise NotImplementedError()
|
|
121
124
|
|
|
122
125
|
await safe_call(self.session.refresh(user))
|
|
126
|
+
await user.load("oauth_accounts")
|
|
123
127
|
oauth_account = self.oauth_account_table(**create_dict)
|
|
124
128
|
self.session.add(oauth_account)
|
|
125
129
|
user.oauth_accounts.append(oauth_account)
|
|
@@ -281,14 +285,14 @@ class DatabaseSessionManager:
|
|
|
281
285
|
"""
|
|
282
286
|
Initializes the tables required for FastAPI RTK to function.
|
|
283
287
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
await self.create_all(
|
|
289
|
+
None,
|
|
290
|
+
tables=[
|
|
291
|
+
table
|
|
292
|
+
for key, table in metadata.tables.items()
|
|
293
|
+
if key in FASTAPI_RTK_TABLES
|
|
294
|
+
],
|
|
295
|
+
)
|
|
292
296
|
|
|
293
297
|
async def close(self):
|
|
294
298
|
"""
|
|
@@ -332,6 +336,8 @@ class DatabaseSessionManager:
|
|
|
332
336
|
Yields:
|
|
333
337
|
AsyncConnection | Connection: The database connection.
|
|
334
338
|
"""
|
|
339
|
+
if bind == DEFAULT_METADATA_KEY:
|
|
340
|
+
bind = None
|
|
335
341
|
engine = self._engine_binds.get(bind) if bind else self._engine
|
|
336
342
|
if not engine:
|
|
337
343
|
raise Exception("DatabaseSessionManager is not initialized")
|
|
@@ -341,14 +347,20 @@ class DatabaseSessionManager:
|
|
|
341
347
|
try:
|
|
342
348
|
yield connection
|
|
343
349
|
except Exception:
|
|
344
|
-
|
|
350
|
+
try:
|
|
351
|
+
await connection.rollback()
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Failed to rollback connection: {e}")
|
|
345
354
|
raise
|
|
346
355
|
else:
|
|
347
356
|
with engine.begin() as connection:
|
|
348
357
|
try:
|
|
349
358
|
yield connection
|
|
350
359
|
except Exception:
|
|
351
|
-
|
|
360
|
+
try:
|
|
361
|
+
connection.rollback()
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"Failed to rollback connection: {e}")
|
|
352
364
|
raise
|
|
353
365
|
|
|
354
366
|
@contextlib.asynccontextmanager
|
|
@@ -367,6 +379,8 @@ class DatabaseSessionManager:
|
|
|
367
379
|
Yields:
|
|
368
380
|
AsyncSession | Session: The database session.
|
|
369
381
|
"""
|
|
382
|
+
if bind == DEFAULT_METADATA_KEY:
|
|
383
|
+
bind = None
|
|
370
384
|
session_maker = (
|
|
371
385
|
self._sessionmaker_binds.get(bind) if bind else self._sessionmaker
|
|
372
386
|
)
|
|
@@ -379,10 +393,16 @@ class DatabaseSessionManager:
|
|
|
379
393
|
try:
|
|
380
394
|
yield session
|
|
381
395
|
except Exception:
|
|
382
|
-
|
|
396
|
+
try:
|
|
397
|
+
await safe_call(session.rollback())
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f"Failed to rollback session: {e}")
|
|
383
400
|
raise
|
|
384
401
|
finally:
|
|
385
|
-
|
|
402
|
+
try:
|
|
403
|
+
await safe_call(session.close())
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.error(f"Failed to close session: {e}")
|
|
386
406
|
self._session_context.reset(token)
|
|
387
407
|
self._sessions_context[bind].reset(bind_token)
|
|
388
408
|
|
|
@@ -423,41 +443,52 @@ class DatabaseSessionManager:
|
|
|
423
443
|
await safe_call(scoped_session_maker.remove())
|
|
424
444
|
|
|
425
445
|
# Used for testing
|
|
426
|
-
async def create_all(
|
|
446
|
+
async def create_all(
|
|
447
|
+
self,
|
|
448
|
+
binds: typing.Literal["all", "default"] | str | list[str] | None = "all",
|
|
449
|
+
**kwargs,
|
|
450
|
+
):
|
|
427
451
|
"""
|
|
428
452
|
Creates all tables in the database.
|
|
429
453
|
|
|
430
454
|
Args:
|
|
431
|
-
binds (
|
|
455
|
+
binds (typing.Literal["all", "default"] | str | list[str] | None, optional): The bind keys to create tables for. If `"default"` or `None`, only the tables for the primary database are created. If `"all"`, tables for all databases are created. If a string or list of strings, only the tables for the specified bind keys are created. Defaults to `"all"`.
|
|
456
|
+
**kwargs: Additional keyword arguments to pass to the `create_all` method of the metadata.
|
|
432
457
|
"""
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if
|
|
437
|
-
|
|
458
|
+
metadata_to_create = list[tuple[str, MetaData]]()
|
|
459
|
+
if binds == "all" or binds == DEFAULT_METADATA_KEY or binds is None:
|
|
460
|
+
metadata_to_create.append((None, metadata))
|
|
461
|
+
if binds == "all":
|
|
462
|
+
metadata_to_create.extend(
|
|
463
|
+
(key, metadatas[key]) for key in self._engine_binds.keys()
|
|
464
|
+
)
|
|
465
|
+
elif binds is not None:
|
|
466
|
+
binds = [binds] if isinstance(binds, str) else binds
|
|
467
|
+
for bind in binds:
|
|
468
|
+
if bind == DEFAULT_METADATA_KEY:
|
|
469
|
+
metadata_to_create.append((None, metadata))
|
|
470
|
+
elif bind in self._engine_binds:
|
|
471
|
+
metadata_to_create.append((bind, metadatas[bind]))
|
|
472
|
+
else:
|
|
473
|
+
raise FastAPIReactToolkitException(f"Bind '{bind}' not found")
|
|
438
474
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
await self._create_all(connection, metadatas[key])
|
|
475
|
+
for bind, current_metadata in metadata_to_create:
|
|
476
|
+
async with self.connect(bind) as connection:
|
|
477
|
+
await self._create_all(connection, current_metadata, **kwargs)
|
|
443
478
|
|
|
444
|
-
async def drop_all(
|
|
479
|
+
async def drop_all(
|
|
480
|
+
self,
|
|
481
|
+
binds: typing.Literal["all", "default"] | str | list[str] | None = "all",
|
|
482
|
+
**kwargs,
|
|
483
|
+
):
|
|
445
484
|
"""
|
|
446
485
|
Drops all tables in the database.
|
|
447
486
|
|
|
448
487
|
Args:
|
|
449
|
-
binds (
|
|
488
|
+
binds (typing.Literal["all", "default"] | str | list[str] | None, optional): The bind keys to drop tables from. If `"default"` or `None`, only the tables for the primary database are dropped. If `"all"`, tables for all databases are dropped. If a string or list of strings, only the tables for the specified bind keys are dropped. Defaults to `"all"`.
|
|
489
|
+
**kwargs: Additional keyword arguments to pass to the `drop_all` method of the metadata.
|
|
450
490
|
"""
|
|
451
|
-
|
|
452
|
-
await self._create_all(connection, metadata, drop=True)
|
|
453
|
-
|
|
454
|
-
if not self._engine_binds or not binds:
|
|
455
|
-
return
|
|
456
|
-
|
|
457
|
-
bind_keys = self._engine_binds.keys() if binds == "all" else binds
|
|
458
|
-
for key in bind_keys:
|
|
459
|
-
async with self.connect(key) as connection:
|
|
460
|
-
await self._create_all(connection, metadatas[key], drop=True)
|
|
491
|
+
return await self.create_all(binds, drop=True, **kwargs)
|
|
461
492
|
|
|
462
493
|
async def autoload_table(self, func: Callable[[Connection], SA_Table]):
|
|
463
494
|
"""
|
|
@@ -542,7 +573,11 @@ class DatabaseSessionManager:
|
|
|
542
573
|
return scoped_session(sessionmaker)
|
|
543
574
|
|
|
544
575
|
async def _create_all(
|
|
545
|
-
self,
|
|
576
|
+
self,
|
|
577
|
+
connection: Connection | AsyncConnection,
|
|
578
|
+
metadata: MetaData,
|
|
579
|
+
drop=False,
|
|
580
|
+
**kwargs,
|
|
546
581
|
):
|
|
547
582
|
"""
|
|
548
583
|
Creates all tables in the database based on the metadata.
|
|
@@ -551,14 +586,15 @@ class DatabaseSessionManager:
|
|
|
551
586
|
connection (Connection | AsyncConnection): The database connection.
|
|
552
587
|
metadata (MetaData): The metadata object containing the tables to create.
|
|
553
588
|
drop (bool, optional): Whether to drop the tables instead of creating them. Defaults to False.
|
|
589
|
+
**kwargs: Additional keyword arguments to pass to the `create_all` or `drop_all` method of the metadata.
|
|
554
590
|
|
|
555
591
|
Returns:
|
|
556
592
|
None
|
|
557
593
|
"""
|
|
558
594
|
func = metadata.drop_all if drop else metadata.create_all
|
|
559
595
|
if isinstance(connection, AsyncConnection):
|
|
560
|
-
return await connection.run_sync(func)
|
|
561
|
-
return func(connection)
|
|
596
|
+
return await connection.run_sync(func, **kwargs)
|
|
597
|
+
return func(connection, **kwargs)
|
|
562
598
|
|
|
563
599
|
|
|
564
600
|
db = DatabaseSessionManager()
|
fastapi_rtk/decorators.py
CHANGED
fastapi_rtk/dependencies.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import fastapi
|
|
2
|
+
import sqlalchemy
|
|
2
3
|
from fastapi import Depends, HTTPException
|
|
3
4
|
|
|
4
5
|
from .api.base_api import BaseApi
|
|
5
|
-
from .const import PERMISSION_PREFIX
|
|
6
|
+
from .const import PERMISSION_PREFIX, ErrorCode, logger
|
|
7
|
+
from .db import db
|
|
6
8
|
from .globals import g
|
|
7
|
-
from .security.sqla.models import PermissionApi, User
|
|
9
|
+
from .security.sqla.models import Api, Permission, PermissionApi, Role, User
|
|
10
|
+
from .utils import smart_run
|
|
8
11
|
|
|
9
12
|
__all__ = [
|
|
10
13
|
"set_global_user",
|
|
11
|
-
"permissions",
|
|
12
14
|
"current_permissions",
|
|
13
15
|
"has_access_dependency",
|
|
14
16
|
]
|
|
@@ -54,57 +56,19 @@ def check_g_user():
|
|
|
54
56
|
|
|
55
57
|
async def check_g_user_dependency():
|
|
56
58
|
if not g.user:
|
|
57
|
-
raise HTTPException(
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
fastapi.status.HTTP_401_UNAUTHORIZED,
|
|
61
|
+
ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
|
|
62
|
+
)
|
|
58
63
|
|
|
59
64
|
return check_g_user_dependency
|
|
60
65
|
|
|
61
66
|
|
|
62
|
-
def permissions(as_object=False):
|
|
63
|
-
"""
|
|
64
|
-
A dependency for FastAPI that will return all permissions of the current user.
|
|
65
|
-
|
|
66
|
-
This will implicitly call the `current_user` dependency from `fastapi_users`. Therefore, it can return `403 Forbidden` if the user is not authenticated.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
as_object (bool): Whether to return the `PermissionApi` objects or return the api names (E.g "AuthApi" or "AuthApi|UserApi").
|
|
70
|
-
|
|
71
|
-
Usage:
|
|
72
|
-
```python
|
|
73
|
-
async def get_info(
|
|
74
|
-
*,
|
|
75
|
-
permissions: List[str] = Depends(permissions()),
|
|
76
|
-
session: AsyncSession | Session = Depends(get_async_session),
|
|
77
|
-
):
|
|
78
|
-
...more code
|
|
79
|
-
```
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
async def permissions_depedency():
|
|
83
|
-
if not g.user:
|
|
84
|
-
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
85
|
-
|
|
86
|
-
if not g.user.roles:
|
|
87
|
-
raise HTTPException(status_code=403, detail="Forbidden")
|
|
88
|
-
|
|
89
|
-
permissions = []
|
|
90
|
-
for role in g.user.roles:
|
|
91
|
-
for permission_api in role.permissions:
|
|
92
|
-
if as_object:
|
|
93
|
-
permissions.append(permission_api)
|
|
94
|
-
else:
|
|
95
|
-
permissions.append(permission_api.api.name)
|
|
96
|
-
permissions = list(set(permissions))
|
|
97
|
-
|
|
98
|
-
return permissions
|
|
99
|
-
|
|
100
|
-
return permissions_depedency
|
|
101
|
-
|
|
102
|
-
|
|
103
67
|
def current_permissions(api: BaseApi):
|
|
104
68
|
"""
|
|
105
69
|
A dependency for FastAPI that will return all permissions of the current user for the specified API.
|
|
106
70
|
|
|
107
|
-
Because it will implicitly
|
|
71
|
+
Because it will implicitly check whether the user is authenticated, it can return `401 Unauthorized` or `403 Forbidden`.
|
|
108
72
|
|
|
109
73
|
Args:
|
|
110
74
|
api (BaseApi): The API to be checked.
|
|
@@ -120,16 +84,45 @@ def current_permissions(api: BaseApi):
|
|
|
120
84
|
```
|
|
121
85
|
"""
|
|
122
86
|
|
|
123
|
-
async def current_permissions_depedency(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
permissions = []
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
87
|
+
async def current_permissions_depedency(_=Depends(_ensure_roles)):
|
|
88
|
+
sm = g.current_app.sm
|
|
89
|
+
api_name = api.__class__.__name__
|
|
90
|
+
permissions = set[str]()
|
|
91
|
+
db_role_ids = list[int]()
|
|
92
|
+
|
|
93
|
+
# Retrieve permissions from built-in roles
|
|
94
|
+
for role in g.user.roles:
|
|
95
|
+
if role.name not in sm.builtin_roles:
|
|
96
|
+
db_role_ids.append(role.id)
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
api_permission_tuples = sm.get_api_permission_tuples_from_builtin_roles(
|
|
100
|
+
role.name
|
|
101
|
+
)
|
|
102
|
+
for multi_apis_str, multi_perms_str in api_permission_tuples:
|
|
103
|
+
api_names = multi_apis_str.split("|")
|
|
104
|
+
perm_names = multi_perms_str.split("|")
|
|
105
|
+
if api_name in api_names:
|
|
106
|
+
permissions.update(perm_names)
|
|
107
|
+
|
|
108
|
+
if db_role_ids:
|
|
109
|
+
query = (
|
|
110
|
+
sqlalchemy.select(Permission)
|
|
111
|
+
.join(PermissionApi)
|
|
112
|
+
.join(PermissionApi.roles)
|
|
113
|
+
.join(Api)
|
|
114
|
+
.where(Api.name == api_name, Role.id.in_(db_role_ids))
|
|
115
|
+
)
|
|
116
|
+
if api.base_permissions:
|
|
117
|
+
query = query.where(Permission.name.in_(api.base_permissions))
|
|
118
|
+
|
|
119
|
+
permissions_in_db = await smart_run(db.current_session.scalars, query)
|
|
120
|
+
permissions.update(perm.name for perm in permissions_in_db.all())
|
|
121
|
+
|
|
130
122
|
if api.base_permissions:
|
|
131
|
-
permissions =
|
|
132
|
-
|
|
123
|
+
permissions = permissions.intersection(set(api.base_permissions))
|
|
124
|
+
|
|
125
|
+
return list(permissions)
|
|
133
126
|
|
|
134
127
|
return current_permissions_depedency
|
|
135
128
|
|
|
@@ -141,7 +134,7 @@ def has_access_dependency(
|
|
|
141
134
|
"""
|
|
142
135
|
A dependency for FastAPI to check whether current user has access to the specified API and permission.
|
|
143
136
|
|
|
144
|
-
Because it will implicitly
|
|
137
|
+
Because it will implicitly check whether the user is authenticated, it can return `401 Unauthorized` or `403 Forbidden`.
|
|
145
138
|
|
|
146
139
|
Usage:
|
|
147
140
|
```python
|
|
@@ -157,13 +150,55 @@ def has_access_dependency(
|
|
|
157
150
|
api (BaseApi): The API to be checked.
|
|
158
151
|
permission (str): The permission to check.
|
|
159
152
|
"""
|
|
153
|
+
permission = f"{PERMISSION_PREFIX}{permission}"
|
|
154
|
+
|
|
155
|
+
async def check_permission():
|
|
156
|
+
_ensure_roles(ErrorCode.PERMISSION_DENIED)
|
|
157
|
+
sm = g.current_app.sm
|
|
158
|
+
|
|
159
|
+
# First, check built-in roles (avoiding unnecessary DB queries)
|
|
160
|
+
# This also covers the case for API and permission name with pipes
|
|
161
|
+
if any(
|
|
162
|
+
sm.has_access_in_builtin_roles(
|
|
163
|
+
role.name, api.__class__.__name__, permission
|
|
164
|
+
)
|
|
165
|
+
for role in g.user.roles
|
|
166
|
+
):
|
|
167
|
+
logger.debug(
|
|
168
|
+
f"User {g.user} has access to {api.__class__.__name__} with permission {permission} via built-in roles."
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
db_role_ids = [
|
|
173
|
+
role.id for role in g.user.roles if role.name not in sm.builtin_roles
|
|
174
|
+
]
|
|
175
|
+
if not db_role_ids:
|
|
176
|
+
raise HTTPException(
|
|
177
|
+
fastapi.status.HTTP_403_FORBIDDEN, ErrorCode.PERMISSION_DENIED
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
api_name = api.__class__.__name__
|
|
181
|
+
stmt = (
|
|
182
|
+
sqlalchemy.select(sqlalchemy.literal(True))
|
|
183
|
+
.select_from(Permission)
|
|
184
|
+
.join(PermissionApi)
|
|
185
|
+
.join(PermissionApi.roles)
|
|
186
|
+
.join(Api)
|
|
187
|
+
.where(
|
|
188
|
+
Api.name == api_name,
|
|
189
|
+
Permission.name == permission,
|
|
190
|
+
Role.id.in_(db_role_ids),
|
|
191
|
+
)
|
|
192
|
+
.limit(1)
|
|
193
|
+
)
|
|
194
|
+
result = await smart_run(db.current_session.scalar, stmt)
|
|
195
|
+
result = bool(result)
|
|
196
|
+
if result:
|
|
197
|
+
return
|
|
160
198
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if f"{PERMISSION_PREFIX}{permission}" not in permissions:
|
|
165
|
-
raise HTTPException(status_code=403, detail="Forbidden")
|
|
166
|
-
return
|
|
199
|
+
raise HTTPException(
|
|
200
|
+
fastapi.status.HTTP_403_FORBIDDEN, ErrorCode.PERMISSION_DENIED
|
|
201
|
+
)
|
|
167
202
|
|
|
168
203
|
return check_permission
|
|
169
204
|
|
|
@@ -182,3 +217,40 @@ async def set_global_background_tasks(background_tasks: fastapi.BackgroundTasks)
|
|
|
182
217
|
...more code
|
|
183
218
|
"""
|
|
184
219
|
g.background_tasks = background_tasks
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def set_global_request(request: fastapi.Request):
|
|
223
|
+
"""
|
|
224
|
+
A dependency for FastAPI that will set the `request` to the global variable `g.request`.
|
|
225
|
+
|
|
226
|
+
Usage:
|
|
227
|
+
```python
|
|
228
|
+
async def get_info(
|
|
229
|
+
*,
|
|
230
|
+
session: AsyncSession | Session = Depends(get_async_session),
|
|
231
|
+
none: None = Depends(set_global_request),
|
|
232
|
+
):
|
|
233
|
+
...more code
|
|
234
|
+
"""
|
|
235
|
+
g.request = request
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _ensure_roles(err_forbidden_message=ErrorCode.GET_USER_NO_ROLES):
|
|
239
|
+
"""
|
|
240
|
+
A dependency for FastAPI that will ensure the current user has roles assigned.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
err_forbidden_message (str): The error message to be used when raising `403 Forbidden`. Defaults to `ErrorCode.GET_USER_NO_ROLES`.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
HTTPException: Raised when the user is not authenticated.
|
|
247
|
+
HTTPException: Raised when the user has no roles assigned.
|
|
248
|
+
"""
|
|
249
|
+
if not g.user:
|
|
250
|
+
raise HTTPException(
|
|
251
|
+
fastapi.status.HTTP_401_UNAUTHORIZED,
|
|
252
|
+
ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if not g.user.roles:
|
|
256
|
+
raise HTTPException(fastapi.status.HTTP_403_FORBIDDEN, err_forbidden_message)
|
fastapi_rtk/exceptions.py
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
+
import fastapi
|
|
4
|
+
import pydantic
|
|
5
|
+
|
|
3
6
|
from .utils import T
|
|
4
7
|
|
|
5
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"raise_exception",
|
|
10
|
+
"FastAPIReactToolkitException",
|
|
11
|
+
"RolesMismatchException",
|
|
12
|
+
"HTTPWithValidationException",
|
|
13
|
+
]
|
|
6
14
|
|
|
7
15
|
|
|
8
16
|
def raise_exception(message: str, return_type: typing.Type[T] | None = None) -> T:
|
|
@@ -28,3 +36,45 @@ class FastAPIReactToolkitException(Exception):
|
|
|
28
36
|
|
|
29
37
|
class RolesMismatchException(FastAPIReactToolkitException):
|
|
30
38
|
"""Exception raised when the roles do not match the expected roles."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HTTPWithValidationException(fastapi.HTTPException):
|
|
42
|
+
"""
|
|
43
|
+
Custom HTTP Exception for FastAPI with validation details.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
URL = f"https://errors.pydantic.dev/{pydantic.version.version_short()}/v"
|
|
47
|
+
_no_input = object()
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
status_code: int,
|
|
52
|
+
type: str,
|
|
53
|
+
location_type: typing.Literal["path", "query", "body"],
|
|
54
|
+
field: str,
|
|
55
|
+
msg: str,
|
|
56
|
+
input: typing.Any = _no_input,
|
|
57
|
+
headers: typing.Dict[str, str] | None = None,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize the HTTPWithValidationException.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
status_code (int): HTTP status code for the exception.
|
|
64
|
+
type (str): Type of the error (e.g., "string_type", "validation_error").
|
|
65
|
+
location_type (typing.Literal["path", "query", "body"]): Location type of the error, either "path", "query", or "body".
|
|
66
|
+
field (str): Field name where the error occurred.
|
|
67
|
+
msg (str): Error message describing the validation issue.
|
|
68
|
+
input (typing.Any, optional): Optional input value that caused the error. Defaults to _no_input.
|
|
69
|
+
headers (typing.Dict[str, str] | None, optional): Optional headers to include in the response. Defaults to None.
|
|
70
|
+
"""
|
|
71
|
+
obj = {"type": type, "loc": [location_type, field], "msg": msg}
|
|
72
|
+
if input is not self._no_input:
|
|
73
|
+
if isinstance(input, pydantic.BaseModel):
|
|
74
|
+
input = input.model_dump(mode="json")
|
|
75
|
+
else:
|
|
76
|
+
input = str(input)
|
|
77
|
+
obj["input"] = input
|
|
78
|
+
obj["url"] = f"{self.URL}/{type}"
|
|
79
|
+
|
|
80
|
+
super().__init__(status_code, [obj], headers)
|