fastapi-rbac-authz 0.3.0__tar.gz → 0.5.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.5.0}/CLAUDE.md +9 -8
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/PKG-INFO +42 -30
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/README.md +41 -29
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/examples/basic_app.py +21 -8
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/__init__.py +2 -10
- fastapi_rbac_authz-0.5.0/fastapi_rbac/context.py +34 -0
- fastapi_rbac_authz-0.5.0/fastapi_rbac/core.py +85 -0
- fastapi_rbac_authz-0.5.0/fastapi_rbac/dependencies.py +128 -0
- fastapi_rbac_authz-0.3.0/fastapi_rbac/exceptions.py → fastapi_rbac_authz-0.5.0/fastapi_rbac/errors.py +2 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/permissions.py +3 -1
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/router.py +24 -70
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/routes.py +2 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/schema.py +3 -3
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/pyproject.toml +1 -1
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_context.py +4 -4
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_context_di.py +30 -32
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_core.py +19 -27
- fastapi_rbac_authz-0.5.0/tests/test_dependencies.py +319 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_integration.py +31 -31
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_router.py +75 -94
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.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.5.0}/.github/workflows/checks.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.github/workflows/pypi-minor-deployment.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.gitignore +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.pre-commit-config.yaml +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/py.typed +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/__init__.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/static/index.html +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/poetry.lock +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/__init__.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/conftest.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_exceptions.py +0 -0
- {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.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.5.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>
|
|
@@ -24,6 +24,14 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
Role-based access control with contextual authorization for FastAPI.
|
|
26
26
|
|
|
27
|
+
Library IS NOT responsible for authentication. You can use any authentication mechanism you want.
|
|
28
|
+
|
|
29
|
+
What you need to provide:
|
|
30
|
+
- A dependency that returns the authenticated user's roles as `set[str]`
|
|
31
|
+
- Permission definitions per role
|
|
32
|
+
- Use `RBACRouter` instead of `APIRouter` for protected routes
|
|
33
|
+
- Define permissions and context checks per endpoint
|
|
34
|
+
|
|
27
35
|
## Installation
|
|
28
36
|
|
|
29
37
|
```bash
|
|
@@ -36,16 +44,26 @@ pip install fastapi-rbac-authz
|
|
|
36
44
|
from typing import Annotated
|
|
37
45
|
from fastapi import Depends, FastAPI
|
|
38
46
|
from fastapi_rbac import (
|
|
39
|
-
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
47
|
+
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
40
48
|
)
|
|
41
49
|
|
|
42
|
-
# 1.
|
|
50
|
+
# 1. You might have your user model (or you may not, we don't care)
|
|
43
51
|
class User:
|
|
44
52
|
def __init__(self, user_id: str, roles: set[str]):
|
|
45
53
|
self.user_id = user_id
|
|
46
54
|
self.roles = roles
|
|
47
55
|
|
|
48
|
-
# 2.
|
|
56
|
+
# 2. Assuming you have your own dependency that returns a user instance
|
|
57
|
+
async def get_current_user() -> User:
|
|
58
|
+
# Your authentication logic here
|
|
59
|
+
return User(user_id="user-1", roles={"viewer"})
|
|
60
|
+
|
|
61
|
+
# 3. Dependency that library **REQUIRES**. **MUST** return a set of user roles
|
|
62
|
+
async def get_current_user_roles() -> set[str]:
|
|
63
|
+
user = await get_current_user()
|
|
64
|
+
return user.roles
|
|
65
|
+
|
|
66
|
+
# 4. Define your roles permissions
|
|
49
67
|
PERMISSIONS = {
|
|
50
68
|
"admin": {
|
|
51
69
|
Global("report:*"), # Admin can do anything with reports
|
|
@@ -55,13 +73,12 @@ PERMISSIONS = {
|
|
|
55
73
|
},
|
|
56
74
|
}
|
|
57
75
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
class ReportAccessContext(ContextualAuthz[User]):
|
|
76
|
+
# 5. Create context authorization checks
|
|
77
|
+
class ReportAccessContext(ContextualAuthz):
|
|
61
78
|
def __init__(
|
|
62
79
|
self,
|
|
63
80
|
report_id: int, # <-- Injected from path parameter
|
|
64
|
-
user: Annotated[User, Depends(
|
|
81
|
+
user: Annotated[User, Depends(get_current_user)], # Your own way how to get the user if you need it
|
|
65
82
|
):
|
|
66
83
|
self.user = user
|
|
67
84
|
self.report_id = report_id
|
|
@@ -71,22 +88,17 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
71
88
|
allowed_reports = {1, 2, 3} # e.g., query from database
|
|
72
89
|
return self.report_id in allowed_reports
|
|
73
90
|
|
|
74
|
-
#
|
|
91
|
+
# 6. Configure RBAC
|
|
75
92
|
app = FastAPI()
|
|
76
93
|
|
|
77
|
-
async def get_current_user() -> User:
|
|
78
|
-
# Your authentication logic here
|
|
79
|
-
return User(user_id="user-1", roles={"viewer"})
|
|
80
|
-
|
|
81
94
|
RBACAuthz(
|
|
82
95
|
app,
|
|
83
|
-
get_roles=lambda u: u.roles,
|
|
84
96
|
permissions=PERMISSIONS,
|
|
85
|
-
|
|
97
|
+
roles_dependency=get_current_user_roles, # Returns set[str]
|
|
86
98
|
ui_path="/_rbac", # Optional: mount visualization UI
|
|
87
99
|
)
|
|
88
100
|
|
|
89
|
-
#
|
|
101
|
+
# 7. Create protected routes
|
|
90
102
|
router = RBACRouter(permissions={"report:read"}, contexts=[ReportAccessContext])
|
|
91
103
|
|
|
92
104
|
@router.get("/reports/{report_id}")
|
|
@@ -136,14 +148,14 @@ PERMISSIONS = {
|
|
|
136
148
|
|
|
137
149
|
## Context Checks
|
|
138
150
|
|
|
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.).
|
|
151
|
+
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
152
|
|
|
141
153
|
```python
|
|
142
|
-
class ReportAccessContext(ContextualAuthz
|
|
154
|
+
class ReportAccessContext(ContextualAuthz):
|
|
143
155
|
def __init__(
|
|
144
156
|
self,
|
|
145
157
|
report_id: int, # Injected from path parameter
|
|
146
|
-
user: Annotated[User, Depends(
|
|
158
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
147
159
|
db: Annotated[AsyncSession, Depends(get_db)], # Database session
|
|
148
160
|
):
|
|
149
161
|
self.user = user
|
|
@@ -161,11 +173,11 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
161
173
|
When a request hits an RBAC-protected endpoint:
|
|
162
174
|
|
|
163
175
|
```
|
|
164
|
-
1.
|
|
165
|
-
└──
|
|
176
|
+
1. Role Resolution
|
|
177
|
+
└── roles_dependency runs → User's roles (set[str]) available
|
|
166
178
|
|
|
167
179
|
2. Permission Check
|
|
168
|
-
└── Does user have ANY grant (
|
|
180
|
+
└── Does user have ANY grant (scoped or wildcard) for required permission?
|
|
169
181
|
├── No → 403 Forbidden
|
|
170
182
|
└── Yes → Continue
|
|
171
183
|
|
|
@@ -175,7 +187,7 @@ When a request hits an RBAC-protected endpoint:
|
|
|
175
187
|
└── No → Continue to context checks
|
|
176
188
|
|
|
177
189
|
4. Context Checks (only for Contextual grants)
|
|
178
|
-
└── Run all context classes via FastAPI DI
|
|
190
|
+
└── Run all context classes via FastAPI DI (each gets its own user via Depends)
|
|
179
191
|
└── Do ALL contexts return True?
|
|
180
192
|
├── No → 403 Forbidden
|
|
181
193
|
└── Yes → Access granted
|
|
@@ -214,8 +226,8 @@ uvicorn examples.basic_app:app --reload
|
|
|
214
226
|
|
|
215
227
|
Then open your browser:
|
|
216
228
|
|
|
217
|
-
- **http://localhost:
|
|
218
|
-
- **http://localhost:
|
|
229
|
+
- **http://localhost:18000/docs** - OpenAPI docs to test the API
|
|
230
|
+
- **http://localhost:18000/_rbac** - Authorization visualization UI
|
|
219
231
|
|
|
220
232
|
### Test with different users
|
|
221
233
|
|
|
@@ -223,15 +235,15 @@ The example uses `X-Token` header for authentication:
|
|
|
223
235
|
|
|
224
236
|
```bash
|
|
225
237
|
# As admin (has Global("*") - full access)
|
|
226
|
-
curl -H "X-Token: admin-token" http://localhost:
|
|
227
|
-
curl -H "X-Token: admin-token" http://localhost:
|
|
238
|
+
curl -H "X-Token: admin-token" http://localhost:18000/reports
|
|
239
|
+
curl -H "X-Token: admin-token" http://localhost:18000/reports/1
|
|
228
240
|
|
|
229
241
|
# As user (has Contextual permissions - can only access own reports)
|
|
230
|
-
curl -H "X-Token: user-token" http://localhost:
|
|
231
|
-
curl -H "X-Token: user-token" http://localhost:
|
|
242
|
+
curl -H "X-Token: user-token" http://localhost:18000/reports/1 # OK (owns report 1)
|
|
243
|
+
curl -H "X-Token: user-token" http://localhost:18000/reports/3 # 403 (doesn't own report 3)
|
|
232
244
|
|
|
233
245
|
# As viewer (has Contextual read - can only read own reports)
|
|
234
|
-
curl -H "X-Token: viewer-token" http://localhost:
|
|
246
|
+
curl -H "X-Token: viewer-token" http://localhost:18000/reports/1 # 403 (doesn't own any)
|
|
235
247
|
```
|
|
236
248
|
|
|
237
249
|
## Visualization UI
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Role-based access control with contextual authorization for FastAPI.
|
|
4
4
|
|
|
5
|
+
Library IS NOT responsible for authentication. You can use any authentication mechanism you want.
|
|
6
|
+
|
|
7
|
+
What you need to provide:
|
|
8
|
+
- A dependency that returns the authenticated user's roles as `set[str]`
|
|
9
|
+
- Permission definitions per role
|
|
10
|
+
- Use `RBACRouter` instead of `APIRouter` for protected routes
|
|
11
|
+
- Define permissions and context checks per endpoint
|
|
12
|
+
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -14,16 +22,26 @@ pip install fastapi-rbac-authz
|
|
|
14
22
|
from typing import Annotated
|
|
15
23
|
from fastapi import Depends, FastAPI
|
|
16
24
|
from fastapi_rbac import (
|
|
17
|
-
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
25
|
+
RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
|
|
18
26
|
)
|
|
19
27
|
|
|
20
|
-
# 1.
|
|
28
|
+
# 1. You might have your user model (or you may not, we don't care)
|
|
21
29
|
class User:
|
|
22
30
|
def __init__(self, user_id: str, roles: set[str]):
|
|
23
31
|
self.user_id = user_id
|
|
24
32
|
self.roles = roles
|
|
25
33
|
|
|
26
|
-
# 2.
|
|
34
|
+
# 2. Assuming you have your own dependency that returns a user instance
|
|
35
|
+
async def get_current_user() -> User:
|
|
36
|
+
# Your authentication logic here
|
|
37
|
+
return User(user_id="user-1", roles={"viewer"})
|
|
38
|
+
|
|
39
|
+
# 3. Dependency that library **REQUIRES**. **MUST** return a set of user roles
|
|
40
|
+
async def get_current_user_roles() -> set[str]:
|
|
41
|
+
user = await get_current_user()
|
|
42
|
+
return user.roles
|
|
43
|
+
|
|
44
|
+
# 4. Define your roles permissions
|
|
27
45
|
PERMISSIONS = {
|
|
28
46
|
"admin": {
|
|
29
47
|
Global("report:*"), # Admin can do anything with reports
|
|
@@ -33,13 +51,12 @@ PERMISSIONS = {
|
|
|
33
51
|
},
|
|
34
52
|
}
|
|
35
53
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
class ReportAccessContext(ContextualAuthz[User]):
|
|
54
|
+
# 5. Create context authorization checks
|
|
55
|
+
class ReportAccessContext(ContextualAuthz):
|
|
39
56
|
def __init__(
|
|
40
57
|
self,
|
|
41
58
|
report_id: int, # <-- Injected from path parameter
|
|
42
|
-
user: Annotated[User, Depends(
|
|
59
|
+
user: Annotated[User, Depends(get_current_user)], # Your own way how to get the user if you need it
|
|
43
60
|
):
|
|
44
61
|
self.user = user
|
|
45
62
|
self.report_id = report_id
|
|
@@ -49,22 +66,17 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
49
66
|
allowed_reports = {1, 2, 3} # e.g., query from database
|
|
50
67
|
return self.report_id in allowed_reports
|
|
51
68
|
|
|
52
|
-
#
|
|
69
|
+
# 6. Configure RBAC
|
|
53
70
|
app = FastAPI()
|
|
54
71
|
|
|
55
|
-
async def get_current_user() -> User:
|
|
56
|
-
# Your authentication logic here
|
|
57
|
-
return User(user_id="user-1", roles={"viewer"})
|
|
58
|
-
|
|
59
72
|
RBACAuthz(
|
|
60
73
|
app,
|
|
61
|
-
get_roles=lambda u: u.roles,
|
|
62
74
|
permissions=PERMISSIONS,
|
|
63
|
-
|
|
75
|
+
roles_dependency=get_current_user_roles, # Returns set[str]
|
|
64
76
|
ui_path="/_rbac", # Optional: mount visualization UI
|
|
65
77
|
)
|
|
66
78
|
|
|
67
|
-
#
|
|
79
|
+
# 7. Create protected routes
|
|
68
80
|
router = RBACRouter(permissions={"report:read"}, contexts=[ReportAccessContext])
|
|
69
81
|
|
|
70
82
|
@router.get("/reports/{report_id}")
|
|
@@ -114,14 +126,14 @@ PERMISSIONS = {
|
|
|
114
126
|
|
|
115
127
|
## Context Checks
|
|
116
128
|
|
|
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.).
|
|
129
|
+
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
130
|
|
|
119
131
|
```python
|
|
120
|
-
class ReportAccessContext(ContextualAuthz
|
|
132
|
+
class ReportAccessContext(ContextualAuthz):
|
|
121
133
|
def __init__(
|
|
122
134
|
self,
|
|
123
135
|
report_id: int, # Injected from path parameter
|
|
124
|
-
user: Annotated[User, Depends(
|
|
136
|
+
user: Annotated[User, Depends(get_current_user)], # Your auth dependency
|
|
125
137
|
db: Annotated[AsyncSession, Depends(get_db)], # Database session
|
|
126
138
|
):
|
|
127
139
|
self.user = user
|
|
@@ -139,11 +151,11 @@ class ReportAccessContext(ContextualAuthz[User]):
|
|
|
139
151
|
When a request hits an RBAC-protected endpoint:
|
|
140
152
|
|
|
141
153
|
```
|
|
142
|
-
1.
|
|
143
|
-
└──
|
|
154
|
+
1. Role Resolution
|
|
155
|
+
└── roles_dependency runs → User's roles (set[str]) available
|
|
144
156
|
|
|
145
157
|
2. Permission Check
|
|
146
|
-
└── Does user have ANY grant (
|
|
158
|
+
└── Does user have ANY grant (scoped or wildcard) for required permission?
|
|
147
159
|
├── No → 403 Forbidden
|
|
148
160
|
└── Yes → Continue
|
|
149
161
|
|
|
@@ -153,7 +165,7 @@ When a request hits an RBAC-protected endpoint:
|
|
|
153
165
|
└── No → Continue to context checks
|
|
154
166
|
|
|
155
167
|
4. Context Checks (only for Contextual grants)
|
|
156
|
-
└── Run all context classes via FastAPI DI
|
|
168
|
+
└── Run all context classes via FastAPI DI (each gets its own user via Depends)
|
|
157
169
|
└── Do ALL contexts return True?
|
|
158
170
|
├── No → 403 Forbidden
|
|
159
171
|
└── Yes → Access granted
|
|
@@ -192,8 +204,8 @@ uvicorn examples.basic_app:app --reload
|
|
|
192
204
|
|
|
193
205
|
Then open your browser:
|
|
194
206
|
|
|
195
|
-
- **http://localhost:
|
|
196
|
-
- **http://localhost:
|
|
207
|
+
- **http://localhost:18000/docs** - OpenAPI docs to test the API
|
|
208
|
+
- **http://localhost:18000/_rbac** - Authorization visualization UI
|
|
197
209
|
|
|
198
210
|
### Test with different users
|
|
199
211
|
|
|
@@ -201,15 +213,15 @@ The example uses `X-Token` header for authentication:
|
|
|
201
213
|
|
|
202
214
|
```bash
|
|
203
215
|
# As admin (has Global("*") - full access)
|
|
204
|
-
curl -H "X-Token: admin-token" http://localhost:
|
|
205
|
-
curl -H "X-Token: admin-token" http://localhost:
|
|
216
|
+
curl -H "X-Token: admin-token" http://localhost:18000/reports
|
|
217
|
+
curl -H "X-Token: admin-token" http://localhost:18000/reports/1
|
|
206
218
|
|
|
207
219
|
# As user (has Contextual permissions - can only access own reports)
|
|
208
|
-
curl -H "X-Token: user-token" http://localhost:
|
|
209
|
-
curl -H "X-Token: user-token" http://localhost:
|
|
220
|
+
curl -H "X-Token: user-token" http://localhost:18000/reports/1 # OK (owns report 1)
|
|
221
|
+
curl -H "X-Token: user-token" http://localhost:18000/reports/3 # 403 (doesn't own report 3)
|
|
210
222
|
|
|
211
223
|
# As viewer (has Contextual read - can only read own reports)
|
|
212
|
-
curl -H "X-Token: viewer-token" http://localhost:
|
|
224
|
+
curl -H "X-Token: viewer-token" http://localhost:18000/reports/1 # 403 (doesn't own any)
|
|
213
225
|
```
|
|
214
226
|
|
|
215
227
|
## Visualization UI
|
|
@@ -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)
|