sanic-security 1.15.2__py3-none-any.whl → 1.16.0__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.
@@ -2,13 +2,13 @@ import functools
2
2
  import re
3
3
  import warnings
4
4
 
5
- from argon2.exceptions import VerifyMismatchError
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 as security_config, DEFAULT_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 and can only include _ or -."
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 VerifyMismatchError:
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
  )
@@ -247,6 +247,7 @@ def requires_authentication(arg=None):
247
247
  DeactivatedError
248
248
  UnverifiedError
249
249
  DisabledError
250
+ SecondFactorRequiredError
250
251
  ExpiredError
251
252
  """
252
253
 
@@ -282,41 +283,37 @@ def validate_password(password: str) -> str:
282
283
  return password
283
284
 
284
285
 
285
- def initialize_security(app: Sanic, create_root=True) -> None:
286
+ def initialize_security(app: Sanic, create_root: bool = True) -> None:
286
287
  """
287
288
  Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
288
289
 
289
290
  Args:
290
- app (Sanic): The main Sanic application instance.
291
+ app (Sanic): Sanic application instance.
291
292
  create_root (bool): Determines root account creation on initialization.
292
293
  """
293
294
 
294
295
  @app.listener("before_server_start")
295
296
  async def audit_configuration(app, loop):
296
- if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
297
+ if config.SECRET == DEFAULT_CONFIG["SECRET"]:
297
298
  warnings.warn("Secret should be changed from default.", AuditWarning, 2)
298
- if not security_config.SESSION_HTTPONLY:
299
+ if not config.SESSION_HTTPONLY:
299
300
  warnings.warn("HttpOnly should be enabled.", AuditWarning, 2)
300
- if not security_config.SESSION_SECURE:
301
+ if not config.SESSION_SECURE:
301
302
  warnings.warn("Secure should be enabled.", AuditWarning, 2)
302
- if (
303
- not security_config.SESSION_SAMESITE
304
- or security_config.SESSION_SAMESITE.lower() == "none"
305
- ):
303
+ if not config.SESSION_SAMESITE or config.SESSION_SAMESITE.lower() == "none":
306
304
  warnings.warn("SameSite should not be none.", AuditWarning, 2)
307
- if not security_config.SESSION_DOMAIN:
305
+ if not config.SESSION_DOMAIN:
308
306
  warnings.warn("Domain should not be none.", AuditWarning, 2)
309
307
  if (
310
308
  create_root
311
- and security_config.INITIAL_ADMIN_EMAIL
312
- == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
309
+ and config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
313
310
  ):
314
311
  warnings.warn(
315
312
  "Initial admin email should be changed from default.", AuditWarning, 2
316
313
  )
317
314
  if (
318
315
  create_root
319
- and security_config.INITIAL_ADMIN_PASSWORD
316
+ and config.INITIAL_ADMIN_PASSWORD
320
317
  == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
321
318
  ):
322
319
  warnings.warn(
@@ -334,13 +331,11 @@ def initialize_security(app: Sanic, create_root=True) -> None:
334
331
  except DoesNotExist:
335
332
  role = await Role.create(
336
333
  description="Has administrator abilities, assign sparingly.",
337
- permissions="*:*",
334
+ permissions=["*:*"],
338
335
  name="Root",
339
336
  )
340
337
  try:
341
- account = await Account.filter(
342
- email=security_config.INITIAL_ADMIN_EMAIL
343
- ).get()
338
+ account = await Account.filter(email=config.INITIAL_ADMIN_EMAIL).get()
344
339
  await account.fetch_related("roles")
345
340
  if role not in account.roles:
346
341
  await account.roles.add(role)
@@ -348,8 +343,8 @@ def initialize_security(app: Sanic, create_root=True) -> None:
348
343
  except DoesNotExist:
349
344
  account = await Account.create(
350
345
  username="Root",
351
- email=security_config.INITIAL_ADMIN_EMAIL,
352
- password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
346
+ email=config.INITIAL_ADMIN_EMAIL,
347
+ password=password_hasher.hash(config.INITIAL_ADMIN_PASSWORD),
353
348
  verified=True,
354
349
  )
355
350
  await account.roles.add(role)
@@ -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 required_permission, role_permission in zip(
69
- required_permissions, role.permissions.split(", ")
70
- ):
71
- if fnmatch(required_permission, role_permission):
72
- request.ctx.session = authentication_session
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
- roles = await authentication_session.bearer.roles.filter(deleted=False).all()
109
- for role in roles:
110
- if role.name in required_roles:
111
- request.ctx.session = authentication_session
112
- return authentication_session
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, account: Account, permissions: str = None, description: str = None
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 require_permissions(*required_permissions: str):
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 require_roles(*required_roles: str):
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
 
@@ -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
@@ -119,7 +119,9 @@ class JWTDecodeError(SessionError):
119
119
  Raised when client JWT is invalid.
120
120
  """
121
121
 
122
- def __init__(self, message, code=400):
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__("Session has expired.")
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, raw
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 as security_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,
@@ -52,7 +52,7 @@ class BaseModel(Model):
52
52
  Base Sanic Security model that all other models derive from.
53
53
 
54
54
  Attributes:
55
- id (int): Primary key of model.
55
+ id (str): Primary key of model.
56
56
  date_created (datetime): Time this model was created in the database.
57
57
  date_updated (datetime): Time this model was updated in the database.
58
58
  deleted (bool): Renders the model filterable without removing from the database.
@@ -67,7 +67,7 @@ class BaseModel(Model):
67
67
 
68
68
  def validate(self) -> None:
69
69
  """
70
- Raises an error with respect to state.
70
+ Raises an error with respect to model's state.
71
71
 
72
72
  Raises:
73
73
  SecurityError
@@ -77,7 +77,7 @@ class BaseModel(Model):
77
77
  @property
78
78
  def json(self) -> dict:
79
79
  """
80
- A JSON serializable dict to be used in an HTTP request or response.
80
+ A JSON serializable dict to be used in a request or response.
81
81
 
82
82
  Example:
83
83
  Below is an example of this method returning a dict to be used for JSON serialization.
@@ -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 protection. Must be hashed via Argon2.
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=security_config.ALLOW_LOGIN_WITH_USERNAME,
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 security_config.ALLOW_LOGIN_WITH_USERNAME:
242
+ if config.ALLOW_LOGIN_WITH_USERNAME:
242
243
  account = await Account.get_via_username(credential)
243
244
  else:
244
245
  raise e
@@ -247,7 +248,7 @@ class Account(BaseModel):
247
248
  @staticmethod
248
249
  async def get_via_header(request: Request):
249
250
  """
250
- Retrieve the account the client is logging into and client's password attempt via the basic authorization header.
251
+ Retrieve an account via the basic authorization header.
251
252
 
252
253
  Args:
253
254
  request (Request): Sanic request parameter.
@@ -276,7 +277,7 @@ class Account(BaseModel):
276
277
  @staticmethod
277
278
  async def get_via_phone(phone: str):
278
279
  """
279
- Retrieve an account with a phone number.
280
+ Retrieve an account via a phone number.
280
281
 
281
282
  Args:
282
283
  phone (str): Phone number associated to account being retrieved.
@@ -332,7 +333,7 @@ class Session(BaseModel):
332
333
 
333
334
  async def deactivate(self):
334
335
  """
335
- Renders session deactivated and unusable.
336
+ Renders session deactivated and therefor unusable.
336
337
 
337
338
  Raises:
338
339
  DeactivatedError
@@ -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
- security_config.SECRET,
365
- security_config.SESSION_ENCODING_ALGORITHM,
362
+ config.SECRET,
363
+ config.SESSION_ENCODING_ALGORITHM,
366
364
  )
367
365
  response.cookies.add_cookie(
368
- cookie,
366
+ f"{config.SESSION_PREFIX}_{self.__class__.__name__[:7].lower()}",
369
367
  str(encoded_session),
370
- httponly=security_config.SESSION_HTTPONLY,
371
- samesite=security_config.SESSION_SAMESITE,
372
- secure=security_config.SESSION_SECURE,
373
- domain=security_config.SESSION_DOMAIN,
374
- expires=(
375
- self.refresh_expiration_date
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 (dict[str, Union[int, str, bool, float, list, dict]]): Extra arguments applied during session creation.
410
+ **kwargs (Union[int, str, bool, float, list, dict]): Extra arguments applied during session creation.
417
411
 
418
412
  Returns:
419
413
  session
@@ -426,7 +420,7 @@ class Session(BaseModel):
426
420
  Retrieves sessions associated to an account.
427
421
 
428
422
  Args:
429
- account (Request): Account associated with sessions being retrieved.
423
+ account (Account): Account associated with sessions being retrieved.
430
424
 
431
425
  Returns:
432
426
  sessions
@@ -442,7 +436,7 @@ class Session(BaseModel):
442
436
  @classmethod
443
437
  def decode_raw(cls, request: Request) -> dict:
444
438
  """
445
- Decodes JWT token from client cookie into a python dict.
439
+ Decodes session JWT token from client cookie into a python dict.
446
440
 
447
441
  Args:
448
442
  request (Request): Sanic request parameter.
@@ -454,16 +448,16 @@ class Session(BaseModel):
454
448
  JWTDecodeError
455
449
  """
456
450
  cookie = request.cookies.get(
457
- f"{security_config.SESSION_PREFIX}_{cls.__name__.lower()[:7]}"
451
+ f"{config.SESSION_PREFIX}_{cls.__name__[:7].lower()}"
458
452
  )
459
453
  try:
460
454
  if not cookie:
461
- raise JWTDecodeError("Session token not provided or expired.", 401)
455
+ raise JWTDecodeError
462
456
  else:
463
457
  return jwt.decode(
464
458
  cookie,
465
- security_config.PUBLIC_SECRET or security_config.SECRET,
466
- security_config.SESSION_ENCODING_ALGORITHM,
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))
@@ -471,7 +465,7 @@ class Session(BaseModel):
471
465
  @classmethod
472
466
  async def decode(cls, request: Request):
473
467
  """
474
- Decodes session JWT from client cookie to a Sanic Security session.
468
+ Decodes session JWT token from client cookie into a session model.
475
469
 
476
470
  Args:
477
471
  request (Request): Sanic request parameter.
@@ -498,11 +492,11 @@ class Session(BaseModel):
498
492
 
499
493
  class VerificationSession(Session):
500
494
  """
501
- Used for a client verification method that requires some form of code, challenge, or key.
495
+ Used for client verification challenges that require some form of code or key.
502
496
 
503
497
  Attributes:
504
- attempts (int): The amount of incorrect times a user entered a code not equal to this verification sessions code.
505
- code (str): Used as a secret key that would be sent via email, text, etc to complete the verification challenge.
498
+ attempts (int): The amount of times a user entered a code not equal to this verification sessions code.
499
+ code (str): A secret key that would be sent via email, text, etc.
506
500
  """
507
501
 
508
502
  attempts: int = fields.IntField(default=0)
@@ -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 < security_config.MAX_CHALLENGE_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(cls, request: Request, account: Account, **kwargs):
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:
@@ -540,17 +539,20 @@ class VerificationSession(Session):
540
539
 
541
540
 
542
541
  class TwoStepSession(VerificationSession):
543
- """Validates a client using a code sent via email or text."""
542
+ """Validates client using a code sent via email or text."""
544
543
 
545
544
  @classmethod
546
- async def new(cls, request: Request, account: Account, **kwargs):
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
 
@@ -559,41 +561,38 @@ class TwoStepSession(VerificationSession):
559
561
 
560
562
 
561
563
  class CaptchaSession(VerificationSession):
562
- """Validates a client with a captcha challenge."""
564
+ """Validates client with a captcha challenge via image or audio."""
563
565
 
564
566
  @classmethod
565
- async def new(cls, request: Request, **kwargs):
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) -> HTTPResponse:
579
+ def get_image(self) -> bytes:
576
580
  """
577
- Retrieves captcha image file.
581
+ Retrieves captcha image data.
578
582
 
579
583
  Returns:
580
584
  captcha_image
581
585
  """
582
- return raw(
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) -> HTTPResponse:
588
+ def get_audio(self) -> bytes:
588
589
  """
589
- Retrieves captcha audio file.
590
+ Retrieves captcha audio data.
590
591
 
591
592
  Returns:
592
593
  captcha_audio
593
594
  """
594
- return raw(
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"
@@ -604,9 +603,9 @@ class AuthenticationSession(Session):
604
603
  Used to authenticate and identify a client.
605
604
 
606
605
  Attributes:
607
- refresh_expiration_date (bool): Date and time the session can no longer be refreshed.
606
+ refresh_expiration_date (datetime): Date and time the session can no longer be refreshed.
608
607
  requires_second_factor (bool): Determines if session requires a second factor.
609
- is_refresh (bool): Will only be true once when instantiated during refresh of expired session.
608
+ is_refresh (bool): Will only be true once when instantiated during the refresh of expired session.
610
609
  """
611
610
 
612
611
  refresh_expiration_date: datetime.datetime = fields.DatetimeField(null=True)
@@ -652,17 +651,21 @@ class AuthenticationSession(Session):
652
651
 
653
652
  @classmethod
654
653
  async def new(
655
- cls, request: Request, account: Account = None, is_refresh=False, **kwargs
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
- security_config.AUTHENTICATION_SESSION_EXPIRATION
665
+ config.AUTHENTICATION_SESSION_EXPIRATION
663
666
  ),
664
667
  refresh_expiration_date=get_expiration_date(
665
- security_config.AUTHENTICATION_REFRESH_EXPIRATION
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 be separated via comma + space and in wildcard format (printer:query, dashboard:info,delete).
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.CharField(max_length=255, null=True)
690
+ permissions: list[str] = fields.JSONField(null=True)
688
691
 
689
692
  def validate(self) -> None:
690
693
  raise NotImplementedError