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.
Files changed (37) hide show
  1. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/CLAUDE.md +9 -8
  2. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/PKG-INFO +42 -30
  3. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/README.md +41 -29
  4. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/examples/basic_app.py +21 -8
  5. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/__init__.py +2 -10
  6. fastapi_rbac_authz-0.5.0/fastapi_rbac/context.py +34 -0
  7. fastapi_rbac_authz-0.5.0/fastapi_rbac/core.py +85 -0
  8. fastapi_rbac_authz-0.5.0/fastapi_rbac/dependencies.py +128 -0
  9. fastapi_rbac_authz-0.3.0/fastapi_rbac/exceptions.py → fastapi_rbac_authz-0.5.0/fastapi_rbac/errors.py +2 -3
  10. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/permissions.py +3 -1
  11. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/router.py +24 -70
  12. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/routes.py +2 -3
  13. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/schema.py +3 -3
  14. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/pyproject.toml +1 -1
  15. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_context.py +4 -4
  16. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_context_di.py +30 -32
  17. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_core.py +19 -27
  18. fastapi_rbac_authz-0.5.0/tests/test_dependencies.py +319 -0
  19. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_integration.py +31 -31
  20. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_router.py +75 -94
  21. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_ui.py +20 -20
  22. fastapi_rbac_authz-0.3.0/fastapi_rbac/context.py +0 -39
  23. fastapi_rbac_authz-0.3.0/fastapi_rbac/core.py +0 -111
  24. fastapi_rbac_authz-0.3.0/fastapi_rbac/dependencies.py +0 -254
  25. fastapi_rbac_authz-0.3.0/tests/test_dependencies.py +0 -431
  26. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.github/workflows/checks.yaml +0 -0
  27. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.github/workflows/pypi-minor-deployment.yaml +0 -0
  28. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.gitignore +0 -0
  29. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/.pre-commit-config.yaml +0 -0
  30. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/py.typed +0 -0
  31. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/__init__.py +0 -0
  32. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/fastapi_rbac/ui/static/index.html +0 -0
  33. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/poetry.lock +0 -0
  34. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/__init__.py +0 -0
  35. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/conftest.py +0 -0
  36. {fastapi_rbac_authz-0.3.0 → fastapi_rbac_authz-0.5.0}/tests/test_exceptions.py +0 -0
  37. {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[UserT]** (`core.py`) - Main configuration class that attaches to FastAPI app. Configures role-to-permission mappings, user dependency injection, and optional visualization UI.
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[UserT]** (`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 should use `Annotated[User, Depends(RBACUser)]` for the user parameter.
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 user dependency injection. Manipulates function signatures dynamically via `inspect.Signature`.
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**: User stored in `request.state.user`, RBAC config in `app.state.rbac`, routers in `app.state._rbac_routers_`
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**: Extract roles → resolve grants → check global permissions first → run contextual checks only if needed
58
- - **Type Safety**: Strict mypy with full generics (`RBACAuthz[UserT]`, `ContextualAuthz[UserT]`)
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, RBACUser, ContextualAuthz
64
+ RBACAuthz, RBACRouter, ContextualAuthz
64
65
  Global, Contextual, PermissionGrant, PermissionScope
65
- Forbidden, create_auth_dependency, create_authz_dependency, evaluate_permissions
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.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, RBACUser
47
+ RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
40
48
  )
41
49
 
42
- # 1. Define your user model
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. Define role permissions
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
- # 3. Create a context check (for contextual permissions)
59
- # All __init__ params are injected by FastAPI's DI system
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(RBACUser)],
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
- # 4. Create your app and configure RBAC
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
- user_dependency=get_current_user,
97
+ roles_dependency=get_current_user_roles, # Returns set[str]
86
98
  ui_path="/_rbac", # Optional: mount visualization UI
87
99
  )
88
100
 
89
- # 5. Create protected routes
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[User]):
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(RBACUser)],
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. Authentication
165
- └── user_dependency runs → User object available
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 (global or contextual) for required permission?
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:8000/docs** - OpenAPI docs to test the API
218
- - **http://localhost:8000/_rbac** - Authorization visualization UI
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:8000/reports
227
- curl -H "X-Token: admin-token" http://localhost:8000/reports/1
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:8000/reports/1 # OK (owns report 1)
231
- curl -H "X-Token: user-token" http://localhost:8000/reports/3 # 403 (doesn't own report 3)
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:8000/reports/1 # 403 (doesn't own any)
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, RBACUser
25
+ RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
18
26
  )
19
27
 
20
- # 1. Define your user model
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. Define role permissions
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
- # 3. Create a context check (for contextual permissions)
37
- # All __init__ params are injected by FastAPI's DI system
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(RBACUser)],
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
- # 4. Create your app and configure RBAC
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
- user_dependency=get_current_user,
75
+ roles_dependency=get_current_user_roles, # Returns set[str]
64
76
  ui_path="/_rbac", # Optional: mount visualization UI
65
77
  )
66
78
 
67
- # 5. Create protected routes
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[User]):
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(RBACUser)],
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. Authentication
143
- └── user_dependency runs → User object available
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 (global or contextual) for required permission?
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:8000/docs** - OpenAPI docs to test the API
196
- - **http://localhost:8000/_rbac** - Authorization visualization UI
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:8000/reports
205
- curl -H "X-Token: admin-token" http://localhost:8000/reports/1
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:8000/reports/1 # OK (owns report 1)
209
- curl -H "X-Token: user-token" http://localhost:8000/reports/3 # 403 (doesn't own report 3)
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:8000/reports/1 # 403 (doesn't own any)
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 Dependency
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[User]):
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(RBACUser)],
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
- user_dependency=get_current_user,
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(RBACUser)]):
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
- RBACUser,
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)