fastapi-rbac-authz 0.2.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.
Files changed (37) hide show
  1. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/.github/workflows/pypi-minor-deployment.yaml +2 -3
  2. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/CLAUDE.md +9 -8
  3. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/PKG-INFO +28 -21
  4. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/README.md +27 -20
  5. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/examples/basic_app.py +21 -8
  6. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/__init__.py +2 -10
  7. fastapi_rbac_authz-0.4.0/fastapi_rbac/context.py +34 -0
  8. fastapi_rbac_authz-0.4.0/fastapi_rbac/core.py +85 -0
  9. fastapi_rbac_authz-0.4.0/fastapi_rbac/dependencies.py +128 -0
  10. fastapi_rbac_authz-0.2.0/fastapi_rbac/exceptions.py → fastapi_rbac_authz-0.4.0/fastapi_rbac/errors.py +2 -3
  11. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/permissions.py +3 -1
  12. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/router.py +24 -70
  13. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/routes.py +2 -3
  14. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/schema.py +3 -3
  15. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/pyproject.toml +1 -1
  16. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_context.py +4 -4
  17. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_context_di.py +30 -32
  18. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_core.py +19 -27
  19. fastapi_rbac_authz-0.4.0/tests/test_dependencies.py +319 -0
  20. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_integration.py +31 -31
  21. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_router.py +75 -94
  22. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_ui.py +20 -20
  23. fastapi_rbac_authz-0.2.0/fastapi_rbac/context.py +0 -39
  24. fastapi_rbac_authz-0.2.0/fastapi_rbac/core.py +0 -111
  25. fastapi_rbac_authz-0.2.0/fastapi_rbac/dependencies.py +0 -254
  26. fastapi_rbac_authz-0.2.0/tests/test_dependencies.py +0 -431
  27. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/.github/workflows/checks.yaml +0 -0
  28. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/.gitignore +0 -0
  29. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/.pre-commit-config.yaml +0 -0
  30. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/py.typed +0 -0
  31. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/__init__.py +0 -0
  32. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/fastapi_rbac/ui/static/index.html +0 -0
  33. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/poetry.lock +0 -0
  34. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/__init__.py +0 -0
  35. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/conftest.py +0 -0
  36. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_exceptions.py +0 -0
  37. {fastapi_rbac_authz-0.2.0 → fastapi_rbac_authz-0.4.0}/tests/test_permissions.py +0 -0
@@ -20,8 +20,7 @@ jobs:
20
20
  needs: ["checks"]
21
21
 
22
22
  env:
23
- PYPI_USER: ${{ secrets.PYPI_USER }}
24
- PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
23
+ PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }}
25
24
 
26
25
  steps:
27
26
  - uses: actions/checkout@v3
@@ -38,7 +37,7 @@ jobs:
38
37
  run: poetry config virtualenvs.create false
39
38
 
40
39
  - name: Configure PyPi credentials
41
- run: poetry config pypi-token.pypi ${{ env.PYPI_PASSWORD }}
40
+ run: poetry config pypi-token.pypi ${{ env.PYPI_TOKEN }}
42
41
 
43
42
  - name: Bump Version
44
43
  run: poetry version minor
@@ -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.2.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, RBACUser
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 role permissions
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
- # 3. Create a context check (for contextual permissions)
59
- # All __init__ params are injected by FastAPI's DI system
60
- class ReportAccessContext(ContextualAuthz[User]):
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(RBACUser)],
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
- # 4. Create your app and configure RBAC
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
- user_dependency=get_current_user,
90
+ roles_dependency=get_current_user_roles, # Returns set[str]
86
91
  ui_path="/_rbac", # Optional: mount visualization UI
87
92
  )
88
93
 
89
- # 5. Create protected routes
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[User]):
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(RBACUser)],
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. Authentication
165
- └── user_dependency runs → User object available
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, RBACUser
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 role permissions
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
- # 3. Create a context check (for contextual permissions)
37
- # All __init__ params are injected by FastAPI's DI system
38
- class ReportAccessContext(ContextualAuthz[User]):
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(RBACUser)],
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
- # 4. Create your app and configure RBAC
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
- user_dependency=get_current_user,
68
+ roles_dependency=get_current_user_roles, # Returns set[str]
64
69
  ui_path="/_rbac", # Optional: mount visualization UI
65
70
  )
66
71
 
67
- # 5. Create protected routes
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[User]):
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(RBACUser)],
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. Authentication
143
- └── user_dependency runs → User object available
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 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)
@@ -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=403, detail=detail)
7
+ super().__init__(status_code=HTTP_403_FORBIDDEN, detail=detail)