Fast-Permissions 0.1.0b0__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.
- fast_permissions-0.1.0b0/PKG-INFO +233 -0
- fast_permissions-0.1.0b0/README.md +203 -0
- fast_permissions-0.1.0b0/fast_permissions/__init__.py +155 -0
- fast_permissions-0.1.0b0/fast_permissions/config.py +2 -0
- fast_permissions-0.1.0b0/fast_permissions/exceptions.py +6 -0
- fast_permissions-0.1.0b0/fast_permissions/html.py +28 -0
- fast_permissions-0.1.0b0/fast_permissions/models.py +51 -0
- fast_permissions-0.1.0b0/fast_permissions/pwa.py +136 -0
- fast_permissions-0.1.0b0/fast_permissions/service.py +116 -0
- fast_permissions-0.1.0b0/pyproject.toml +65 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Fast-Permissions
|
|
3
|
+
Version: 0.1.0b0
|
|
4
|
+
Summary: Add robust authentication to your FastAPI endpoints
|
|
5
|
+
Keywords: authentication,authorization,fastapi,permissions,security,access
|
|
6
|
+
Author-Email: Cody M Sommer <bassmastacod@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Project-URL: Repository, https://github.com/BassMastaCod/Fast-Permissions.git
|
|
19
|
+
Project-URL: Issues, https://github.com/BassMastaCod/Fast-Permissions/issues
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: fastapi
|
|
22
|
+
Requires-Dist: daomodel
|
|
23
|
+
Requires-Dist: pyjwt
|
|
24
|
+
Requires-Dist: python-multipart
|
|
25
|
+
Requires-Dist: jinja2
|
|
26
|
+
Requires-Dist: bcrypt
|
|
27
|
+
Provides-Extra: pwa
|
|
28
|
+
Requires-Dist: fastpwa[controller]; extra == "pwa"
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Fast-Permissions
|
|
32
|
+
|
|
33
|
+
Fast-Permissions is a library designed to add authentication and authorization capabilities to FastAPI applications,
|
|
34
|
+
particularly those using the Fast-Controller framework.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install Fast-Permissions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For PWA functionality, install with the PWA extra:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install Fast-Permissions[pwa]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**NOTE**: The rest of the README is AI-generated.
|
|
49
|
+
I will rewrite once the library is in a stable state with most of the planned features implemented.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Here's a simple example of how to use Fast-Permissions with Fast-Controller:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from fastapi import FastAPI, Request
|
|
57
|
+
from typing import Optional
|
|
58
|
+
|
|
59
|
+
from daomodel.db import create_engine, init_db
|
|
60
|
+
from daomodel.fields import Identifier
|
|
61
|
+
from fast_controller import Resource, Action
|
|
62
|
+
from fast_permissions import RestrictedController
|
|
63
|
+
from fast_permissions.models import User
|
|
64
|
+
from fast_permissions.service import UserService, Unauthorized
|
|
65
|
+
|
|
66
|
+
# Define your resources
|
|
67
|
+
class Item(Resource, table=True):
|
|
68
|
+
name: Identifier[str]
|
|
69
|
+
description: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
# Set up the database
|
|
72
|
+
engine = create_engine("sqlite:///app.db")
|
|
73
|
+
init_db(engine)
|
|
74
|
+
|
|
75
|
+
# Create the FastAPI app
|
|
76
|
+
app = FastAPI()
|
|
77
|
+
|
|
78
|
+
# Define a function to get the current user from the request
|
|
79
|
+
def get_current_user(request: Request) -> User:
|
|
80
|
+
token = request.cookies.get('access_token')
|
|
81
|
+
if not token:
|
|
82
|
+
raise Unauthorized('No access token provided')
|
|
83
|
+
|
|
84
|
+
# You'll need to provide a way to get DAOs - this is just an example
|
|
85
|
+
with controller.dao_context() as daos:
|
|
86
|
+
return UserService(daos).from_token(token)
|
|
87
|
+
|
|
88
|
+
# Create a RestrictedController
|
|
89
|
+
controller = RestrictedController(
|
|
90
|
+
app=app,
|
|
91
|
+
engine=engine,
|
|
92
|
+
get_current_user=get_current_user,
|
|
93
|
+
public_by_default=True # Set to False to require auth by default
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Register your resources, specifying which actions don't require authentication
|
|
97
|
+
# When public_by_default=True, all actions are public unless marked restricted
|
|
98
|
+
controller.register_resource(Item)
|
|
99
|
+
|
|
100
|
+
# Create an admin user (for development/testing)
|
|
101
|
+
# In production, you would create users through your API
|
|
102
|
+
controller.register_admin("secure-password")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Authentication
|
|
106
|
+
|
|
107
|
+
Fast-Permissions uses cookie-based authentication with JWT tokens. Users can authenticate by sending a POST request to the `/api/sessions` endpoint:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
POST /api/sessions
|
|
111
|
+
Content-Type: application/x-www-form-urlencoded
|
|
112
|
+
|
|
113
|
+
username=admin&password=secure-password
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This will set an HTTP-only cookie with the JWT token. The authentication is handled automatically through cookies, so no manual token management is required in the browser.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Before using Fast-Permissions, you need to set a secret key for JWT token signing:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from fast_permissions import config
|
|
124
|
+
config.SECRET_KEY = "your-secret-key-here"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## User Management
|
|
128
|
+
|
|
129
|
+
You can manage users through the User resource that is automatically registered by RestrictedController:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Create a new user
|
|
133
|
+
POST /user
|
|
134
|
+
{
|
|
135
|
+
"username": "john",
|
|
136
|
+
"password": "password123"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Get a user
|
|
140
|
+
GET /user/john
|
|
141
|
+
|
|
142
|
+
# Update a user's password
|
|
143
|
+
PUT /user/john
|
|
144
|
+
{
|
|
145
|
+
"password": "new-password"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Delete a user
|
|
149
|
+
DELETE /user/john
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Resource Ownership
|
|
153
|
+
|
|
154
|
+
Fast-Permissions provides two base classes for resource ownership:
|
|
155
|
+
|
|
156
|
+
1. `OrphanableResource`: Resources that can exist without an owner
|
|
157
|
+
2. `OwnedResource`: Resources that are deleted when their owner is deleted
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from daomodel.fields import Identifier
|
|
163
|
+
from fast_permissions.models import OwnedResource
|
|
164
|
+
|
|
165
|
+
class Note(OwnedResource, table=True):
|
|
166
|
+
id: Identifier[int]
|
|
167
|
+
content: str
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
When a user creates a Note, they automatically become its owner. Only the owner can modify or delete the Note.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## PWA (Progressive Web App) Support
|
|
174
|
+
|
|
175
|
+
Fast-Permissions provides PWA support through the `PWAWithAuth` class, which extends the FastPWA library with authentication capabilities.
|
|
176
|
+
|
|
177
|
+
### Installation
|
|
178
|
+
|
|
179
|
+
To use PWA features, install with the PWA extra:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install Fast-Permissions[pwa]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Basic PWA Setup
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from fast_permissions.pwa import PWAWithAuth
|
|
189
|
+
|
|
190
|
+
# Create a PWA with authentication
|
|
191
|
+
pwa = PWAWithAuth(
|
|
192
|
+
title="My App",
|
|
193
|
+
public_by_default=True, # Set to False to require auth by default
|
|
194
|
+
unauthorized_redirect="/login" # Where to redirect when not authenticated
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Register a simple login page
|
|
198
|
+
pwa.register_simple_login_page()
|
|
199
|
+
|
|
200
|
+
# Create restricted pages that require authentication
|
|
201
|
+
@pwa.restricted_page('/dashboard', 'dashboard.html')
|
|
202
|
+
async def dashboard(request):
|
|
203
|
+
return {'title': 'Dashboard'}
|
|
204
|
+
|
|
205
|
+
# Create public pages (no authentication required)
|
|
206
|
+
@pwa.page('/public', 'public.html')
|
|
207
|
+
async def public_page(request):
|
|
208
|
+
return {'title': 'Public Page'}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Custom Authentication
|
|
212
|
+
|
|
213
|
+
You can provide your own authentication function:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from fastapi import Request
|
|
217
|
+
from fast_permissions.models import User
|
|
218
|
+
from fast_permissions.service import UserService, Unauthorized
|
|
219
|
+
|
|
220
|
+
def my_get_current_user(request: Request) -> User:
|
|
221
|
+
# Your custom authentication logic
|
|
222
|
+
token = request.cookies.get('access_token')
|
|
223
|
+
if not token:
|
|
224
|
+
raise Unauthorized('No token provided')
|
|
225
|
+
# ... validate token and return user
|
|
226
|
+
return user
|
|
227
|
+
|
|
228
|
+
pwa = PWAWithAuth(
|
|
229
|
+
title="My App",
|
|
230
|
+
get_current_user=my_get_current_user,
|
|
231
|
+
unauthorized_redirect="/login"
|
|
232
|
+
)
|
|
233
|
+
```
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Fast-Permissions
|
|
2
|
+
|
|
3
|
+
Fast-Permissions is a library designed to add authentication and authorization capabilities to FastAPI applications,
|
|
4
|
+
particularly those using the Fast-Controller framework.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install Fast-Permissions
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For PWA functionality, install with the PWA extra:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install Fast-Permissions[pwa]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**NOTE**: The rest of the README is AI-generated.
|
|
19
|
+
I will rewrite once the library is in a stable state with most of the planned features implemented.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Here's a simple example of how to use Fast-Permissions with Fast-Controller:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from fastapi import FastAPI, Request
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from daomodel.db import create_engine, init_db
|
|
30
|
+
from daomodel.fields import Identifier
|
|
31
|
+
from fast_controller import Resource, Action
|
|
32
|
+
from fast_permissions import RestrictedController
|
|
33
|
+
from fast_permissions.models import User
|
|
34
|
+
from fast_permissions.service import UserService, Unauthorized
|
|
35
|
+
|
|
36
|
+
# Define your resources
|
|
37
|
+
class Item(Resource, table=True):
|
|
38
|
+
name: Identifier[str]
|
|
39
|
+
description: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
# Set up the database
|
|
42
|
+
engine = create_engine("sqlite:///app.db")
|
|
43
|
+
init_db(engine)
|
|
44
|
+
|
|
45
|
+
# Create the FastAPI app
|
|
46
|
+
app = FastAPI()
|
|
47
|
+
|
|
48
|
+
# Define a function to get the current user from the request
|
|
49
|
+
def get_current_user(request: Request) -> User:
|
|
50
|
+
token = request.cookies.get('access_token')
|
|
51
|
+
if not token:
|
|
52
|
+
raise Unauthorized('No access token provided')
|
|
53
|
+
|
|
54
|
+
# You'll need to provide a way to get DAOs - this is just an example
|
|
55
|
+
with controller.dao_context() as daos:
|
|
56
|
+
return UserService(daos).from_token(token)
|
|
57
|
+
|
|
58
|
+
# Create a RestrictedController
|
|
59
|
+
controller = RestrictedController(
|
|
60
|
+
app=app,
|
|
61
|
+
engine=engine,
|
|
62
|
+
get_current_user=get_current_user,
|
|
63
|
+
public_by_default=True # Set to False to require auth by default
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Register your resources, specifying which actions don't require authentication
|
|
67
|
+
# When public_by_default=True, all actions are public unless marked restricted
|
|
68
|
+
controller.register_resource(Item)
|
|
69
|
+
|
|
70
|
+
# Create an admin user (for development/testing)
|
|
71
|
+
# In production, you would create users through your API
|
|
72
|
+
controller.register_admin("secure-password")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Authentication
|
|
76
|
+
|
|
77
|
+
Fast-Permissions uses cookie-based authentication with JWT tokens. Users can authenticate by sending a POST request to the `/api/sessions` endpoint:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
POST /api/sessions
|
|
81
|
+
Content-Type: application/x-www-form-urlencoded
|
|
82
|
+
|
|
83
|
+
username=admin&password=secure-password
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This will set an HTTP-only cookie with the JWT token. The authentication is handled automatically through cookies, so no manual token management is required in the browser.
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
Before using Fast-Permissions, you need to set a secret key for JWT token signing:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fast_permissions import config
|
|
94
|
+
config.SECRET_KEY = "your-secret-key-here"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## User Management
|
|
98
|
+
|
|
99
|
+
You can manage users through the User resource that is automatically registered by RestrictedController:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Create a new user
|
|
103
|
+
POST /user
|
|
104
|
+
{
|
|
105
|
+
"username": "john",
|
|
106
|
+
"password": "password123"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Get a user
|
|
110
|
+
GET /user/john
|
|
111
|
+
|
|
112
|
+
# Update a user's password
|
|
113
|
+
PUT /user/john
|
|
114
|
+
{
|
|
115
|
+
"password": "new-password"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Delete a user
|
|
119
|
+
DELETE /user/john
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Resource Ownership
|
|
123
|
+
|
|
124
|
+
Fast-Permissions provides two base classes for resource ownership:
|
|
125
|
+
|
|
126
|
+
1. `OrphanableResource`: Resources that can exist without an owner
|
|
127
|
+
2. `OwnedResource`: Resources that are deleted when their owner is deleted
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from daomodel.fields import Identifier
|
|
133
|
+
from fast_permissions.models import OwnedResource
|
|
134
|
+
|
|
135
|
+
class Note(OwnedResource, table=True):
|
|
136
|
+
id: Identifier[int]
|
|
137
|
+
content: str
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When a user creates a Note, they automatically become its owner. Only the owner can modify or delete the Note.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## PWA (Progressive Web App) Support
|
|
144
|
+
|
|
145
|
+
Fast-Permissions provides PWA support through the `PWAWithAuth` class, which extends the FastPWA library with authentication capabilities.
|
|
146
|
+
|
|
147
|
+
### Installation
|
|
148
|
+
|
|
149
|
+
To use PWA features, install with the PWA extra:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install Fast-Permissions[pwa]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Basic PWA Setup
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from fast_permissions.pwa import PWAWithAuth
|
|
159
|
+
|
|
160
|
+
# Create a PWA with authentication
|
|
161
|
+
pwa = PWAWithAuth(
|
|
162
|
+
title="My App",
|
|
163
|
+
public_by_default=True, # Set to False to require auth by default
|
|
164
|
+
unauthorized_redirect="/login" # Where to redirect when not authenticated
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Register a simple login page
|
|
168
|
+
pwa.register_simple_login_page()
|
|
169
|
+
|
|
170
|
+
# Create restricted pages that require authentication
|
|
171
|
+
@pwa.restricted_page('/dashboard', 'dashboard.html')
|
|
172
|
+
async def dashboard(request):
|
|
173
|
+
return {'title': 'Dashboard'}
|
|
174
|
+
|
|
175
|
+
# Create public pages (no authentication required)
|
|
176
|
+
@pwa.page('/public', 'public.html')
|
|
177
|
+
async def public_page(request):
|
|
178
|
+
return {'title': 'Public Page'}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Custom Authentication
|
|
182
|
+
|
|
183
|
+
You can provide your own authentication function:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from fastapi import Request
|
|
187
|
+
from fast_permissions.models import User
|
|
188
|
+
from fast_permissions.service import UserService, Unauthorized
|
|
189
|
+
|
|
190
|
+
def my_get_current_user(request: Request) -> User:
|
|
191
|
+
# Your custom authentication logic
|
|
192
|
+
token = request.cookies.get('access_token')
|
|
193
|
+
if not token:
|
|
194
|
+
raise Unauthorized('No token provided')
|
|
195
|
+
# ... validate token and return user
|
|
196
|
+
return user
|
|
197
|
+
|
|
198
|
+
pwa = PWAWithAuth(
|
|
199
|
+
title="My App",
|
|
200
|
+
get_current_user=my_get_current_user,
|
|
201
|
+
unauthorized_redirect="/login"
|
|
202
|
+
)
|
|
203
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Annotated, Callable
|
|
2
|
+
|
|
3
|
+
from daomodel.db import DAOFactory
|
|
4
|
+
|
|
5
|
+
from fast_controller import Controller, Action
|
|
6
|
+
from fast_controller.util import no_cache
|
|
7
|
+
from fastapi import Depends, APIRouter, Response, Request, status, FastAPI, Security, HTTPException
|
|
8
|
+
from fastapi.security import OAuth2PasswordRequestForm, APIKeyCookie
|
|
9
|
+
|
|
10
|
+
from fast_permissions import config
|
|
11
|
+
from fast_permissions.exceptions import Unauthorized
|
|
12
|
+
from fast_permissions.models import Session, User
|
|
13
|
+
from fast_permissions.service import UserService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_security = Security(APIKeyCookie(name='HTTP Only Cookie', description='Must use endpoint to login', auto_error=False))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Auth:
|
|
20
|
+
"""Provides decorators for specifying access levels for endpoints."""
|
|
21
|
+
def access(self, level: str):
|
|
22
|
+
"""Decorator that sets the access level of an endpoint i.e. public, restricted, etc..."""
|
|
23
|
+
def wrapper(func):
|
|
24
|
+
func._fp_access = level
|
|
25
|
+
return func
|
|
26
|
+
return wrapper
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def public(self):
|
|
30
|
+
"""Decorator that configures an endpoint to have no access restrictions."""
|
|
31
|
+
return self.access('public')
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def restricted(self):
|
|
35
|
+
"""Decorator that configures an endpoint to require authentication."""
|
|
36
|
+
return self.access('restricted')
|
|
37
|
+
|
|
38
|
+
auth = Auth()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_session_endpoints(router: APIRouter, controller: Controller):
|
|
42
|
+
@router.post('', status_code=status.HTTP_204_NO_CONTENT)
|
|
43
|
+
@auth.public
|
|
44
|
+
async def login(response: Response,
|
|
45
|
+
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
|
46
|
+
daos: DAOFactory = controller.daos) -> None:
|
|
47
|
+
"""Authenticates the user and sets a cookie with the access token."""
|
|
48
|
+
try:
|
|
49
|
+
user = UserService(daos).authenticate(form_data.username, form_data.password)
|
|
50
|
+
response.set_cookie(
|
|
51
|
+
key='access_token',
|
|
52
|
+
value=user.access_token,
|
|
53
|
+
httponly=True,
|
|
54
|
+
secure=True,
|
|
55
|
+
samesite='lax',
|
|
56
|
+
max_age=60 * 60 * 24,
|
|
57
|
+
path='/'
|
|
58
|
+
)
|
|
59
|
+
except TypeError:
|
|
60
|
+
if config.SECRET_KEY is None:
|
|
61
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Fast-Permissions SECRET_KEY is not configured')
|
|
62
|
+
except Unauthorized:
|
|
63
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Incorrect username or password')
|
|
64
|
+
|
|
65
|
+
@router.delete('', status_code=status.HTTP_204_NO_CONTENT)
|
|
66
|
+
@auth.restricted
|
|
67
|
+
async def logout(response: Response, request: Request, daos: DAOFactory = controller.daos) -> None:
|
|
68
|
+
"""Invalidates the caller's token and clears the cookie."""
|
|
69
|
+
caller_token = request.cookies.get('access_token')
|
|
70
|
+
UserService(daos).invalidate_token(caller_token)
|
|
71
|
+
|
|
72
|
+
response.status_code = status.HTTP_204_NO_CONTENT
|
|
73
|
+
response.delete_cookie(key='access_token', path='/')
|
|
74
|
+
|
|
75
|
+
@no_cache
|
|
76
|
+
@router.head('')
|
|
77
|
+
@auth.restricted
|
|
78
|
+
async def check_auth() -> Response:
|
|
79
|
+
"""Checks if the user is authenticated.
|
|
80
|
+
|
|
81
|
+
Returns 200 if the caller has a valid access token and 401 otherwise.
|
|
82
|
+
"""
|
|
83
|
+
return Response()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RestrictedRouter(APIRouter):
|
|
87
|
+
"""A custom APIRouter that automatically adds authentication middleware.
|
|
88
|
+
|
|
89
|
+
:param user_dep: The dependency that validates the user
|
|
90
|
+
:param public_by_default: True to only require authentication for endpoints marked with @auth.restricted
|
|
91
|
+
"""
|
|
92
|
+
def __init__(self, *args, user_dep: Depends, public_by_default: bool = False, **kwargs):
|
|
93
|
+
super().__init__(*args, **kwargs)
|
|
94
|
+
self.user_dep = user_dep
|
|
95
|
+
self.public_by_default = public_by_default
|
|
96
|
+
|
|
97
|
+
def add_api_route(self, path, endpoint, **kwargs) -> None:
|
|
98
|
+
"""Overrides the default add_api_route method to add authentication middleware."""
|
|
99
|
+
deps = kwargs.pop('dependencies')
|
|
100
|
+
if not deps:
|
|
101
|
+
deps = []
|
|
102
|
+
|
|
103
|
+
level = getattr(endpoint, '_fp_access', None)
|
|
104
|
+
if level == 'restricted':
|
|
105
|
+
deps.append(Depends(self.user_dep))
|
|
106
|
+
deps.append(_security)
|
|
107
|
+
elif level == 'public':
|
|
108
|
+
pass
|
|
109
|
+
else:
|
|
110
|
+
if not self.public_by_default:
|
|
111
|
+
deps.append(Depends(self.user_dep))
|
|
112
|
+
deps.append(_security)
|
|
113
|
+
kwargs['dependencies'] = deps
|
|
114
|
+
super().add_api_route(path, endpoint, **kwargs)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RestrictedController(Controller):
|
|
118
|
+
"""A controller that includes authentication middleware."""
|
|
119
|
+
def __init__(self, *args,
|
|
120
|
+
get_current_user: Callable,
|
|
121
|
+
public_by_default: bool = False,
|
|
122
|
+
token_endpoints: Callable = default_session_endpoints,
|
|
123
|
+
**kwargs):
|
|
124
|
+
self.get_current_user = get_current_user
|
|
125
|
+
self.public_by_default = public_by_default
|
|
126
|
+
super().__init__(*args, **kwargs)
|
|
127
|
+
self.register_resource(Session, skip=set(Action), additional_endpoints=token_endpoints)
|
|
128
|
+
self.register_resource(User)
|
|
129
|
+
|
|
130
|
+
def _create_router(self) -> APIRouter:
|
|
131
|
+
return RestrictedRouter(
|
|
132
|
+
prefix=self.prefix,
|
|
133
|
+
user_dep=self.get_current_user,
|
|
134
|
+
public_by_default=self.public_by_default
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def include_controller(self, app: FastAPI) -> None:
|
|
138
|
+
super().include_controller(app)
|
|
139
|
+
|
|
140
|
+
@app.exception_handler(Unauthorized)
|
|
141
|
+
async def unauthorized_handler(request: Request, exc: Unauthorized):
|
|
142
|
+
return Response(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
143
|
+
|
|
144
|
+
def register_admin(self, password: str) -> None:
|
|
145
|
+
"""Creates an admin user having the given password.
|
|
146
|
+
|
|
147
|
+
This only needs to be called once.
|
|
148
|
+
Once logged in as the admin, additional users can be created through the ReST API.
|
|
149
|
+
|
|
150
|
+
:param password: The password for the admin user (this will be hashed and stored in the database).
|
|
151
|
+
"""
|
|
152
|
+
admin = User(username='admin')
|
|
153
|
+
admin.password = password
|
|
154
|
+
with self.dao_context() as daos:
|
|
155
|
+
daos[User].upsert(admin)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
login_template = '''
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Login</title>
|
|
8
|
+
<style>
|
|
9
|
+
body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 0; background-color: #f4f4f4; }
|
|
10
|
+
.login-form { width: 300px; margin: 100px auto; padding: 20px; background: white; border: 1px solid #ccc; border-radius: 5px; }
|
|
11
|
+
.login-form h2 { margin-bottom: 20px; }
|
|
12
|
+
.login-form input { width: calc(100% - 20px); margin-bottom: 15px; padding: 8px; border: 1px solid #ccc; border-radius: 5px; }
|
|
13
|
+
.login-form button { padding: 10px 20px; border: none; border-radius: 5px; background: #007BFF; color: white; cursor: pointer; }
|
|
14
|
+
.login-form button:hover { background: #0056b3; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div class="login-form">
|
|
19
|
+
<h2>Login</h2>
|
|
20
|
+
<form action="${path}" method="post">
|
|
21
|
+
<input type="text" name="username" placeholder="Username" required>
|
|
22
|
+
<input type="password" name="password" placeholder="Password" required>
|
|
23
|
+
<button type="submit">Login</button>
|
|
24
|
+
</form>
|
|
25
|
+
</div>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
'''
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
import bcrypt
|
|
4
|
+
from daomodel.fields import Identifier, Unsearchable
|
|
5
|
+
from fast_controller import Resource
|
|
6
|
+
from fast_controller.schema import schemas
|
|
7
|
+
|
|
8
|
+
from fast_permissions.exceptions import InvalidPassword
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserBase(Resource):
|
|
12
|
+
username: Identifier[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@schemas(output=UserBase)
|
|
16
|
+
class User(UserBase, table=True):
|
|
17
|
+
password: Unsearchable[str]
|
|
18
|
+
|
|
19
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
20
|
+
# Automatically hash any set password
|
|
21
|
+
if key == 'password':
|
|
22
|
+
value = bcrypt.hashpw(value.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
|
23
|
+
super.__setattr__(self, key, value)
|
|
24
|
+
|
|
25
|
+
def verify(self, password: str) -> None:
|
|
26
|
+
"""Verify the user's password, raises InvalidPassword if incorrect."""
|
|
27
|
+
if not bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')):
|
|
28
|
+
raise InvalidPassword()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OrphanableResource(Resource):
|
|
32
|
+
"""A resource that can be owned by a user, but not necessarily."""
|
|
33
|
+
__abstract__ = True
|
|
34
|
+
owner: Optional[User]
|
|
35
|
+
|
|
36
|
+
def is_owned(self) -> bool:
|
|
37
|
+
return self.owner is not None
|
|
38
|
+
|
|
39
|
+
def is_owned_by(self, user: User) -> bool:
|
|
40
|
+
return self.owner == user.username
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OwnedResource(OrphanableResource):
|
|
44
|
+
"""A resource that belongs to a specific User."""
|
|
45
|
+
__abstract__ = True
|
|
46
|
+
owner: User
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Session(OwnedResource, table=True):
|
|
50
|
+
access_token: Identifier[str]
|
|
51
|
+
token_type: str = 'bearer' # is this valid or needed/weanted?
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Callable
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from daomodel.db import init_db, create_engine
|
|
6
|
+
from fastapi import Request, Depends, HTTPException, status
|
|
7
|
+
from fastapi.responses import HTMLResponse
|
|
8
|
+
|
|
9
|
+
from fast_permissions import RestrictedController
|
|
10
|
+
from fast_permissions.html import login_template
|
|
11
|
+
from fast_permissions.models import User
|
|
12
|
+
from fast_permissions.service import Unauthorized, UserService
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fastpwa import PWA, ensure_list, logger
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
'PWAWithAuth requires FastPWA. Install with: pip install fast-permissions[pwa]'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Redirect(HTTPException):
|
|
23
|
+
"""HTTP Response that redirects the client to the given URL."""
|
|
24
|
+
def __init__(self, url: str, code: int = status.HTTP_302_FOUND):
|
|
25
|
+
super().__init__(
|
|
26
|
+
status_code=code,
|
|
27
|
+
headers={"Location": url}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PWAWithAuth(PWA):
|
|
32
|
+
"""Extension of PWA that adds authentication functionality.
|
|
33
|
+
|
|
34
|
+
This implementation provides a way to create pages that require authentication.
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, *args,
|
|
37
|
+
get_current_user: Optional[Callable] = None,
|
|
38
|
+
public_by_default: Optional[bool] = None,
|
|
39
|
+
unauthorized_redirect: Optional[str] = None,
|
|
40
|
+
**kwargs):
|
|
41
|
+
self.get_current_user = get_current_user or self._default_get_current_user
|
|
42
|
+
self.public_by_default = public_by_default
|
|
43
|
+
if public_by_default is not None:
|
|
44
|
+
if get_current_user:
|
|
45
|
+
raise ValueError('`public_by_default` can only be set if not using a custom `get_current_user` function')
|
|
46
|
+
self.unauthorized_redirect = unauthorized_redirect
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def _default_controller(self):
|
|
50
|
+
controller = RestrictedController(
|
|
51
|
+
prefix=self.api_prefix,
|
|
52
|
+
get_current_user=self.get_current_user,
|
|
53
|
+
public_by_default=self.public_by_default
|
|
54
|
+
)
|
|
55
|
+
controller.engine = create_engine('database.db')
|
|
56
|
+
init_db(controller.engine)
|
|
57
|
+
return controller
|
|
58
|
+
|
|
59
|
+
def _default_get_current_user(self, request: Request) -> User:
|
|
60
|
+
"""Returns the currently logged-in user, or raises Unauthorized if not logged in."""
|
|
61
|
+
token = request.cookies.get('access_token')
|
|
62
|
+
with self.controller.dao_context() as daos:
|
|
63
|
+
return UserService(daos).from_token(token)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def restricted_dep(self):
|
|
67
|
+
return Depends(self.get_current_user_with_redirect(no_return=True))
|
|
68
|
+
|
|
69
|
+
def register_simple_login_page(self,
|
|
70
|
+
page_path: str = 'login',
|
|
71
|
+
api_path: str = '/api/sessions',
|
|
72
|
+
redirect: bool = True) -> None:
|
|
73
|
+
"""Creates a basic login page.
|
|
74
|
+
|
|
75
|
+
This page is extremely rudimentary and is only intended for developer use.
|
|
76
|
+
UX is lacking; it does not redirect after login or even provide feedback to the user.
|
|
77
|
+
|
|
78
|
+
:param page_path: Where to host the login page (/login by default)
|
|
79
|
+
:param api_path: Where to send the login form (/api/sessions by default)
|
|
80
|
+
:param redirect: False to avoid automatically redirecting to this page when not logged in
|
|
81
|
+
"""
|
|
82
|
+
@self.page(page_path, html=login_template.replace('${path}', api_path))
|
|
83
|
+
async def login_page() -> dict:
|
|
84
|
+
return {'title': f'{self.title} Login'}
|
|
85
|
+
|
|
86
|
+
if redirect:
|
|
87
|
+
self.unauthorized_redirect = '/login'
|
|
88
|
+
|
|
89
|
+
def get_current_user_with_redirect(self, url: Optional[str] = None, no_return: bool = False):
|
|
90
|
+
"""Returns a dependency that validates the user and redirects back to the original page once logged in."""
|
|
91
|
+
if not url:
|
|
92
|
+
if not self.unauthorized_redirect:
|
|
93
|
+
raise ValueError('Unauthorized redirect URL not specified. '
|
|
94
|
+
'Please set unauthorized_redirect= when creating PWA or page.')
|
|
95
|
+
url = self.unauthorized_redirect
|
|
96
|
+
async def wrapper(request: Request):
|
|
97
|
+
try:
|
|
98
|
+
return self.get_current_user(request)
|
|
99
|
+
except Unauthorized:
|
|
100
|
+
if no_return:
|
|
101
|
+
raise Redirect(url)
|
|
102
|
+
original = quote(request.url.path)
|
|
103
|
+
sep = '&' if '?' in url else '?'
|
|
104
|
+
raise Redirect(f'{url}{sep}from={original}')
|
|
105
|
+
return wrapper
|
|
106
|
+
|
|
107
|
+
def restricted_page(self,
|
|
108
|
+
route: str,
|
|
109
|
+
html: str | Path,
|
|
110
|
+
css: Optional[str | list[str]] = None,
|
|
111
|
+
js: Optional[str | list[str]] = None,
|
|
112
|
+
js_libraries: Optional[str | list[str]] = None,
|
|
113
|
+
color: Optional[str] = None,
|
|
114
|
+
unauthorized_redirect: Optional[str] = None,
|
|
115
|
+
**get_kwargs):
|
|
116
|
+
"""Decorator that creates a page requiring authentication."""
|
|
117
|
+
route = self.with_prefix(route)
|
|
118
|
+
url = unauthorized_redirect or self.unauthorized_redirect
|
|
119
|
+
get_user = self.get_current_user_with_redirect(url) if url else self.get_current_user
|
|
120
|
+
|
|
121
|
+
def decorator(func):
|
|
122
|
+
async def response_wrapper(request: Request, context: dict = Depends(func), user: User = Depends(get_user)):
|
|
123
|
+
return HTMLResponse(self.page_template.render(
|
|
124
|
+
path_prefix=self.prefix,
|
|
125
|
+
request=request,
|
|
126
|
+
title=context.get('title', self.title),
|
|
127
|
+
color=color,
|
|
128
|
+
css=ensure_list(css) + self.global_css,
|
|
129
|
+
js=ensure_list(js) + self.global_js,
|
|
130
|
+
js_libraries=ensure_list(js_libraries),
|
|
131
|
+
body=self.env.get_template(html).render(**context)
|
|
132
|
+
))
|
|
133
|
+
self.get(route, include_in_schema=False, **get_kwargs)(response_wrapper)
|
|
134
|
+
logger.info(f'Registered restricted page at {route}')
|
|
135
|
+
return func
|
|
136
|
+
return decorator
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from datetime import timedelta, datetime, timezone
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
|
|
4
|
+
import jwt
|
|
5
|
+
from daomodel.dao import NotFound
|
|
6
|
+
from daomodel.db import DAOFactory
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
|
|
9
|
+
from fast_permissions import config
|
|
10
|
+
from fast_permissions.exceptions import InvalidPassword, Unauthorized
|
|
11
|
+
from fast_permissions.models import User, Session, OwnedResource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
|
15
|
+
"""Creates a JWT access token.
|
|
16
|
+
|
|
17
|
+
:param data: The payload to include in the token
|
|
18
|
+
:param expires_delta: The duration for which the token is valid
|
|
19
|
+
:return: The encoded access token
|
|
20
|
+
"""
|
|
21
|
+
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
|
|
22
|
+
to_encode = {**data, 'exp': expire}
|
|
23
|
+
return jwt.encode(to_encode, config.SECRET_KEY, algorithm=config.ALGORITHM)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def decode_token(token: str) -> dict[str, Any]:
|
|
27
|
+
"""Decodes a JWT token and returns the payload."""
|
|
28
|
+
try:
|
|
29
|
+
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
|
|
30
|
+
if not payload.get('username'):
|
|
31
|
+
raise HTTPException(status_code=401, detail='Invalid token')
|
|
32
|
+
return payload
|
|
33
|
+
except jwt.ExpiredSignatureError:
|
|
34
|
+
raise HTTPException(status_code=401, detail='Token expired')
|
|
35
|
+
except Exception:
|
|
36
|
+
raise HTTPException(status_code=401, detail='Invalid token')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UserService:
|
|
40
|
+
def __init__(self, daos: DAOFactory):
|
|
41
|
+
self.daos = daos
|
|
42
|
+
self.user_dao = daos[User]
|
|
43
|
+
self.token_dao = daos[Session]
|
|
44
|
+
|
|
45
|
+
def register(self, username: str, password: str) -> User:
|
|
46
|
+
"""Creates a new User and saves it to the database.
|
|
47
|
+
|
|
48
|
+
:param username: The new username
|
|
49
|
+
:param password: The unencrypted password for the new username
|
|
50
|
+
:return: The newly created User
|
|
51
|
+
:raises PrimaryKeyConflict: If the username is already taken
|
|
52
|
+
"""
|
|
53
|
+
user = self.user_dao.create_with(commit=False, username=username)
|
|
54
|
+
self.set_password(user, password)
|
|
55
|
+
return user
|
|
56
|
+
|
|
57
|
+
def authenticate(self, username: str, password: str) -> User:
|
|
58
|
+
"""Authenticates a User and returns a token if successful.
|
|
59
|
+
|
|
60
|
+
:param username: The username to authenticate
|
|
61
|
+
:param password: The unencrypted password for the username
|
|
62
|
+
:return: The authenticated User, containing the access token
|
|
63
|
+
:raises HTTPException: If the username or password is incorrect
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
user = self.get_user(username)
|
|
67
|
+
user.verify(password)
|
|
68
|
+
user.access_token = create_access_token(user.model_dump(), expires_delta=timedelta(days=1))
|
|
69
|
+
self.token_dao.create_with(access_token=user.access_token, owner=user.username)
|
|
70
|
+
return user
|
|
71
|
+
except (NotFound, InvalidPassword) as e:
|
|
72
|
+
raise Unauthorized('Authentication failed due to incorrect username or password') from e
|
|
73
|
+
|
|
74
|
+
def get_user(self, username: str) -> User:
|
|
75
|
+
"""Finds a User by their username."""
|
|
76
|
+
return self.user_dao.get(username)
|
|
77
|
+
|
|
78
|
+
def set_password(self, user: User, password: str) -> None:
|
|
79
|
+
"""Sets a new password for a user and updates the User record in the database."""
|
|
80
|
+
user.password = password
|
|
81
|
+
self.user_dao.update(user)
|
|
82
|
+
|
|
83
|
+
def get_owned(self, user: User, resource: type[OwnedResource]) -> list[OwnedResource]:
|
|
84
|
+
"""Returns resources that belong to a specific User.
|
|
85
|
+
|
|
86
|
+
:param user: The user whose resources to find
|
|
87
|
+
:param resource: The type of resource to find
|
|
88
|
+
:return: A list of resources owned by the user
|
|
89
|
+
"""
|
|
90
|
+
return self.daos[resource].find(owner=user.username)
|
|
91
|
+
|
|
92
|
+
def from_token(self, token: str) -> User:
|
|
93
|
+
"""Finds a User by their token.
|
|
94
|
+
|
|
95
|
+
:param token: The token the User is authenticated with
|
|
96
|
+
:return: The User associated with the token
|
|
97
|
+
"""
|
|
98
|
+
if not token:
|
|
99
|
+
raise Unauthorized('No token provided')
|
|
100
|
+
try:
|
|
101
|
+
payload = decode_token(token)
|
|
102
|
+
username = payload['username']
|
|
103
|
+
self.token_dao.get(token)
|
|
104
|
+
return self.get_user(username)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise Unauthorized(f'Authentication failed: {str(e)}') from e
|
|
107
|
+
|
|
108
|
+
def invalidate_token(self, token: str) -> None:
|
|
109
|
+
"""Deauthenticates a session by invalidating its token."""
|
|
110
|
+
if not token:
|
|
111
|
+
return
|
|
112
|
+
try:
|
|
113
|
+
entry = self.token_dao.get(token)
|
|
114
|
+
self.token_dao.remove(entry)
|
|
115
|
+
except NotFound:
|
|
116
|
+
pass
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"pdm-backend",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "pdm.backend"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "Fast-Permissions"
|
|
9
|
+
dynamic = []
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Cody M Sommer", email = "bassmastacod@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
description = "Add robust authentication to your FastAPI endpoints"
|
|
14
|
+
keywords = [
|
|
15
|
+
"authentication",
|
|
16
|
+
"authorization",
|
|
17
|
+
"fastapi",
|
|
18
|
+
"permissions",
|
|
19
|
+
"security",
|
|
20
|
+
"access",
|
|
21
|
+
]
|
|
22
|
+
readme = "README.md"
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"fastapi",
|
|
26
|
+
"daomodel",
|
|
27
|
+
"pyjwt",
|
|
28
|
+
"python-multipart",
|
|
29
|
+
"jinja2",
|
|
30
|
+
"bcrypt",
|
|
31
|
+
]
|
|
32
|
+
classifiers = [
|
|
33
|
+
"License :: OSI Approved :: MIT License",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Programming Language :: Python :: 3.14",
|
|
39
|
+
"Development Status :: 4 - Beta",
|
|
40
|
+
"Intended Audience :: Developers",
|
|
41
|
+
"Topic :: Software Development :: Libraries",
|
|
42
|
+
"Typing :: Typed",
|
|
43
|
+
]
|
|
44
|
+
version = "0.1.0b0"
|
|
45
|
+
|
|
46
|
+
[project.license]
|
|
47
|
+
text = "MIT"
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Repository = "https://github.com/BassMastaCod/Fast-Permissions.git"
|
|
51
|
+
Issues = "https://github.com/BassMastaCod/Fast-Permissions/issues"
|
|
52
|
+
|
|
53
|
+
[project.optional-dependencies]
|
|
54
|
+
pwa = [
|
|
55
|
+
"fastpwa[controller]",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.pdm.version]
|
|
59
|
+
source = "scm"
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
pythonpath = "fast_permissions"
|
|
63
|
+
addopts = [
|
|
64
|
+
"--import-mode=importlib",
|
|
65
|
+
]
|