casbin-fastapi-decorator 0.1.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.
- casbin_fastapi_decorator-0.1.0/LICENSE +21 -0
- casbin_fastapi_decorator-0.1.0/PKG-INFO +197 -0
- casbin_fastapi_decorator-0.1.0/README.md +160 -0
- casbin_fastapi_decorator-0.1.0/pyproject.toml +66 -0
- casbin_fastapi_decorator-0.1.0/src/casbin_fastapi_decorator/__init__.py +6 -0
- casbin_fastapi_decorator-0.1.0/src/casbin_fastapi_decorator/_builder.py +64 -0
- casbin_fastapi_decorator-0.1.0/src/casbin_fastapi_decorator/_guard.py +60 -0
- casbin_fastapi_decorator-0.1.0/src/casbin_fastapi_decorator/_types.py +20 -0
- casbin_fastapi_decorator-0.1.0/src/casbin_fastapi_decorator/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Neko1313
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: casbin-fastapi-decorator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Casbin authorization decorator factory for FastAPI
|
|
5
|
+
Author: Neko1313
|
|
6
|
+
Author-email: Neko1313 <nikita.ribalchencko@yandex.ru>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Neko1313
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
Requires-Dist: fastapi>=0.115.0
|
|
29
|
+
Requires-Dist: fastapi-decorators>=0.5.0
|
|
30
|
+
Requires-Dist: casbin>=1.36.0
|
|
31
|
+
Requires-Dist: casbin-fastapi-decorator-db ; extra == 'db'
|
|
32
|
+
Requires-Dist: casbin-fastapi-decorator-jwt ; extra == 'jwt'
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Provides-Extra: db
|
|
35
|
+
Provides-Extra: jwt
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# casbin-fastapi-decorator
|
|
39
|
+
|
|
40
|
+
Authorization decorator factory for FastAPI based on [Casbin](https://casbin.org/) and [fastapi-decorators](https://pypi.org/project/fastapi-decorators/).
|
|
41
|
+
|
|
42
|
+
Decorators are applied to routes — no middleware or dependencies in the endpoint signature.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install casbin-fastapi-decorator
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Additional providers:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "casbin-fastapi-decorator[jwt]" # JWT authentication
|
|
54
|
+
pip install "casbin-fastapi-decorator[db]" # Policies from DB (SQLAlchemy)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import casbin
|
|
61
|
+
from fastapi import FastAPI, HTTPException
|
|
62
|
+
from casbin_fastapi_decorator import AccessSubject, PermissionGuard
|
|
63
|
+
|
|
64
|
+
# 1. Providers — regular FastAPI dependencies
|
|
65
|
+
async def get_current_user() -> dict:
|
|
66
|
+
return {"sub": "alice", "role": "admin"}
|
|
67
|
+
|
|
68
|
+
async def get_enforcer() -> casbin.Enforcer:
|
|
69
|
+
return casbin.Enforcer("model.conf", "policy.csv")
|
|
70
|
+
|
|
71
|
+
# 2. Decorator factory
|
|
72
|
+
guard = PermissionGuard(
|
|
73
|
+
user_provider=get_current_user,
|
|
74
|
+
enforcer_provider=get_enforcer,
|
|
75
|
+
error_factory=lambda user, *rv: HTTPException(403, "Forbidden"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
app = FastAPI()
|
|
79
|
+
|
|
80
|
+
# 3. Authentication only
|
|
81
|
+
@app.get("/me")
|
|
82
|
+
@guard.auth_required()
|
|
83
|
+
async def me():
|
|
84
|
+
return {"ok": True}
|
|
85
|
+
|
|
86
|
+
# 4. Static permission check
|
|
87
|
+
@app.get("/articles")
|
|
88
|
+
@guard.require_permission("articles", "read")
|
|
89
|
+
async def list_articles():
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
# 5. Dynamic check — value from request
|
|
93
|
+
async def get_article(article_id: int) -> dict:
|
|
94
|
+
return {"id": article_id, "owner": "alice"}
|
|
95
|
+
|
|
96
|
+
@app.get("/articles/{article_id}")
|
|
97
|
+
@guard.require_permission(
|
|
98
|
+
AccessSubject(val=get_article, selector=lambda a: a["owner"]),
|
|
99
|
+
"read",
|
|
100
|
+
)
|
|
101
|
+
async def read_article(article_id: int):
|
|
102
|
+
return {"article_id": article_id}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Arguments of `require_permission` are passed to `enforcer.enforce(user, *args)` in the same order. `AccessSubject` is resolved via FastAPI DI, then transformed by the `selector`.
|
|
106
|
+
|
|
107
|
+
## API
|
|
108
|
+
|
|
109
|
+
### `PermissionGuard`
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
PermissionGuard(
|
|
113
|
+
user_provider=..., # FastAPI dependency that returns the current user
|
|
114
|
+
enforcer_provider=..., # FastAPI dependency that returns a casbin.Enforcer
|
|
115
|
+
error_factory=..., # callable(user, *rvals) -> Exception
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Method | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `auth_required()` | Decorator: authentication only (user_provider must not raise an exception) |
|
|
122
|
+
| `require_permission(*args)` | Decorator: permission check via `enforcer.enforce(user, *args)` |
|
|
123
|
+
|
|
124
|
+
### `AccessSubject`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
AccessSubject(
|
|
128
|
+
val=get_item, # FastAPI dependency
|
|
129
|
+
selector=lambda item: item["name"], # transformation before enforce
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Wraps a dependency whose value needs to be obtained from the request and passed to the enforcer. By default, `selector` is identity (`lambda x: x`).
|
|
134
|
+
|
|
135
|
+
## JWT provider
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from casbin_fastapi_decorator_jwt import JWTUserProvider
|
|
139
|
+
|
|
140
|
+
user_provider = JWTUserProvider(
|
|
141
|
+
secret_key="your-secret",
|
|
142
|
+
algorithm="HS256", # default
|
|
143
|
+
cookie_name="access_token", # optional, enables reading from cookie
|
|
144
|
+
user_model=UserSchema, # optional, Pydantic model for payload validation
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Extracts JWT from the Bearer header and/or cookie. If `user_model` is specified, validates the payload via `model_validate()`.
|
|
149
|
+
|
|
150
|
+
## DB provider
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from casbin_fastapi_decorator_db import DatabaseEnforcerProvider
|
|
154
|
+
|
|
155
|
+
enforcer_provider = DatabaseEnforcerProvider(
|
|
156
|
+
model_path="model.conf",
|
|
157
|
+
session_factory=get_async_session,
|
|
158
|
+
policy_model=PolicyORM,
|
|
159
|
+
policy_mapper=lambda p: (p.sub, p.obj, p.act),
|
|
160
|
+
default_policies=[("admin", "*", "*")], # optional
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Loads policies from a SQLAlchemy async session and creates a `casbin.Enforcer` per request. `default_policies` are added on top of the DB policies.
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
| Example | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| [`examples/core`](examples/core) | Bearer token auth, file-based Casbin policies |
|
|
171
|
+
| [`examples/core-jwt`](examples/core-jwt) | JWT auth via `JWTUserProvider`, file-based policies |
|
|
172
|
+
| [`examples/core-db`](examples/core-db) | Bearer token auth, policies from SQLite via `DatabaseEnforcerProvider` |
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
Requires Python 3.10+, [uv](https://docs.astral.sh/uv/), [task](https://taskfile.dev/).
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
task install # uv sync --all-groups + install extras (jwt, db)
|
|
180
|
+
task lint # ruff + ty + bandit for all packages
|
|
181
|
+
task tests # all tests (core + jwt + db)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Individual package tasks:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
task core:lint # lint core only
|
|
188
|
+
task core:test # test core only
|
|
189
|
+
task jwt:lint # lint JWT package
|
|
190
|
+
task jwt:test # test JWT package
|
|
191
|
+
task db:lint # lint DB package
|
|
192
|
+
task db:test # test DB package (requires Docker for testcontainers)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# casbin-fastapi-decorator
|
|
2
|
+
|
|
3
|
+
Authorization decorator factory for FastAPI based on [Casbin](https://casbin.org/) and [fastapi-decorators](https://pypi.org/project/fastapi-decorators/).
|
|
4
|
+
|
|
5
|
+
Decorators are applied to routes — no middleware or dependencies in the endpoint signature.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install casbin-fastapi-decorator
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Additional providers:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install "casbin-fastapi-decorator[jwt]" # JWT authentication
|
|
17
|
+
pip install "casbin-fastapi-decorator[db]" # Policies from DB (SQLAlchemy)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import casbin
|
|
24
|
+
from fastapi import FastAPI, HTTPException
|
|
25
|
+
from casbin_fastapi_decorator import AccessSubject, PermissionGuard
|
|
26
|
+
|
|
27
|
+
# 1. Providers — regular FastAPI dependencies
|
|
28
|
+
async def get_current_user() -> dict:
|
|
29
|
+
return {"sub": "alice", "role": "admin"}
|
|
30
|
+
|
|
31
|
+
async def get_enforcer() -> casbin.Enforcer:
|
|
32
|
+
return casbin.Enforcer("model.conf", "policy.csv")
|
|
33
|
+
|
|
34
|
+
# 2. Decorator factory
|
|
35
|
+
guard = PermissionGuard(
|
|
36
|
+
user_provider=get_current_user,
|
|
37
|
+
enforcer_provider=get_enforcer,
|
|
38
|
+
error_factory=lambda user, *rv: HTTPException(403, "Forbidden"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
app = FastAPI()
|
|
42
|
+
|
|
43
|
+
# 3. Authentication only
|
|
44
|
+
@app.get("/me")
|
|
45
|
+
@guard.auth_required()
|
|
46
|
+
async def me():
|
|
47
|
+
return {"ok": True}
|
|
48
|
+
|
|
49
|
+
# 4. Static permission check
|
|
50
|
+
@app.get("/articles")
|
|
51
|
+
@guard.require_permission("articles", "read")
|
|
52
|
+
async def list_articles():
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
# 5. Dynamic check — value from request
|
|
56
|
+
async def get_article(article_id: int) -> dict:
|
|
57
|
+
return {"id": article_id, "owner": "alice"}
|
|
58
|
+
|
|
59
|
+
@app.get("/articles/{article_id}")
|
|
60
|
+
@guard.require_permission(
|
|
61
|
+
AccessSubject(val=get_article, selector=lambda a: a["owner"]),
|
|
62
|
+
"read",
|
|
63
|
+
)
|
|
64
|
+
async def read_article(article_id: int):
|
|
65
|
+
return {"article_id": article_id}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Arguments of `require_permission` are passed to `enforcer.enforce(user, *args)` in the same order. `AccessSubject` is resolved via FastAPI DI, then transformed by the `selector`.
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### `PermissionGuard`
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
PermissionGuard(
|
|
76
|
+
user_provider=..., # FastAPI dependency that returns the current user
|
|
77
|
+
enforcer_provider=..., # FastAPI dependency that returns a casbin.Enforcer
|
|
78
|
+
error_factory=..., # callable(user, *rvals) -> Exception
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Method | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `auth_required()` | Decorator: authentication only (user_provider must not raise an exception) |
|
|
85
|
+
| `require_permission(*args)` | Decorator: permission check via `enforcer.enforce(user, *args)` |
|
|
86
|
+
|
|
87
|
+
### `AccessSubject`
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
AccessSubject(
|
|
91
|
+
val=get_item, # FastAPI dependency
|
|
92
|
+
selector=lambda item: item["name"], # transformation before enforce
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Wraps a dependency whose value needs to be obtained from the request and passed to the enforcer. By default, `selector` is identity (`lambda x: x`).
|
|
97
|
+
|
|
98
|
+
## JWT provider
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from casbin_fastapi_decorator_jwt import JWTUserProvider
|
|
102
|
+
|
|
103
|
+
user_provider = JWTUserProvider(
|
|
104
|
+
secret_key="your-secret",
|
|
105
|
+
algorithm="HS256", # default
|
|
106
|
+
cookie_name="access_token", # optional, enables reading from cookie
|
|
107
|
+
user_model=UserSchema, # optional, Pydantic model for payload validation
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Extracts JWT from the Bearer header and/or cookie. If `user_model` is specified, validates the payload via `model_validate()`.
|
|
112
|
+
|
|
113
|
+
## DB provider
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from casbin_fastapi_decorator_db import DatabaseEnforcerProvider
|
|
117
|
+
|
|
118
|
+
enforcer_provider = DatabaseEnforcerProvider(
|
|
119
|
+
model_path="model.conf",
|
|
120
|
+
session_factory=get_async_session,
|
|
121
|
+
policy_model=PolicyORM,
|
|
122
|
+
policy_mapper=lambda p: (p.sub, p.obj, p.act),
|
|
123
|
+
default_policies=[("admin", "*", "*")], # optional
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Loads policies from a SQLAlchemy async session and creates a `casbin.Enforcer` per request. `default_policies` are added on top of the DB policies.
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
| Example | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| [`examples/core`](examples/core) | Bearer token auth, file-based Casbin policies |
|
|
134
|
+
| [`examples/core-jwt`](examples/core-jwt) | JWT auth via `JWTUserProvider`, file-based policies |
|
|
135
|
+
| [`examples/core-db`](examples/core-db) | Bearer token auth, policies from SQLite via `DatabaseEnforcerProvider` |
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
Requires Python 3.10+, [uv](https://docs.astral.sh/uv/), [task](https://taskfile.dev/).
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
task install # uv sync --all-groups + install extras (jwt, db)
|
|
143
|
+
task lint # ruff + ty + bandit for all packages
|
|
144
|
+
task tests # all tests (core + jwt + db)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Individual package tasks:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
task core:lint # lint core only
|
|
151
|
+
task core:test # test core only
|
|
152
|
+
task jwt:lint # lint JWT package
|
|
153
|
+
task jwt:test # test JWT package
|
|
154
|
+
task db:lint # lint DB package
|
|
155
|
+
task db:test # test DB package (requires Docker for testcontainers)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "casbin-fastapi-decorator"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Casbin authorization decorator factory for FastAPI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Neko1313", email = "nikita.ribalchencko@yandex.ru" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.115.0",
|
|
13
|
+
"fastapi-decorators>=0.5.0",
|
|
14
|
+
"casbin>=1.36.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
jwt = [
|
|
19
|
+
"casbin-fastapi-decorator-jwt"
|
|
20
|
+
]
|
|
21
|
+
db = [
|
|
22
|
+
"casbin-fastapi-decorator-db"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.uv.workspace]
|
|
30
|
+
members = [
|
|
31
|
+
"packages/*",
|
|
32
|
+
"examples/core",
|
|
33
|
+
"examples/core-jwt",
|
|
34
|
+
"examples/core-db",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
casbin-fastapi-decorator-jwt = { workspace = true }
|
|
39
|
+
casbin-fastapi-decorator-db = { workspace = true }
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
asyncio_mode = "auto"
|
|
44
|
+
python_files = ["test_*.py", "*_test.py", "act.py", "error.py"]
|
|
45
|
+
markers = [
|
|
46
|
+
"integration: integration tests (real I/O, HTTP)",
|
|
47
|
+
"unit: unit tests (no real I/O)",
|
|
48
|
+
"permission_guard: PermissionGuard component tests",
|
|
49
|
+
"access_subject: AccessSubject component tests",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.bandit]
|
|
53
|
+
exclude_dirs = ["tests"]
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
lint = [
|
|
57
|
+
"bandit>=1.9.3",
|
|
58
|
+
"ruff>=0.15.0",
|
|
59
|
+
"ty>=0.0.15",
|
|
60
|
+
]
|
|
61
|
+
test = [
|
|
62
|
+
"pytest>=9.0.2",
|
|
63
|
+
"pytest-asyncio>=1.3.0",
|
|
64
|
+
"pytest-cov>=7.0.0",
|
|
65
|
+
"httpx>=0.28.0",
|
|
66
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from inspect import isawaitable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends
|
|
7
|
+
from fastapi_decorators import depends
|
|
8
|
+
|
|
9
|
+
from casbin_fastapi_decorator._types import AccessSubject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_auth_decorator(user_provider: "Callable[..., Any]") -> Callable:
|
|
13
|
+
"""Build an authentication-only decorator."""
|
|
14
|
+
return depends(Depends(user_provider))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_permission_decorator(
|
|
18
|
+
*,
|
|
19
|
+
user_provider: "Callable[..., Any]",
|
|
20
|
+
enforcer_provider: "Callable[..., Any]",
|
|
21
|
+
error_factory: "Callable[..., Exception]",
|
|
22
|
+
args: tuple[AccessSubject | Any, ...],
|
|
23
|
+
) -> "Callable":
|
|
24
|
+
"""
|
|
25
|
+
Build a permission-check decorator via casbin enforcer.
|
|
26
|
+
|
|
27
|
+
Resolved values are passed to
|
|
28
|
+
``enforcer.enforce(user, *rvals)``
|
|
29
|
+
in the same order as *args*.
|
|
30
|
+
"""
|
|
31
|
+
depends_kwargs: dict[str, Any] = {
|
|
32
|
+
"__fguard_user__": Depends(user_provider),
|
|
33
|
+
"__fguard_enforcer__": Depends(enforcer_provider),
|
|
34
|
+
}
|
|
35
|
+
for i, arg in enumerate(args):
|
|
36
|
+
if isinstance(arg, AccessSubject):
|
|
37
|
+
depends_kwargs[f"__fguard_{i}__"] = Depends(arg.val)
|
|
38
|
+
|
|
39
|
+
def decorator(func: "Callable") -> "Callable":
|
|
40
|
+
@depends(**depends_kwargs)
|
|
41
|
+
@wraps(func)
|
|
42
|
+
async def wrapper(*fn_args: Any, **kw: Any) -> Any:
|
|
43
|
+
user = kw.pop("__fguard_user__")
|
|
44
|
+
enforcer = kw.pop("__fguard_enforcer__")
|
|
45
|
+
|
|
46
|
+
rvals: list[Any] = []
|
|
47
|
+
for i, arg in enumerate(args):
|
|
48
|
+
if isinstance(arg, AccessSubject):
|
|
49
|
+
raw = kw.pop(f"__fguard_{i}__")
|
|
50
|
+
rvals.append(arg.selector(raw))
|
|
51
|
+
else:
|
|
52
|
+
rvals.append(arg)
|
|
53
|
+
|
|
54
|
+
result = enforcer.enforce(user, *rvals)
|
|
55
|
+
if isawaitable(result):
|
|
56
|
+
result = await result
|
|
57
|
+
if not result:
|
|
58
|
+
raise error_factory(user, *rvals)
|
|
59
|
+
|
|
60
|
+
return await func(*fn_args, **kw)
|
|
61
|
+
|
|
62
|
+
return wrapper
|
|
63
|
+
|
|
64
|
+
return decorator
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from casbin_fastapi_decorator._builder import (
|
|
6
|
+
build_auth_decorator,
|
|
7
|
+
build_permission_decorator,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from casbin_fastapi_decorator._types import AccessSubject
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PermissionGuard:
|
|
17
|
+
"""
|
|
18
|
+
Factory for authorization decorators.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
user_provider: FastAPI dependency returning
|
|
22
|
+
the current user.
|
|
23
|
+
enforcer_provider: FastAPI dependency returning
|
|
24
|
+
a casbin Enforcer (``.enforce()``).
|
|
25
|
+
error_factory: Callable that creates an exception
|
|
26
|
+
on access denial. Receives ``(user, *rvals)``.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
user_provider: Callable[..., Any],
|
|
34
|
+
enforcer_provider: Callable[..., Any],
|
|
35
|
+
error_factory: Callable[..., Exception],
|
|
36
|
+
) -> None:
|
|
37
|
+
self._user_provider = user_provider
|
|
38
|
+
self._enforcer_provider = enforcer_provider
|
|
39
|
+
self._error_factory = error_factory
|
|
40
|
+
|
|
41
|
+
def auth_required(self) -> Callable:
|
|
42
|
+
"""Return an authentication-only decorator."""
|
|
43
|
+
return build_auth_decorator(self._user_provider)
|
|
44
|
+
|
|
45
|
+
def require_permission(self, *args: AccessSubject | Any) -> Callable:
|
|
46
|
+
"""
|
|
47
|
+
Return a permission-check decorator.
|
|
48
|
+
|
|
49
|
+
Positional arguments are passed to
|
|
50
|
+
``enforcer.enforce(user, *resolved_values)``
|
|
51
|
+
in the same order. ``AccessSubject`` values are
|
|
52
|
+
resolved via FastAPI DI and transformed with
|
|
53
|
+
their selector. Other values are passed as-is.
|
|
54
|
+
"""
|
|
55
|
+
return build_permission_decorator(
|
|
56
|
+
user_provider=self._user_provider,
|
|
57
|
+
enforcer_provider=self._enforcer_provider,
|
|
58
|
+
error_factory=self._error_factory,
|
|
59
|
+
args=args,
|
|
60
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class AccessSubject:
|
|
12
|
+
"""
|
|
13
|
+
Wrapper around a FastAPI dependency with a selector.
|
|
14
|
+
|
|
15
|
+
val: callable — FastAPI dep, wrapped in Depends()
|
|
16
|
+
selector: transforms the resolved value before enforce
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
val: Callable[..., Any]
|
|
20
|
+
selector: Callable[[Any], Any] = field(default=lambda x: x)
|
|
File without changes
|