sanic-security 1.15.3__py3-none-any.whl → 1.16.1__py3-none-any.whl
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.
- sanic_security/authentication.py +16 -22
- sanic_security/authorization.py +44 -18
- sanic_security/configuration.py +9 -0
- sanic_security/exceptions.py +5 -3
- sanic_security/models.py +55 -52
- sanic_security/oauth.py +240 -0
- sanic_security/test/server.py +75 -28
- sanic_security/test/tests.py +25 -10
- sanic_security/utils.py +9 -3
- sanic_security/verification.py +1 -4
- {sanic_security-1.15.3.dist-info → sanic_security-1.16.1.dist-info}/METADATA +132 -46
- sanic_security-1.16.1.dist-info/RECORD +17 -0
- {sanic_security-1.15.3.dist-info → sanic_security-1.16.1.dist-info}/WHEEL +1 -1
- sanic_security-1.15.3.dist-info/RECORD +0 -16
- {sanic_security-1.15.3.dist-info → sanic_security-1.16.1.dist-info}/LICENSE +0 -0
- {sanic_security-1.15.3.dist-info → sanic_security-1.16.1.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -2,13 +2,13 @@ import functools
|
|
2
2
|
import re
|
3
3
|
import warnings
|
4
4
|
|
5
|
-
from argon2.exceptions import
|
5
|
+
from argon2.exceptions import VerificationError, InvalidHashError
|
6
6
|
from sanic import Sanic
|
7
7
|
from sanic.log import logger
|
8
8
|
from sanic.request import Request
|
9
9
|
from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
|
10
10
|
|
11
|
-
from sanic_security.configuration import config
|
11
|
+
from sanic_security.configuration import config, DEFAULT_CONFIG
|
12
12
|
from sanic_security.exceptions import (
|
13
13
|
CredentialsError,
|
14
14
|
DeactivatedError,
|
@@ -74,7 +74,7 @@ async def register(
|
|
74
74
|
return account
|
75
75
|
except ValidationError as e:
|
76
76
|
raise CredentialsError(
|
77
|
-
"Username must be 3-32 characters long
|
77
|
+
"Username must be 3-32 characters long."
|
78
78
|
if "username" in e.args[0]
|
79
79
|
else "Invalid email or phone number."
|
80
80
|
)
|
@@ -128,7 +128,7 @@ async def login(
|
|
128
128
|
f"Client {get_ip(request)} has logged in with authentication session {authentication_session.id}."
|
129
129
|
)
|
130
130
|
return authentication_session
|
131
|
-
except
|
131
|
+
except (VerificationError, InvalidHashError):
|
132
132
|
logger.warning(
|
133
133
|
f"Client {get_ip(request)} has failed to log into account {account.id}."
|
134
134
|
)
|
@@ -283,7 +283,7 @@ def validate_password(password: str) -> str:
|
|
283
283
|
return password
|
284
284
|
|
285
285
|
|
286
|
-
def initialize_security(app: Sanic, create_root=True) -> None:
|
286
|
+
def initialize_security(app: Sanic, create_root: bool = True) -> None:
|
287
287
|
"""
|
288
288
|
Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
|
289
289
|
|
@@ -294,30 +294,26 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
294
294
|
|
295
295
|
@app.listener("before_server_start")
|
296
296
|
async def audit_configuration(app, loop):
|
297
|
-
if
|
297
|
+
if config.SECRET == DEFAULT_CONFIG["SECRET"]:
|
298
298
|
warnings.warn("Secret should be changed from default.", AuditWarning, 2)
|
299
|
-
if not
|
299
|
+
if not config.SESSION_HTTPONLY:
|
300
300
|
warnings.warn("HttpOnly should be enabled.", AuditWarning, 2)
|
301
|
-
if not
|
301
|
+
if not config.SESSION_SECURE:
|
302
302
|
warnings.warn("Secure should be enabled.", AuditWarning, 2)
|
303
|
-
if (
|
304
|
-
not security_config.SESSION_SAMESITE
|
305
|
-
or security_config.SESSION_SAMESITE.lower() == "none"
|
306
|
-
):
|
303
|
+
if not config.SESSION_SAMESITE or config.SESSION_SAMESITE.lower() == "none":
|
307
304
|
warnings.warn("SameSite should not be none.", AuditWarning, 2)
|
308
|
-
if not
|
305
|
+
if not config.SESSION_DOMAIN:
|
309
306
|
warnings.warn("Domain should not be none.", AuditWarning, 2)
|
310
307
|
if (
|
311
308
|
create_root
|
312
|
-
and
|
313
|
-
== DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
|
309
|
+
and config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
|
314
310
|
):
|
315
311
|
warnings.warn(
|
316
312
|
"Initial admin email should be changed from default.", AuditWarning, 2
|
317
313
|
)
|
318
314
|
if (
|
319
315
|
create_root
|
320
|
-
and
|
316
|
+
and config.INITIAL_ADMIN_PASSWORD
|
321
317
|
== DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
|
322
318
|
):
|
323
319
|
warnings.warn(
|
@@ -335,13 +331,11 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
335
331
|
except DoesNotExist:
|
336
332
|
role = await Role.create(
|
337
333
|
description="Has administrator abilities, assign sparingly.",
|
338
|
-
permissions="*:*",
|
334
|
+
permissions=["*:*"],
|
339
335
|
name="Root",
|
340
336
|
)
|
341
337
|
try:
|
342
|
-
account = await Account.filter(
|
343
|
-
email=security_config.INITIAL_ADMIN_EMAIL
|
344
|
-
).get()
|
338
|
+
account = await Account.filter(email=config.INITIAL_ADMIN_EMAIL).get()
|
345
339
|
await account.fetch_related("roles")
|
346
340
|
if role not in account.roles:
|
347
341
|
await account.roles.add(role)
|
@@ -349,8 +343,8 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
349
343
|
except DoesNotExist:
|
350
344
|
account = await Account.create(
|
351
345
|
username="Root",
|
352
|
-
email=
|
353
|
-
password=password_hasher.hash(
|
346
|
+
email=config.INITIAL_ADMIN_EMAIL,
|
347
|
+
password=password_hasher.hash(config.INITIAL_ADMIN_PASSWORD),
|
354
348
|
verified=True,
|
355
349
|
)
|
356
350
|
await account.roles.add(role)
|
sanic_security/authorization.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import functools
|
2
|
-
from fnmatch import fnmatch
|
3
2
|
|
4
3
|
from sanic.log import logger
|
5
4
|
from sanic.request import Request
|
@@ -65,18 +64,39 @@ async def check_permissions(
|
|
65
64
|
raise AnonymousError
|
66
65
|
roles = await authentication_session.bearer.roles.filter(deleted=False).all()
|
67
66
|
for role in roles:
|
68
|
-
for
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
return authentication_session
|
67
|
+
for role_permission in role.permissions:
|
68
|
+
for required_permission in required_permissions:
|
69
|
+
if check_wildcard(role_permission, required_permission):
|
70
|
+
request.ctx.session = authentication_session
|
71
|
+
return authentication_session
|
74
72
|
logger.warning(
|
75
73
|
f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action."
|
76
74
|
)
|
77
75
|
raise AuthorizationError("Insufficient permissions required for this action.")
|
78
76
|
|
79
77
|
|
78
|
+
def check_wildcard(wildcard: str, pattern: str):
|
79
|
+
"""
|
80
|
+
Evaluates if the input matches the pattern considering wildcards (`*`) and comma-separated options in the pattern.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
wildcard (str): A wildcard string (e.g., "a:b:c").
|
84
|
+
pattern (str): A wildcard pattern optional (`*`) or comma-separated values to match against (e.g., "a:b,c:*").
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
is_match
|
88
|
+
"""
|
89
|
+
wildcard_parts = [set(part.split(",")) for part in wildcard.split(":")]
|
90
|
+
pattern_parts = [set(part.split(",")) for part in pattern.split(":")]
|
91
|
+
for i, pattern_part in enumerate(pattern_parts):
|
92
|
+
if i >= len(wildcard_parts):
|
93
|
+
return False
|
94
|
+
wildcard_part = wildcard_parts[i]
|
95
|
+
if "*" not in wildcard_part and not wildcard_part.issuperset(pattern_part):
|
96
|
+
return False
|
97
|
+
return all("*" in part for part in wildcard_parts[len(pattern_parts) :])
|
98
|
+
|
99
|
+
|
80
100
|
async def check_roles(request: Request, *required_roles: str) -> AuthenticationSession:
|
81
101
|
"""
|
82
102
|
Authenticates client and determines if the account has sufficient roles for an action.
|
@@ -105,19 +125,25 @@ async def check_roles(request: Request, *required_roles: str) -> AuthenticationS
|
|
105
125
|
f"Client {get_ip(request)} attempted an unauthorized action anonymously."
|
106
126
|
)
|
107
127
|
raise AnonymousError
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
128
|
+
if set(required_roles) & {
|
129
|
+
role.name
|
130
|
+
for role in await authentication_session.bearer.roles.filter(
|
131
|
+
deleted=False
|
132
|
+
).all()
|
133
|
+
}:
|
134
|
+
request.ctx.session = authentication_session
|
135
|
+
return authentication_session
|
113
136
|
logger.warning(
|
114
|
-
f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action
|
137
|
+
f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action"
|
115
138
|
)
|
116
|
-
raise AuthorizationError("Insufficient roles required for this action
|
139
|
+
raise AuthorizationError("Insufficient roles required for this action")
|
117
140
|
|
118
141
|
|
119
142
|
async def assign_role(
|
120
|
-
name: str,
|
143
|
+
name: str,
|
144
|
+
account: Account,
|
145
|
+
description: str = None,
|
146
|
+
*permissions: str,
|
121
147
|
) -> Role:
|
122
148
|
"""
|
123
149
|
Easy account role assignment. Role being assigned to an account will be created if it doesn't exist.
|
@@ -125,8 +151,8 @@ async def assign_role(
|
|
125
151
|
Args:
|
126
152
|
name (str): The name of the role associated with the account.
|
127
153
|
account (Account): The account associated with the created role.
|
128
|
-
permissions (str): The permissions of the role associated with the account. Permissions must be separated via ", " and in wildcard format.
|
129
154
|
description (str): The description of the role associated with the account.
|
155
|
+
*permissions (Tuple[str, ...]): The permissions of the role associated with the account, must be in wildcard format.
|
130
156
|
"""
|
131
157
|
try:
|
132
158
|
role = await Role.filter(name=name).get()
|
@@ -140,7 +166,7 @@ async def assign_role(
|
|
140
166
|
return role
|
141
167
|
|
142
168
|
|
143
|
-
def
|
169
|
+
def requires_permission(*required_permissions: str):
|
144
170
|
"""
|
145
171
|
Authenticates client and determines if the account has sufficient permissions for an action.
|
146
172
|
|
@@ -178,7 +204,7 @@ def require_permissions(*required_permissions: str):
|
|
178
204
|
return decorator
|
179
205
|
|
180
206
|
|
181
|
-
def
|
207
|
+
def requires_role(*required_roles: str):
|
182
208
|
"""
|
183
209
|
Authenticates client and determines if the account has sufficient roles for an action.
|
184
210
|
|
sanic_security/configuration.py
CHANGED
@@ -27,6 +27,9 @@ SOFTWARE.
|
|
27
27
|
DEFAULT_CONFIG = {
|
28
28
|
"SECRET": "This is a big secret. Shhhhh",
|
29
29
|
"PUBLIC_SECRET": None,
|
30
|
+
"OAUTH_CLIENT": None,
|
31
|
+
"OAUTH_SECRET": None,
|
32
|
+
"OAUTH_REDIRECT": None,
|
30
33
|
"SESSION_SAMESITE": "Strict",
|
31
34
|
"SESSION_SECURE": True,
|
32
35
|
"SESSION_HTTPONLY": True,
|
@@ -54,6 +57,9 @@ class Config(dict):
|
|
54
57
|
Attributes:
|
55
58
|
SECRET (str): The secret used by the hashing algorithm for generating and signing JWTs. This should be a string unique to your application. Keep it safe.
|
56
59
|
PUBLIC_SECRET (str): The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application.
|
60
|
+
OAUTH_CLIENT (str): The client ID provided by the OAuth provider, this is used to identify the application making the OAuth request.
|
61
|
+
OAUTH_SECRET (str): The client secret provided by the OAuth provider, this is used in conjunction with the client ID to authenticate the application.
|
62
|
+
OAUTH_REDIRECT (str): The redirect URI registered with the OAuth provider, This is the URI where the user will be redirected after a successful authentication.
|
57
63
|
SESSION_SAMESITE (str): The SameSite attribute of session cookies.
|
58
64
|
SESSION_SECURE (bool): The Secure attribute of session cookies.
|
59
65
|
SESSION_HTTPONLY (bool): The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing.
|
@@ -75,6 +81,9 @@ class Config(dict):
|
|
75
81
|
|
76
82
|
SECRET: str
|
77
83
|
PUBLIC_SECRET: str
|
84
|
+
OAUTH_CLIENT: str
|
85
|
+
OAUTH_SECRET: str
|
86
|
+
OAUTH_REDIRECT: str
|
78
87
|
SESSION_SAMESITE: str
|
79
88
|
SESSION_SECURE: bool
|
80
89
|
SESSION_HTTPONLY: bool
|
sanic_security/exceptions.py
CHANGED
@@ -119,7 +119,9 @@ class JWTDecodeError(SessionError):
|
|
119
119
|
Raised when client JWT is invalid.
|
120
120
|
"""
|
121
121
|
|
122
|
-
def __init__(
|
122
|
+
def __init__(
|
123
|
+
self, message="Session token invalid, not provided, or expired.", code=400
|
124
|
+
):
|
123
125
|
super().__init__(message, code)
|
124
126
|
|
125
127
|
|
@@ -141,8 +143,8 @@ class ExpiredError(SessionError):
|
|
141
143
|
Raised when session has expired.
|
142
144
|
"""
|
143
145
|
|
144
|
-
def __init__(self):
|
145
|
-
super().__init__(
|
146
|
+
def __init__(self, message="Session has expired."):
|
147
|
+
super().__init__(message)
|
146
148
|
|
147
149
|
|
148
150
|
class SecondFactorRequiredError(SessionError):
|
sanic_security/models.py
CHANGED
@@ -8,12 +8,12 @@ from typing import Union
|
|
8
8
|
import jwt
|
9
9
|
from jwt import DecodeError
|
10
10
|
from sanic.request import Request
|
11
|
-
from sanic.response import HTTPResponse
|
11
|
+
from sanic.response import HTTPResponse
|
12
12
|
from tortoise import fields, Model
|
13
13
|
from tortoise.exceptions import DoesNotExist, ValidationError
|
14
14
|
from tortoise.validators import RegexValidator
|
15
15
|
|
16
|
-
from sanic_security.configuration import config
|
16
|
+
from sanic_security.configuration import config
|
17
17
|
from sanic_security.exceptions import *
|
18
18
|
from sanic_security.utils import (
|
19
19
|
get_ip,
|
@@ -108,16 +108,16 @@ class Account(BaseModel):
|
|
108
108
|
username (str): Public identifier.
|
109
109
|
email (str): Private identifier and can be used for verification.
|
110
110
|
phone (str): Mobile phone number with country code included and can be used for verification. Can be null or empty.
|
111
|
-
password (str): Password of account for user protection
|
111
|
+
password (str): Password of account for user protection, must be hashed via Argon2.
|
112
|
+
oauth_id (str): Identifier associated with an OAuth 2.0 authorization flow.
|
112
113
|
disabled (bool): Renders the account unusable but available.
|
113
114
|
verified (bool): Renders the account unusable until verified via two-step verification or other method.
|
114
115
|
roles (ManyToManyRelation[Role]): Roles associated with this account.
|
115
116
|
"""
|
116
117
|
|
117
118
|
username: str = fields.CharField(
|
118
|
-
unique=
|
119
|
+
unique=config.ALLOW_LOGIN_WITH_USERNAME,
|
119
120
|
max_length=32,
|
120
|
-
validators=[RegexValidator(r"^[A-Za-z0-9_-]*$", re.I)],
|
121
121
|
)
|
122
122
|
email: str = fields.CharField(
|
123
123
|
unique=True,
|
@@ -135,6 +135,7 @@ class Account(BaseModel):
|
|
135
135
|
],
|
136
136
|
)
|
137
137
|
password: str = fields.CharField(max_length=255)
|
138
|
+
oauth_id: str = fields.CharField(unique=True, null=True, max_length=255)
|
138
139
|
disabled: bool = fields.BooleanField(default=False)
|
139
140
|
verified: bool = fields.BooleanField(default=False)
|
140
141
|
roles: fields.ManyToManyRelation["Role"] = fields.ManyToManyField(
|
@@ -238,7 +239,7 @@ class Account(BaseModel):
|
|
238
239
|
try:
|
239
240
|
account = await Account.get_via_email(credential)
|
240
241
|
except NotFoundError as e:
|
241
|
-
if
|
242
|
+
if config.ALLOW_LOGIN_WITH_USERNAME:
|
242
243
|
account = await Account.get_via_username(credential)
|
243
244
|
else:
|
244
245
|
raise e
|
@@ -350,9 +351,6 @@ class Session(BaseModel):
|
|
350
351
|
Args:
|
351
352
|
response (HTTPResponse): Sanic response used to store JWT into a cookie on the client.
|
352
353
|
"""
|
353
|
-
cookie = (
|
354
|
-
f"{security_config.SESSION_PREFIX}_{self.__class__.__name__.lower()[:7]}"
|
355
|
-
)
|
356
354
|
encoded_session = jwt.encode(
|
357
355
|
{
|
358
356
|
"id": self.id,
|
@@ -361,22 +359,18 @@ class Session(BaseModel):
|
|
361
359
|
"bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
|
362
360
|
"ip": self.ip,
|
363
361
|
},
|
364
|
-
|
365
|
-
|
362
|
+
config.SECRET,
|
363
|
+
config.SESSION_ENCODING_ALGORITHM,
|
366
364
|
)
|
367
365
|
response.cookies.add_cookie(
|
368
|
-
|
366
|
+
f"{config.SESSION_PREFIX}_{self.__class__.__name__[:7].lower()}",
|
369
367
|
str(encoded_session),
|
370
|
-
httponly=
|
371
|
-
samesite=
|
372
|
-
secure=
|
373
|
-
domain=
|
374
|
-
expires=(
|
375
|
-
|
376
|
-
if hasattr(self, "refresh_expiration_date")
|
377
|
-
and self.refresh_expiration_date
|
378
|
-
else self.expiration_date
|
379
|
-
),
|
368
|
+
httponly=config.SESSION_HTTPONLY,
|
369
|
+
samesite=config.SESSION_SAMESITE,
|
370
|
+
secure=config.SESSION_SECURE,
|
371
|
+
domain=config.SESSION_DOMAIN,
|
372
|
+
expires=getattr(self, "refresh_expiration_date", None)
|
373
|
+
or self.expiration_date,
|
380
374
|
)
|
381
375
|
|
382
376
|
@property
|
@@ -405,7 +399,7 @@ class Session(BaseModel):
|
|
405
399
|
cls,
|
406
400
|
request: Request,
|
407
401
|
account: Account,
|
408
|
-
**kwargs: Union[int, str, bool, float, list, dict],
|
402
|
+
**kwargs: dict[str, Union[int, str, bool, float, list, dict]],
|
409
403
|
):
|
410
404
|
"""
|
411
405
|
Creates session with pre-set values.
|
@@ -413,7 +407,7 @@ class Session(BaseModel):
|
|
413
407
|
Args:
|
414
408
|
request (Request): Sanic request parameter.
|
415
409
|
account (Account): Account being associated to the session.
|
416
|
-
**kwargs (
|
410
|
+
**kwargs (Union[int, str, bool, float, list, dict]): Extra arguments applied during session creation.
|
417
411
|
|
418
412
|
Returns:
|
419
413
|
session
|
@@ -454,16 +448,16 @@ class Session(BaseModel):
|
|
454
448
|
JWTDecodeError
|
455
449
|
"""
|
456
450
|
cookie = request.cookies.get(
|
457
|
-
f"{
|
451
|
+
f"{config.SESSION_PREFIX}_{cls.__name__[:7].lower()}"
|
458
452
|
)
|
459
453
|
try:
|
460
454
|
if not cookie:
|
461
|
-
raise JWTDecodeError
|
455
|
+
raise JWTDecodeError
|
462
456
|
else:
|
463
457
|
return jwt.decode(
|
464
458
|
cookie,
|
465
|
-
|
466
|
-
|
459
|
+
config.PUBLIC_SECRET or config.SECRET,
|
460
|
+
config.SESSION_ENCODING_ALGORITHM,
|
467
461
|
)
|
468
462
|
except DecodeError as e:
|
469
463
|
raise JWTDecodeError(str(e))
|
@@ -521,7 +515,7 @@ class VerificationSession(Session):
|
|
521
515
|
"""
|
522
516
|
if not code or self.code != code.upper():
|
523
517
|
self.attempts += 1
|
524
|
-
if self.attempts <
|
518
|
+
if self.attempts < config.MAX_CHALLENGE_ATTEMPTS:
|
525
519
|
await self.save(update_fields=["attempts"])
|
526
520
|
raise ChallengeError(
|
527
521
|
"Your code does not match verification session code."
|
@@ -532,7 +526,12 @@ class VerificationSession(Session):
|
|
532
526
|
await self.deactivate()
|
533
527
|
|
534
528
|
@classmethod
|
535
|
-
async def new(
|
529
|
+
async def new(
|
530
|
+
cls,
|
531
|
+
request: Request,
|
532
|
+
account: Account,
|
533
|
+
**kwargs: Union[int, str, bool, float, list, dict],
|
534
|
+
):
|
536
535
|
raise NotImplementedError
|
537
536
|
|
538
537
|
class Meta:
|
@@ -543,14 +542,17 @@ class TwoStepSession(VerificationSession):
|
|
543
542
|
"""Validates client using a code sent via email or text."""
|
544
543
|
|
545
544
|
@classmethod
|
546
|
-
async def new(
|
545
|
+
async def new(
|
546
|
+
cls,
|
547
|
+
request: Request,
|
548
|
+
account: Account,
|
549
|
+
**kwargs: Union[int, str, bool, float, list, dict],
|
550
|
+
):
|
547
551
|
return await cls.create(
|
548
552
|
**kwargs,
|
549
553
|
ip=get_ip(request),
|
550
554
|
bearer=account,
|
551
|
-
expiration_date=get_expiration_date(
|
552
|
-
security_config.TWO_STEP_SESSION_EXPIRATION
|
553
|
-
),
|
555
|
+
expiration_date=get_expiration_date(config.TWO_STEP_SESSION_EXPIRATION),
|
554
556
|
code=get_code(True),
|
555
557
|
)
|
556
558
|
|
@@ -562,38 +564,35 @@ class CaptchaSession(VerificationSession):
|
|
562
564
|
"""Validates client with a captcha challenge via image or audio."""
|
563
565
|
|
564
566
|
@classmethod
|
565
|
-
async def new(
|
567
|
+
async def new(
|
568
|
+
cls,
|
569
|
+
request: Request,
|
570
|
+
**kwargs: Union[int, str, bool, float, list, dict],
|
571
|
+
):
|
566
572
|
return await cls.create(
|
567
573
|
**kwargs,
|
568
574
|
ip=get_ip(request),
|
569
575
|
code=get_code(),
|
570
|
-
expiration_date=get_expiration_date(
|
571
|
-
security_config.CAPTCHA_SESSION_EXPIRATION
|
572
|
-
),
|
576
|
+
expiration_date=get_expiration_date(config.CAPTCHA_SESSION_EXPIRATION),
|
573
577
|
)
|
574
578
|
|
575
|
-
def get_image(self) ->
|
579
|
+
def get_image(self) -> bytes:
|
576
580
|
"""
|
577
581
|
Retrieves captcha image data.
|
578
582
|
|
579
583
|
Returns:
|
580
584
|
captcha_image
|
581
585
|
"""
|
582
|
-
return
|
583
|
-
image_generator.generate(self.code, "jpeg").getvalue(),
|
584
|
-
content_type="image/jpeg",
|
585
|
-
)
|
586
|
+
return image_generator.generate(self.code, "jpeg").getvalue()
|
586
587
|
|
587
|
-
def get_audio(self) ->
|
588
|
+
def get_audio(self) -> bytes:
|
588
589
|
"""
|
589
590
|
Retrieves captcha audio data.
|
590
591
|
|
591
592
|
Returns:
|
592
593
|
captcha_audio
|
593
594
|
"""
|
594
|
-
return
|
595
|
-
bytes(audio_generator.generate(self.code)), content_type="audio/mpeg"
|
596
|
-
)
|
595
|
+
return bytes(audio_generator.generate(self.code))
|
597
596
|
|
598
597
|
class Meta:
|
599
598
|
table = "captcha_session"
|
@@ -652,17 +651,21 @@ class AuthenticationSession(Session):
|
|
652
651
|
|
653
652
|
@classmethod
|
654
653
|
async def new(
|
655
|
-
cls,
|
654
|
+
cls,
|
655
|
+
request: Request,
|
656
|
+
account: Account = None,
|
657
|
+
is_refresh=False,
|
658
|
+
**kwargs: Union[int, str, bool, float, list, dict],
|
656
659
|
):
|
657
660
|
authentication_session = await cls.create(
|
658
661
|
**kwargs,
|
659
662
|
bearer=account,
|
660
663
|
ip=get_ip(request),
|
661
664
|
expiration_date=get_expiration_date(
|
662
|
-
|
665
|
+
config.AUTHENTICATION_SESSION_EXPIRATION
|
663
666
|
),
|
664
667
|
refresh_expiration_date=get_expiration_date(
|
665
|
-
|
668
|
+
config.AUTHENTICATION_REFRESH_EXPIRATION
|
666
669
|
),
|
667
670
|
)
|
668
671
|
authentication_session.is_refresh = is_refresh
|
@@ -679,12 +682,12 @@ class Role(BaseModel):
|
|
679
682
|
Attributes:
|
680
683
|
name (str): Name of the role.
|
681
684
|
description (str): Description of the role.
|
682
|
-
permissions (str): Permissions of the role. Must
|
685
|
+
permissions (list[str]): Permissions of the role. Must in wildcard format (printer:query, dashboard:info,delete).
|
683
686
|
"""
|
684
687
|
|
685
688
|
name: str = fields.CharField(unique=True, max_length=255)
|
686
689
|
description: str = fields.CharField(max_length=255, null=True)
|
687
|
-
permissions: str = fields.
|
690
|
+
permissions: list[str] = fields.JSONField(null=True)
|
688
691
|
|
689
692
|
def validate(self) -> None:
|
690
693
|
raise NotImplementedError
|