fastapi-rbac-authz 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/CLAUDE.md +9 -8
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/PKG-INFO +28 -21
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/README.md +27 -20
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/examples/basic_app.py +21 -8
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/__init__.py +2 -10
- fastapi_rbac_authz-0.4.0/fastapi_rbac/context.py +34 -0
- fastapi_rbac_authz-0.4.0/fastapi_rbac/core.py +85 -0
- fastapi_rbac_authz-0.4.0/fastapi_rbac/dependencies.py +128 -0
- fastapi_rbac_authz-0.3.0/fastapi_rbac/exceptions.py → fastapi_rbac_authz-0.4.0/fastapi_rbac/errors.py +2 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/permissions.py +3 -1
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/router.py +24 -70
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/routes.py +2 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/schema.py +3 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/pyproject.toml +1 -1
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_context.py +4 -4
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_context_di.py +30 -32
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_core.py +19 -27
- fastapi_rbac_authz-0.4.0/tests/test_dependencies.py +319 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_integration.py +31 -31
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_router.py +75 -94
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_ui.py +20 -20
- fastapi_rbac_authz-0.3.0/fastapi_rbac/context.py +0 -39
- fastapi_rbac_authz-0.3.0/fastapi_rbac/core.py +0 -111
- fastapi_rbac_authz-0.3.0/fastapi_rbac/dependencies.py +0 -254
- fastapi_rbac_authz-0.3.0/tests/test_dependencies.py +0 -431
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/.github/workflows/checks.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/.github/workflows/pypi-minor-deployment.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/.gitignore +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/.pre-commit-config.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/py.typed +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/__init__.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/static/index.html +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/poetry.lock +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/__init__.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/conftest.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_exceptions.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.4.0}/tests/test_permissions.py +0 -0
|
@@ -30,6 +30,7 @@ poetry build
|
|
|
30
30
|
|
|
31
31
|
**IMPORTANT:** Always run pre-commit after making any code changes:
|
|
32
32
|
```bash
|
|
33
|
+
source .venv/bin/activate
|
|
33
34
|
pre-commit run --all-files
|
|
34
35
|
```
|
|
35
36
|
This runs ruff (linting + formatting) and mypy (type checking on fastapi_rbac).
|
|
@@ -38,29 +39,29 @@ This runs ruff (linting + formatting) and mypy (type checking on fastapi_rbac).
|
|
|
38
39
|
|
|
39
40
|
### Core Components
|
|
40
41
|
|
|
41
|
-
**RBACAuthz
|
|
42
|
+
**RBACAuthz** (`core.py`) - Main configuration class that attaches to FastAPI app. Configures role-to-permission mappings, roles dependency injection, and optional visualization UI. Takes a `roles_dependency` that returns `set[str]`.
|
|
42
43
|
|
|
43
44
|
**RBACRouter** (`router.py`) - Extended APIRouter with permission decorators (`@router.get(permissions=..., contexts=...)`). Supports default permissions at router level with per-endpoint overrides. Stores endpoint metadata for UI introspection.
|
|
44
45
|
|
|
45
46
|
**Permission System** (`permissions.py`) - Two scopes: `Global` (bypasses context checks) and `Contextual` (requires context validation). Supports wildcard matching (`resource:*` matches `resource:read`). Wildcards only allowed in role grants, not endpoint requirements.
|
|
46
47
|
|
|
47
|
-
**ContextualAuthz
|
|
48
|
+
**ContextualAuthz** (`context.py`) - Abstract base class for context-specific authorization. Subclasses are FastAPI dependencies that implement `async has_permissions() -> bool`. Supports full FastAPI DI in `__init__`. Context classes are responsible for their own authentication via `Annotated[User, Depends(get_current_user)]`.
|
|
48
49
|
|
|
49
|
-
**Dependencies** (`dependencies.py`) - Creates FastAPI dependencies for auth and authz. Uses `dependency_overrides` pattern for
|
|
50
|
+
**Dependencies** (`dependencies.py`) - Creates FastAPI dependencies for auth and authz. Uses `dependency_overrides` pattern for roles dependency injection. Manipulates function signatures dynamically via `inspect.Signature`.
|
|
50
51
|
|
|
51
52
|
**UI System** (`ui/`) - Cytoscape.js visualization mounted at configurable path. Introspects RBAC configuration to display role → permission → endpoint ← context relationships.
|
|
52
53
|
|
|
53
54
|
### Key Patterns
|
|
54
55
|
|
|
55
|
-
- **Request State**:
|
|
56
|
+
- **Request State**: RBAC config in `app.state.rbac`, routers in `app.state.rbac.routers`
|
|
56
57
|
- **Metadata Tracking**: Endpoints store `_rbac_metadata_` attribute; routers track `endpoint_metadata[(path, method)]`
|
|
57
|
-
- **Permission Resolution**:
|
|
58
|
-
- **
|
|
58
|
+
- **Permission Resolution**: Roles dependency returns roles → resolve grants → check global permissions first → run contextual checks only if needed
|
|
59
|
+
- **Simplified Auth**: The library only needs roles (`set[str]`), not a user object. Context classes handle their own auth via FastAPI DI.
|
|
59
60
|
|
|
60
61
|
### Public API (from `__init__.py`)
|
|
61
62
|
|
|
62
63
|
```python
|
|
63
|
-
RBACAuthz, RBACRouter,
|
|
64
|
+
RBACAuthz, RBACRouter, ContextualAuthz
|
|
64
65
|
Global, Contextual, PermissionGrant, PermissionScope
|
|
65
|
-
Forbidden,
|
|
66
|
+
Forbidden, create_authz_dependency
|
|
66
67
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-rbac-authz
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Role-based access control with contextual authorization for FastAPI
|
|
5
5
|
Project-URL: Homepage, https://github.com/parikls/fastapi-rbac
|
|
6
6
|
Author-email: Dmytro Smyk <porovozls@gmail.com>
|
|
@@ -36,7 +36,7 @@ pip install fastapi-rbac-authz
|
|
|
36
36
|
from typing import Annotated
|
|
37
37
|
from fastapi import Depends, FastAPI
|
|
38
38
|
from fastapi_rbac import (
|
|
39
|
-
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
39
|
+
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
# 1. Define your user model
|
|
@@ -45,7 +45,17 @@ class User:
|
|
|
45
45
|
self.user_id = user_id
|
|
46
46
|
self.roles = roles
|
|
47
47
|
|
|
48
|
-
# 2. Define
|
|
48
|
+
# 2. Define your authentication dependencies
|
|
49
|
+
# The library only needs roles - you can authenticate however you like
|
|
50
|
+
async def get_current_user() -> User:
|
|
51
|
+
# Your authentication logic here
|
|
52
|
+
return User(user_id="user-1", roles={"viewer"})
|
|
53
|
+
|
|
54
|
+
async def get_current_user_roles() -> set[str]:
|
|
55
|
+
user = await get_current_user()
|
|
56
|
+
return user.roles
|
|
57
|
+
|
|
58
|
+
# 3. Define role permissions
|
|
49
59
|
PERMISSIONS = {
|
|
50
60
|
"admin": {
|
|
51
61
|
Global("report:*"), # Admin can do anything with reports
|
|
@@ -55,13 +65,13 @@ PERMISSIONS = {
|
|
|
55
65
|
},
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
class ReportAccessContext(ContextualAuthz
|
|
68
|
+
# 4. Create a context check (for contextual permissions)
|
|
69
|
+
# Context classes are responsible for their own authentication via FastAPI DI
|
|
70
|
+
class ReportAccessContext(ContextualAuthz):
|
|
61
71
|
def __init__(
|
|
62
72
|
self,
|
|
63
73
|
report_id: int, # <-- Injected from path parameter
|
|
64
|
-
user: Annotated[User, Depends(
|
|
74
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
65
75
|
):
|
|
66
76
|
self.user = user
|
|
67
77
|
self.report_id = report_id
|
|
@@ -71,22 +81,17 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
71
81
|
allowed_reports = {1, 2, 3} # e.g., query from database
|
|
72
82
|
return self.report_id in allowed_reports
|
|
73
83
|
|
|
74
|
-
#
|
|
84
|
+
# 5. Create your app and configure RBAC
|
|
75
85
|
app = FastAPI()
|
|
76
86
|
|
|
77
|
-
async def get_current_user() -> User:
|
|
78
|
-
# Your authentication logic here
|
|
79
|
-
return User(user_id="user-1", roles={"viewer"})
|
|
80
|
-
|
|
81
87
|
RBACAuthz(
|
|
82
88
|
app,
|
|
83
|
-
get_roles=lambda u: u.roles,
|
|
84
89
|
permissions=PERMISSIONS,
|
|
85
|
-
|
|
90
|
+
roles_dependency=get_current_user_roles, # Returns set[str]
|
|
86
91
|
ui_path="/_rbac", # Optional: mount visualization UI
|
|
87
92
|
)
|
|
88
93
|
|
|
89
|
-
#
|
|
94
|
+
# 6. Create protected routes
|
|
90
95
|
router = RBACRouter(permissions={"report:read"}, contexts=[ReportAccessContext])
|
|
91
96
|
|
|
92
97
|
@router.get("/reports/{report_id}")
|
|
@@ -100,6 +105,8 @@ async def create_report():
|
|
|
100
105
|
app.include_router(router, prefix="/api")
|
|
101
106
|
```
|
|
102
107
|
|
|
108
|
+
> **Note:** The library doesn't care how you authenticate - whether via dependency, middleware, JWT decode, or any other method. You just need to provide a dependency that returns the user's roles as `set[str]`. Context classes are responsible for their own authentication and can use any FastAPI dependency pattern.
|
|
109
|
+
|
|
103
110
|
## Permission Scopes
|
|
104
111
|
|
|
105
112
|
Permissions can be granted with two scopes:
|
|
@@ -136,14 +143,14 @@ PERMISSIONS = {
|
|
|
136
143
|
|
|
137
144
|
## Context Checks
|
|
138
145
|
|
|
139
|
-
Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.).
|
|
146
|
+
Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.). Each context class is responsible for its own authentication via FastAPI's dependency injection.
|
|
140
147
|
|
|
141
148
|
```python
|
|
142
|
-
class ReportAccessContext(ContextualAuthz
|
|
149
|
+
class ReportAccessContext(ContextualAuthz):
|
|
143
150
|
def __init__(
|
|
144
151
|
self,
|
|
145
152
|
report_id: int, # Injected from path parameter
|
|
146
|
-
user: Annotated[User, Depends(
|
|
153
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
147
154
|
db: Annotated[AsyncSession, Depends(get_db)], # Database session
|
|
148
155
|
):
|
|
149
156
|
self.user = user
|
|
@@ -161,8 +168,8 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
161
168
|
When a request hits an RBAC-protected endpoint:
|
|
162
169
|
|
|
163
170
|
```
|
|
164
|
-
1.
|
|
165
|
-
└──
|
|
171
|
+
1. Role Resolution
|
|
172
|
+
└── roles_dependency runs → User's roles (set[str]) available
|
|
166
173
|
|
|
167
174
|
2. Permission Check
|
|
168
175
|
└── Does user have ANY grant (global or contextual) for required permission?
|
|
@@ -175,7 +182,7 @@ When a request hits an RBAC-protected endpoint:
|
|
|
175
182
|
└── No → Continue to context checks
|
|
176
183
|
|
|
177
184
|
4. Context Checks (only for Contextual grants)
|
|
178
|
-
└── Run all context classes via FastAPI DI
|
|
185
|
+
└── Run all context classes via FastAPI DI (each gets its own user via Depends)
|
|
179
186
|
└── Do ALL contexts return True?
|
|
180
187
|
├── No → 403 Forbidden
|
|
181
188
|
└── Yes → Access granted
|
|
@@ -14,7 +14,7 @@ pip install fastapi-rbac-authz
|
|
|
14
14
|
from typing import Annotated
|
|
15
15
|
from fastapi import Depends, FastAPI
|
|
16
16
|
from fastapi_rbac import (
|
|
17
|
-
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
17
|
+
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
# 1. Define your user model
|
|
@@ -23,7 +23,17 @@ class User:
|
|
|
23
23
|
self.user_id = user_id
|
|
24
24
|
self.roles = roles
|
|
25
25
|
|
|
26
|
-
# 2. Define
|
|
26
|
+
# 2. Define your authentication dependencies
|
|
27
|
+
# The library only needs roles - you can authenticate however you like
|
|
28
|
+
async def get_current_user() -> User:
|
|
29
|
+
# Your authentication logic here
|
|
30
|
+
return User(user_id="user-1", roles={"viewer"})
|
|
31
|
+
|
|
32
|
+
async def get_current_user_roles() -> set[str]:
|
|
33
|
+
user = await get_current_user()
|
|
34
|
+
return user.roles
|
|
35
|
+
|
|
36
|
+
# 3. Define role permissions
|
|
27
37
|
PERMISSIONS = {
|
|
28
38
|
"admin": {
|
|
29
39
|
Global("report:*"), # Admin can do anything with reports
|
|
@@ -33,13 +43,13 @@ PERMISSIONS = {
|
|
|
33
43
|
},
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
class ReportAccessContext(ContextualAuthz
|
|
46
|
+
# 4. Create a context check (for contextual permissions)
|
|
47
|
+
# Context classes are responsible for their own authentication via FastAPI DI
|
|
48
|
+
class ReportAccessContext(ContextualAuthz):
|
|
39
49
|
def __init__(
|
|
40
50
|
self,
|
|
41
51
|
report_id: int, # <-- Injected from path parameter
|
|
42
|
-
user: Annotated[User, Depends(
|
|
52
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
43
53
|
):
|
|
44
54
|
self.user = user
|
|
45
55
|
self.report_id = report_id
|
|
@@ -49,22 +59,17 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
49
59
|
allowed_reports = {1, 2, 3} # e.g., query from database
|
|
50
60
|
return self.report_id in allowed_reports
|
|
51
61
|
|
|
52
|
-
#
|
|
62
|
+
# 5. Create your app and configure RBAC
|
|
53
63
|
app = FastAPI()
|
|
54
64
|
|
|
55
|
-
async def get_current_user() -> User:
|
|
56
|
-
# Your authentication logic here
|
|
57
|
-
return User(user_id="user-1", roles={"viewer"})
|
|
58
|
-
|
|
59
65
|
RBACAuthz(
|
|
60
66
|
app,
|
|
61
|
-
get_roles=lambda u: u.roles,
|
|
62
67
|
permissions=PERMISSIONS,
|
|
63
|
-
|
|
68
|
+
roles_dependency=get_current_user_roles, # Returns set[str]
|
|
64
69
|
ui_path="/_rbac", # Optional: mount visualization UI
|
|
65
70
|
)
|
|
66
71
|
|
|
67
|
-
#
|
|
72
|
+
# 6. Create protected routes
|
|
68
73
|
router = RBACRouter(permissions={"report:read"}, contexts=[ReportAccessContext])
|
|
69
74
|
|
|
70
75
|
@router.get("/reports/{report_id}")
|
|
@@ -78,6 +83,8 @@ async def create_report():
|
|
|
78
83
|
app.include_router(router, prefix="/api")
|
|
79
84
|
```
|
|
80
85
|
|
|
86
|
+
> **Note:** The library doesn't care how you authenticate - whether via dependency, middleware, JWT decode, or any other method. You just need to provide a dependency that returns the user's roles as `set[str]`. Context classes are responsible for their own authentication and can use any FastAPI dependency pattern.
|
|
87
|
+
|
|
81
88
|
## Permission Scopes
|
|
82
89
|
|
|
83
90
|
Permissions can be granted with two scopes:
|
|
@@ -114,14 +121,14 @@ PERMISSIONS = {
|
|
|
114
121
|
|
|
115
122
|
## Context Checks
|
|
116
123
|
|
|
117
|
-
Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.).
|
|
124
|
+
Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.). Each context class is responsible for its own authentication via FastAPI's dependency injection.
|
|
118
125
|
|
|
119
126
|
```python
|
|
120
|
-
class ReportAccessContext(ContextualAuthz
|
|
127
|
+
class ReportAccessContext(ContextualAuthz):
|
|
121
128
|
def __init__(
|
|
122
129
|
self,
|
|
123
130
|
report_id: int, # Injected from path parameter
|
|
124
|
-
user: Annotated[User, Depends(
|
|
131
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
125
132
|
db: Annotated[AsyncSession, Depends(get_db)], # Database session
|
|
126
133
|
):
|
|
127
134
|
self.user = user
|
|
@@ -139,8 +146,8 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
139
146
|
When a request hits an RBAC-protected endpoint:
|
|
140
147
|
|
|
141
148
|
```
|
|
142
|
-
1.
|
|
143
|
-
└──
|
|
149
|
+
1. Role Resolution
|
|
150
|
+
└── roles_dependency runs → User's roles (set[str]) available
|
|
144
151
|
|
|
145
152
|
2. Permission Check
|
|
146
153
|
└── Does user have ANY grant (global or contextual) for required permission?
|
|
@@ -153,7 +160,7 @@ When a request hits an RBAC-protected endpoint:
|
|
|
153
160
|
└── No → Continue to context checks
|
|
154
161
|
|
|
155
162
|
4. Context Checks (only for Contextual grants)
|
|
156
|
-
└── Run all context classes via FastAPI DI
|
|
163
|
+
└── Run all context classes via FastAPI DI (each gets its own user via Depends)
|
|
157
164
|
└── Do ALL contexts return True?
|
|
158
165
|
├── No → 403 Forbidden
|
|
159
166
|
└── Yes → Access granted
|
|
@@ -20,7 +20,6 @@ from fastapi_rbac import (
|
|
|
20
20
|
Global,
|
|
21
21
|
RBACAuthz,
|
|
22
22
|
RBACRouter,
|
|
23
|
-
RBACUser,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
|
|
@@ -67,8 +66,10 @@ PERMISSIONS = {
|
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
# =============================================================================
|
|
70
|
-
# Authentication
|
|
69
|
+
# Authentication Dependencies
|
|
71
70
|
# =============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
72
73
|
async def get_current_user(x_token: Annotated[str, Header()]) -> User:
|
|
73
74
|
"""Simulate authentication via X-Token header."""
|
|
74
75
|
user = USERS.get(x_token)
|
|
@@ -77,16 +78,29 @@ async def get_current_user(x_token: Annotated[str, Header()]) -> User:
|
|
|
77
78
|
return user
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
async def get_current_user_roles(user: User = Depends(get_current_user)) -> set[str]:
|
|
82
|
+
"""Get the roles for the current user.
|
|
83
|
+
|
|
84
|
+
This dependency returns only the roles - the library doesn't need
|
|
85
|
+
the full user object for authorization checks.
|
|
86
|
+
"""
|
|
87
|
+
return user.roles
|
|
88
|
+
|
|
89
|
+
|
|
80
90
|
# =============================================================================
|
|
81
91
|
# Context Check
|
|
82
92
|
# =============================================================================
|
|
83
|
-
class ReportOwnerContext(ContextualAuthz
|
|
84
|
-
"""Check if user owns the report or is allowed to access it.
|
|
93
|
+
class ReportOwnerContext(ContextualAuthz):
|
|
94
|
+
"""Check if user owns the report or is allowed to access it.
|
|
95
|
+
|
|
96
|
+
Context classes are responsible for their own authentication.
|
|
97
|
+
They use FastAPI's Depends() to get the user via your auth dependency.
|
|
98
|
+
"""
|
|
85
99
|
|
|
86
100
|
def __init__(
|
|
87
101
|
self,
|
|
88
102
|
report_id: int,
|
|
89
|
-
user: Annotated[User, Depends(
|
|
103
|
+
user: Annotated[User, Depends(get_current_user)],
|
|
90
104
|
):
|
|
91
105
|
self.user = user
|
|
92
106
|
self.report_id = report_id
|
|
@@ -109,9 +123,8 @@ app = FastAPI(
|
|
|
109
123
|
|
|
110
124
|
RBACAuthz(
|
|
111
125
|
app,
|
|
112
|
-
get_roles=lambda user: user.roles,
|
|
113
126
|
permissions=PERMISSIONS,
|
|
114
|
-
|
|
127
|
+
roles_dependency=get_current_user_roles,
|
|
115
128
|
ui_path="/_rbac",
|
|
116
129
|
)
|
|
117
130
|
|
|
@@ -147,7 +160,7 @@ async def update_report(report_id: int, title: str):
|
|
|
147
160
|
|
|
148
161
|
|
|
149
162
|
@router.post("", permissions={"report:create"}, contexts=[])
|
|
150
|
-
async def create_report(title: str, user: Annotated[User, Depends(
|
|
163
|
+
async def create_report(title: str, user: Annotated[User, Depends(get_current_user)]):
|
|
151
164
|
"""Create a new report. Requires report:create permission (no context check)."""
|
|
152
165
|
new_id = max(REPORTS.keys()) + 1
|
|
153
166
|
REPORTS[new_id] = {"title": title, "owner_id": user.user_id}
|
|
@@ -4,13 +4,8 @@ __version__ = "0.1.0"
|
|
|
4
4
|
|
|
5
5
|
from fastapi_rbac.context import ContextualAuthz
|
|
6
6
|
from fastapi_rbac.core import RBACAuthz
|
|
7
|
-
from fastapi_rbac.dependencies import
|
|
8
|
-
|
|
9
|
-
create_auth_dependency,
|
|
10
|
-
create_authz_dependency,
|
|
11
|
-
evaluate_permissions,
|
|
12
|
-
)
|
|
13
|
-
from fastapi_rbac.exceptions import Forbidden
|
|
7
|
+
from fastapi_rbac.dependencies import create_authz_dependency
|
|
8
|
+
from fastapi_rbac.errors import Forbidden
|
|
14
9
|
from fastapi_rbac.permissions import (
|
|
15
10
|
Contextual,
|
|
16
11
|
Global,
|
|
@@ -22,14 +17,11 @@ from fastapi_rbac.router import RBACRouter
|
|
|
22
17
|
__all__ = [
|
|
23
18
|
"RBACAuthz",
|
|
24
19
|
"RBACRouter",
|
|
25
|
-
"RBACUser",
|
|
26
20
|
"ContextualAuthz",
|
|
27
21
|
"Global",
|
|
28
22
|
"Contextual",
|
|
29
23
|
"PermissionGrant",
|
|
30
24
|
"PermissionScope",
|
|
31
25
|
"Forbidden",
|
|
32
|
-
"create_auth_dependency",
|
|
33
26
|
"create_authz_dependency",
|
|
34
|
-
"evaluate_permissions",
|
|
35
27
|
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContextualAuthz(ABC):
|
|
5
|
+
"""Base class for contextual authorization checks.
|
|
6
|
+
|
|
7
|
+
Subclasses can use FastAPI dependencies in the __init__ method.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
>>> class MyContext(ContextualAuthz):
|
|
12
|
+
>>> def __init__(
|
|
13
|
+
>>> self,
|
|
14
|
+
>>> user: Annotated[User, Depends(get_current_user)], # your auth dep
|
|
15
|
+
>>> request: Request, # fastapi dep
|
|
16
|
+
>>> db: AsyncSession = Depends(get_db), # your database dep
|
|
17
|
+
>>> ):
|
|
18
|
+
>>> self.user = user
|
|
19
|
+
>>> self.request = request
|
|
20
|
+
>>> self.db = db
|
|
21
|
+
>>>
|
|
22
|
+
>>> async def has_permissions(self) -> bool:
|
|
23
|
+
>>> # Check access using self.user, self.request, self.db
|
|
24
|
+
>>> return True
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def has_permissions(self) -> bool:
|
|
29
|
+
"""Check if the user has permission in this context.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if access should be granted, False otherwise.
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, FastAPI
|
|
5
|
+
|
|
6
|
+
from fastapi_rbac.dependencies import _rbac_roles_dependency_placeholder
|
|
7
|
+
from fastapi_rbac.permissions import PermissionGrant
|
|
8
|
+
from fastapi_rbac.router import RBACRouter
|
|
9
|
+
from fastapi_rbac.ui.routes import create_ui_router
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RBACAuthz:
|
|
13
|
+
"""Main RBAC authorization configuration.
|
|
14
|
+
|
|
15
|
+
Attaches to a FastAPI application and provides authorization
|
|
16
|
+
configuration for RBACRouter endpoints.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
app: The FastAPI application instance.
|
|
20
|
+
permissions: Mapping of role names to sets of permission grants.
|
|
21
|
+
roles_dependency: FastAPI dependency that returns the user's roles as set[str].
|
|
22
|
+
This dependency is injected into all RBAC-protected endpoints via
|
|
23
|
+
FastAPI's dependency_overrides mechanism, and will be evaluated before RBAC checks
|
|
24
|
+
ui_path: Optional path to mount the authorization UI (e.g., "/_rbac").
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
app: FastAPI,
|
|
30
|
+
permissions: dict[str, set[PermissionGrant]],
|
|
31
|
+
roles_dependency: Callable[..., set[str]] | Callable[..., Awaitable[set[str]]],
|
|
32
|
+
ui_path: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.app = app
|
|
35
|
+
self.permissions = permissions
|
|
36
|
+
self.roles_dependency = roles_dependency
|
|
37
|
+
self.ui_path = ui_path
|
|
38
|
+
|
|
39
|
+
# Instance attributes for state management
|
|
40
|
+
self.routers: list[tuple[str, RBACRouter]] = []
|
|
41
|
+
self._include_router_wrapped: bool = False
|
|
42
|
+
|
|
43
|
+
# Attach to app state for access from routers
|
|
44
|
+
app.state.rbac = self
|
|
45
|
+
|
|
46
|
+
# Override the placeholder dependency with user's roles dependency
|
|
47
|
+
# This allows the roles dependency to be injected into all
|
|
48
|
+
# RBAC-protected endpoints with proper FastAPI dependency resolution
|
|
49
|
+
app.dependency_overrides[_rbac_roles_dependency_placeholder] = roles_dependency
|
|
50
|
+
self._wrap_app_include_router()
|
|
51
|
+
|
|
52
|
+
if ui_path:
|
|
53
|
+
self._mount_ui()
|
|
54
|
+
|
|
55
|
+
def _wrap_app_include_router(self) -> None:
|
|
56
|
+
"""Wrap the app's include_router method to track RBACRouters."""
|
|
57
|
+
if self._include_router_wrapped:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
original_include_router = self.app.include_router
|
|
61
|
+
self.app.include_router = self._create_wrapped_include_router(original_include_router) # type: ignore[method-assign]
|
|
62
|
+
self._include_router_wrapped = True
|
|
63
|
+
|
|
64
|
+
def _create_wrapped_include_router(self, original_include_router: Callable[..., None]) -> Callable[..., None]:
|
|
65
|
+
"""Create a wrapped include_router that tracks RBACRouters."""
|
|
66
|
+
|
|
67
|
+
def wrapped_include_router(
|
|
68
|
+
router: APIRouter,
|
|
69
|
+
*,
|
|
70
|
+
prefix: str = "",
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> None:
|
|
73
|
+
if isinstance(router, RBACRouter):
|
|
74
|
+
self.routers.append((prefix, router))
|
|
75
|
+
return original_include_router(router, prefix=prefix, **kwargs)
|
|
76
|
+
|
|
77
|
+
return wrapped_include_router
|
|
78
|
+
|
|
79
|
+
def _mount_ui(self) -> None:
|
|
80
|
+
"""Mount the authorization visualization UI."""
|
|
81
|
+
if not self.ui_path:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
ui_router = create_ui_router(self.ui_path)
|
|
85
|
+
self.app.include_router(ui_router, prefix=self.ui_path)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable, Coroutine
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, Request
|
|
6
|
+
|
|
7
|
+
from fastapi_rbac.context import ContextualAuthz
|
|
8
|
+
from fastapi_rbac.errors import Forbidden
|
|
9
|
+
from fastapi_rbac.permissions import (
|
|
10
|
+
has_global_permission,
|
|
11
|
+
has_permission,
|
|
12
|
+
resolve_grants,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Type alias for context classes
|
|
16
|
+
ContextClass = type[ContextualAuthz]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _rbac_roles_dependency_placeholder(request: Request) -> set[str]: # noqa: ARG001
|
|
20
|
+
"""Placeholder dependency for user roles.
|
|
21
|
+
|
|
22
|
+
This placeholder is replaced at runtime via FastAPI's dependency_overrides
|
|
23
|
+
mechanism when RBACAuthz is initialized. The roles_dependency provided to
|
|
24
|
+
RBACAuthz will be used instead of this placeholder.
|
|
25
|
+
"""
|
|
26
|
+
raise RuntimeError("RBACAuthz not configured with roles_dependency")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_authz_dependency(
|
|
30
|
+
required_permissions: set[str],
|
|
31
|
+
context_classes: list[ContextClass],
|
|
32
|
+
) -> Callable[..., Coroutine[Any, Any, None]]:
|
|
33
|
+
"""Create an authorization dependency that uses FastAPI's full DI for contexts.
|
|
34
|
+
|
|
35
|
+
This function creates a FastAPI dependency that:
|
|
36
|
+
1. Resolves a list of user roles via the injected roles_dependency
|
|
37
|
+
2. Gets RBAC config from app.state.rbac
|
|
38
|
+
3. Uses FastAPI's Depends(context_class) to instantiate each context
|
|
39
|
+
- Context classes can use ANY FastAPI dependency patterns in __init__:
|
|
40
|
+
- user: AuthUser (resolved via Depends)
|
|
41
|
+
- request: Request (built-in)
|
|
42
|
+
- org_id: str (from path parameter)
|
|
43
|
+
- body: SomeModel (from request body)
|
|
44
|
+
- db: Db (via Depends)
|
|
45
|
+
4. Calls has_permissions() on each resolved context
|
|
46
|
+
|
|
47
|
+
The roles_dependency is injected via FastAPI's dependency_overrides mechanism.
|
|
48
|
+
When RBACAuthz is initialized with a roles_dependency, it overrides the
|
|
49
|
+
_rbac_roles_dependency_placeholder with the provided dependency.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
required_permissions: Set of permission strings required for access.
|
|
53
|
+
context_classes: List of ContextualAuthz subclasses to check.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
An async dependency function for use with FastAPI's Depends().
|
|
57
|
+
"""
|
|
58
|
+
# Build context parameters - each context class becomes a Depends(context_class)
|
|
59
|
+
# FastAPI will resolve all __init__ parameters automatically
|
|
60
|
+
if not required_permissions and not context_classes:
|
|
61
|
+
raise RuntimeError("Endpoint must be protected with either permissions or contexts")
|
|
62
|
+
|
|
63
|
+
context_params: list[inspect.Parameter] = []
|
|
64
|
+
for i, ctx_class in enumerate(context_classes):
|
|
65
|
+
param = inspect.Parameter(
|
|
66
|
+
f"_fastapi_rbac_authz_ctx_{i}_",
|
|
67
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
68
|
+
default=None,
|
|
69
|
+
annotation=Annotated[ctx_class, Depends(ctx_class)],
|
|
70
|
+
)
|
|
71
|
+
context_params.append(param)
|
|
72
|
+
|
|
73
|
+
async def authz_dependency(
|
|
74
|
+
request: Request,
|
|
75
|
+
_rbac_roles_: Annotated[set[str], Depends(_rbac_roles_dependency_placeholder)],
|
|
76
|
+
**kwargs: Any,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Authorization dependency that checks permissions and contexts."""
|
|
79
|
+
rbac = getattr(request.app.state, "rbac", None)
|
|
80
|
+
if rbac is None:
|
|
81
|
+
raise RuntimeError("RBACAuthz not configured. Make sure to create an RBACAuthz instance with your app.")
|
|
82
|
+
|
|
83
|
+
roles = _rbac_roles_
|
|
84
|
+
if not roles:
|
|
85
|
+
raise Forbidden("User has no roles")
|
|
86
|
+
|
|
87
|
+
grants = resolve_grants(roles, rbac.permissions)
|
|
88
|
+
|
|
89
|
+
if not grants:
|
|
90
|
+
raise Forbidden()
|
|
91
|
+
|
|
92
|
+
need_contextual_check = not required_permissions
|
|
93
|
+
|
|
94
|
+
for required in required_permissions:
|
|
95
|
+
if has_global_permission(grants, required):
|
|
96
|
+
# Global permission - no need for contextual check for this permission
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
need_contextual_check = True
|
|
100
|
+
if not has_permission(grants, required):
|
|
101
|
+
raise Forbidden()
|
|
102
|
+
|
|
103
|
+
# Run contextual checks if needed
|
|
104
|
+
# Context instances are already resolved by FastAPI via Depends(context_class)
|
|
105
|
+
if need_contextual_check:
|
|
106
|
+
for i in range(len(context_classes)):
|
|
107
|
+
context = kwargs.get(f"_fastapi_rbac_authz_ctx_{i}_")
|
|
108
|
+
if context is not None and not await context.has_permissions():
|
|
109
|
+
raise Forbidden()
|
|
110
|
+
|
|
111
|
+
# Build the final signature with context params
|
|
112
|
+
base_params = [
|
|
113
|
+
inspect.Parameter(
|
|
114
|
+
"request",
|
|
115
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
116
|
+
annotation=Request,
|
|
117
|
+
),
|
|
118
|
+
inspect.Parameter(
|
|
119
|
+
"_rbac_roles_",
|
|
120
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
121
|
+
default=None,
|
|
122
|
+
annotation=Annotated[set[str], Depends(_rbac_roles_dependency_placeholder)],
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
new_sig = inspect.Signature(parameters=base_params + context_params)
|
|
127
|
+
authz_dependency.__signature__ = new_sig # type: ignore[attr-defined]
|
|
128
|
+
return authz_dependency
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from fastapi import HTTPException
|
|
2
|
+
from starlette.status import HTTP_403_FORBIDDEN
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Forbidden(HTTPException):
|
|
5
|
-
"""403 Forbidden - user lacks required permissions."""
|
|
6
|
-
|
|
7
6
|
def __init__(self, detail: str = "Forbidden") -> None:
|
|
8
|
-
super().__init__(status_code=
|
|
7
|
+
super().__init__(status_code=HTTP_403_FORBIDDEN, detail=detail)
|
|
@@ -23,9 +23,11 @@ class PermissionGrant:
|
|
|
23
23
|
def __hash__(self) -> int:
|
|
24
24
|
return hash((self.permission, self.scope))
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def __str__(self) -> str:
|
|
27
27
|
return f"{self.__class__.__name__}({self.permission!r})"
|
|
28
28
|
|
|
29
|
+
__repr__ = __str__
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
class Global(PermissionGrant):
|
|
31
33
|
"""A global permission grant - bypasses contextual checks."""
|