sanic-security 1.13.1__py3-none-any.whl → 1.13.3__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.
@@ -7,7 +7,7 @@ from argon2.exceptions import VerifyMismatchError
7
7
  from sanic import Sanic
8
8
  from sanic.log import logger
9
9
  from sanic.request import Request
10
- from tortoise.exceptions import DoesNotExist
10
+ from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
11
11
 
12
12
  from sanic_security.configuration import config as security_config, DEFAULT_CONFIG
13
13
  from sanic_security.exceptions import (
@@ -62,32 +62,31 @@ async def register(
62
62
  Raises:
63
63
  CredentialsError
64
64
  """
65
- email_lower = validate_email(request.form.get("email").lower())
66
- if await Account.filter(email=email_lower).exists():
67
- raise CredentialsError("An account with this email may already exist.", 409)
68
- elif await Account.filter(
69
- username=validate_username(request.form.get("username"))
70
- ).exists():
71
- raise CredentialsError("An account with this username may already exist.", 409)
72
- elif (
73
- request.form.get("phone")
74
- and await Account.filter(
75
- phone=validate_phone(request.form.get("phone"))
76
- ).exists()
77
- ):
65
+ try:
66
+ account = await Account.create(
67
+ email=request.form.get("email").lower(),
68
+ username=request.form.get("username"),
69
+ password=password_hasher.hash(
70
+ validate_password(request.form.get("password"))
71
+ ),
72
+ phone=request.form.get("phone"),
73
+ verified=verified,
74
+ disabled=disabled,
75
+ )
76
+ logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
77
+ return account
78
+ except ValidationError as e:
78
79
  raise CredentialsError(
79
- "An account with this phone number may already exist.", 409
80
+ "Username must be 3-32 characters long and can only include _ or -."
81
+ if "username" in e.args[0]
82
+ else "Invalid email or phone number."
83
+ )
84
+ except IntegrityError as e:
85
+ raise CredentialsError(
86
+ f"An account with this {"username" if "username" in str(e.args[0]) else "email or phone number"} "
87
+ "may already exist.",
88
+ 409,
80
89
  )
81
- account = await Account.create(
82
- email=email_lower,
83
- username=request.form.get("username"),
84
- password=password_hasher.hash(validate_password(request.form.get("password"))),
85
- phone=request.form.get("phone"),
86
- verified=verified,
87
- disabled=disabled,
88
- )
89
- logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
90
- return account
91
90
 
92
91
 
93
92
  async def login(
@@ -266,65 +265,6 @@ def requires_authentication(arg=None):
266
265
  return decorator(arg) if callable(arg) else decorator
267
266
 
268
267
 
269
- def validate_email(email: str) -> str:
270
- """
271
- Validates email format.
272
-
273
- Args:
274
- email (str): Email being validated.
275
-
276
- Returns:
277
- email
278
-
279
- Raises:
280
- CredentialsError
281
- """
282
- if not re.search(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email):
283
- raise CredentialsError("Please use a valid email address.", 400)
284
- return email
285
-
286
-
287
- def validate_username(username: str) -> str:
288
- """
289
- Validates username format.
290
-
291
- Args:
292
- username (str): Username being validated.
293
-
294
- Returns:
295
- username
296
-
297
- Raises:
298
- CredentialsError
299
- """
300
- if not re.search(r"^[A-Za-z0-9_-]{3,32}$", username):
301
- raise CredentialsError(
302
- "Username must be between 3-32 characters and not contain any special characters other than _ or -.",
303
- 400,
304
- )
305
- return username
306
-
307
-
308
- def validate_phone(phone: str) -> str:
309
- """
310
- Validates phone number format.
311
-
312
- Args:
313
- phone (str): Phone number being validated.
314
-
315
- Returns:
316
- phone
317
-
318
- Raises:
319
- CredentialsError
320
- """
321
- if phone and not re.search(
322
- r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", phone
323
- ):
324
- raise CredentialsError("Please use a valid phone number.", 400)
325
- return phone
326
-
327
-
328
268
  def validate_password(password: str) -> str:
329
269
  """
330
270
  Validates password requirements.
@@ -370,8 +310,13 @@ def initialize_security(app: Sanic, create_root=True) -> None:
370
310
  warnings.warn("HttpOnly should be enabled.", AuditWarning)
371
311
  if not security_config.SESSION_SECURE:
372
312
  warnings.warn("Secure should be enabled.", AuditWarning)
373
- if security_config.SESSION_SAMESITE.lower() == "none":
374
- warnings.warn("SameSite should not be set to none.", AuditWarning)
313
+ if (
314
+ not security_config.SESSION_SAMESITE
315
+ or security_config.SESSION_SAMESITE.lower() == "none"
316
+ ):
317
+ warnings.warn("SameSite should not be none.", AuditWarning)
318
+ if not security_config.SESSION_DOMAIN:
319
+ warnings.warn("Domain should not be none.", AuditWarning)
375
320
  if (
376
321
  create_root
377
322
  and security_config.INITIAL_ADMIN_EMAIL
sanic_security/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import datetime
3
+ import re
3
4
  from io import BytesIO
4
5
  from typing import Union
5
6
 
@@ -9,7 +10,8 @@ from jwt import DecodeError
9
10
  from sanic.request import Request
10
11
  from sanic.response import HTTPResponse, raw
11
12
  from tortoise import fields, Model
12
- from tortoise.exceptions import DoesNotExist
13
+ from tortoise.exceptions import DoesNotExist, ValidationError
14
+ from tortoise.validators import RegexValidator
13
15
 
14
16
  from sanic_security.configuration import config as security_config
15
17
  from sanic_security.exceptions import *
@@ -103,9 +105,26 @@ class Account(BaseModel):
103
105
  roles (ManyToManyRelation[Role]): Roles associated with this account.
104
106
  """
105
107
 
106
- username: str = fields.CharField(unique=True, max_length=32)
107
- email: str = fields.CharField(unique=True, max_length=255)
108
- phone: str = fields.CharField(unique=True, max_length=14, null=True)
108
+ username: str = fields.CharField(
109
+ unique=security_config.ALLOW_LOGIN_WITH_USERNAME,
110
+ max_length=32,
111
+ validators=[RegexValidator(r"^[A-Za-z0-9_-]*$", re.I)],
112
+ )
113
+ email: str = fields.CharField(
114
+ unique=True,
115
+ max_length=255,
116
+ validators=[
117
+ RegexValidator(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", re.I)
118
+ ],
119
+ )
120
+ phone: str = fields.CharField(
121
+ unique=True,
122
+ max_length=14,
123
+ null=True,
124
+ validators=[
125
+ RegexValidator(r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", re.I)
126
+ ],
127
+ )
109
128
  password: str = fields.CharField(max_length=255)
110
129
  disabled: bool = fields.BooleanField(default=False)
111
130
  verified: bool = fields.BooleanField(default=False)
@@ -192,7 +211,7 @@ class Account(BaseModel):
192
211
  try:
193
212
  account = await Account.filter(username=username, deleted=False).get()
194
213
  return account
195
- except DoesNotExist:
214
+ except (DoesNotExist, ValidationError):
196
215
  raise NotFoundError("Account with this username does not exist.")
197
216
 
198
217
  @staticmethod
@@ -211,7 +230,7 @@ class Account(BaseModel):
211
230
  """
212
231
  try:
213
232
  account = await Account.get_via_email(credential)
214
- except NotFoundError as e:
233
+ except (NotFoundError, ValidationError) as e:
215
234
  if security_config.ALLOW_LOGIN_WITH_USERNAME:
216
235
  account = await Account.get_via_username(credential)
217
236
  else:
@@ -336,6 +355,7 @@ class Session(BaseModel):
336
355
  "id": self.id,
337
356
  "date_created": str(self.date_created),
338
357
  "expiration_date": str(self.expiration_date),
358
+ "bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
339
359
  "ip": self.ip,
340
360
  },
341
361
  security_config.SECRET,
@@ -347,19 +367,14 @@ class Session(BaseModel):
347
367
  httponly=security_config.SESSION_HTTPONLY,
348
368
  samesite=security_config.SESSION_SAMESITE,
349
369
  secure=security_config.SESSION_SECURE,
350
- )
351
- if self.expiration_date: # Overrides refresh expiration.
352
- response.cookies.get_cookie(cookie).expires = (
370
+ domain=security_config.SESSION_DOMAIN,
371
+ expires=(
353
372
  self.refresh_expiration_date
354
- if (
355
- hasattr(self, "refresh_expiration_date")
356
- and self.refresh_expiration_date
357
- )
373
+ if hasattr(self, "refresh_expiration_date")
374
+ and self.refresh_expiration_date
358
375
  else self.expiration_date
359
- )
360
-
361
- if security_config.SESSION_DOMAIN:
362
- response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
376
+ ),
377
+ )
363
378
 
364
379
  @property
365
380
  def json(self) -> dict:
@@ -368,9 +383,7 @@ class Session(BaseModel):
368
383
  "date_created": str(self.date_created),
369
384
  "date_updated": str(self.date_updated),
370
385
  "expiration_date": str(self.expiration_date),
371
- "bearer": (
372
- self.bearer.username if isinstance(self.bearer, Account) else None
373
- ),
386
+ "bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
374
387
  "active": self.active,
375
388
  }
376
389
 
@@ -490,7 +503,7 @@ class VerificationSession(Session):
490
503
  """
491
504
 
492
505
  attempts: int = fields.IntField(default=0)
493
- code: str = fields.CharField(max_length=10, default=get_code, null=True)
506
+ code: str = fields.CharField(max_length=6, default=get_code, null=True)
494
507
 
495
508
  async def check_code(self, code: str) -> None:
496
509
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.13.1
3
+ Version: 1.13.3
4
4
  Summary: An async security library for the Sanic framework.
5
5
  Author-email: Aidan Stewart <me@na-stewart.com>
6
6
  Project-URL: Documentation, https://security.na-stewart.com/
@@ -64,7 +64,7 @@ Requires-Dist: cryptography; extra == "dev"
64
64
  * [Usage](#usage)
65
65
  * [Authentication](#authentication)
66
66
  * [Captcha](#captcha)
67
- * [Two-Step Verification](#two-step-verification)
67
+ * [Two-step Verification](#two-step-verification)
68
68
  * [Authorization](#authorization)
69
69
  * [Testing](#testing)
70
70
  * [Tortoise](#tortoise)
@@ -82,8 +82,8 @@ Sanic Security is an authentication, authorization, and verification library des
82
82
  * Role based authorization with wildcard permissions
83
83
  * Two-factor authentication
84
84
  * Two-step verification
85
+ * Logging & Auditing
85
86
  * Captcha
86
- * Logging
87
87
 
88
88
  Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
89
89
 
@@ -158,7 +158,7 @@ You can load environment variables with a different prefix via `config.load_envi
158
158
  | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
159
159
  | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
160
160
  | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
161
- | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username and email. |
161
+ | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
162
162
  | **INITIAL_ADMIN_EMAIL** | admin@example.com | Email used when creating the initial admin account. |
163
163
  | **INITIAL_ADMIN_PASSWORD** | admin123 | Password used when creating the initial admin account. |
164
164
 
@@ -1,16 +1,16 @@
1
1
  sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=h0Yq2hWFv1GZoqhPQBmoJnqywGQd6fOYu7zyaqfv6wQ,14432
2
+ sanic_security/authentication.py,sha256=Rc2QqAjY17EialuML-wAThIrOxK8FKIFq927bst70IU,13218
3
3
  sanic_security/authorization.py,sha256=1SHx4cU_ibC0o_nEDDYURH_l_K6Q66M0SLzpRQrsIXc,7534
4
4
  sanic_security/configuration.py,sha256=br2hI3MHsTBh3yfPer5f3bkKSWfQdCeqfLqWmaDNVoM,5510
5
5
  sanic_security/exceptions.py,sha256=JiCaBR2kjE1Cj0fc_08y-32fqJJXa_1UCw205T4_RTY,5493
6
- sanic_security/models.py,sha256=v3tJyL420HEdZXqJCq9uPPSivuYXuQNtqf9QC9wF0TU,22274
6
+ sanic_security/models.py,sha256=XF9u3RIU76M19QzNiM4C7LoLHe4uv6tfcELGH4W7L5k,22637
7
7
  sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
8
8
  sanic_security/verification.py,sha256=js2PkqJU6o46atslJ76n-_cYoY5iz5fbyiV0OFwoySo,8668
9
9
  sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  sanic_security/test/server.py,sha256=79HaNIH1skWTrh2gIbh8WWVNxvYqPA5GlQ8AqRaCsXQ,12094
11
11
  sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
- sanic_security-1.13.1.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
- sanic_security-1.13.1.dist-info/METADATA,sha256=7oqMMHZfK9wPEeR1gQ04FvUPqCgVdETi7LES-w16uEM,23011
14
- sanic_security-1.13.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
15
- sanic_security-1.13.1.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
- sanic_security-1.13.1.dist-info/RECORD,,
12
+ sanic_security-1.13.3.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
+ sanic_security-1.13.3.dist-info/METADATA,sha256=PUPuAXTXIRtjpdBzYWl9Pz8S3m-VdJB2skIEnHqQdGM,23022
14
+ sanic_security-1.13.3.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
15
+ sanic_security-1.13.3.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
+ sanic_security-1.13.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5