sanic-security 1.12.7__py3-none-any.whl → 1.13.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.
@@ -15,6 +15,7 @@ from sanic_security.exceptions import (
15
15
  DeactivatedError,
16
16
  SecondFactorFulfilledError,
17
17
  ExpiredError,
18
+ AuditWarning,
18
19
  )
19
20
  from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
20
21
  from sanic_security.utils import get_ip
@@ -85,7 +86,6 @@ async def register(
85
86
  verified=verified,
86
87
  disabled=disabled,
87
88
  )
88
- logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
89
89
  return account
90
90
 
91
91
 
@@ -128,10 +128,13 @@ async def login(
128
128
  request, account, requires_second_factor=require_second_factor
129
129
  )
130
130
  logger.info(
131
- f"Client has logged into account {account.id} with authentication session {authentication_session.id}."
131
+ f"Client {get_ip(request)} has logged into account {account.id} with authentication session {authentication_session.id}."
132
132
  )
133
133
  return authentication_session
134
134
  except VerifyMismatchError:
135
+ logger.warning(
136
+ f"Client {get_ip(request)} has failed to log into account {account.id}."
137
+ )
135
138
  raise CredentialsError("Incorrect password.", 401)
136
139
 
137
140
 
@@ -156,8 +159,8 @@ async def logout(request: Request) -> AuthenticationSession:
156
159
  authentication_session.active = False
157
160
  await authentication_session.save(update_fields=["active"])
158
161
  logger.info(
159
- f"Client has logged out{" anonymously" if authentication_session.anonymous else
160
- f" of account {authentication_session.bearer.id}"} with authentication session {authentication_session.id}."
162
+ f"Client {get_ip(request)} has logged out {"anonymously" if authentication_session.anonymous
163
+ else f"of account {authentication_session.bearer.id}"} with authentication session {authentication_session.id}."
161
164
  )
162
165
  return authentication_session
163
166
 
@@ -190,9 +193,6 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
190
193
  await two_step_session.check_code(request.form.get("code"))
191
194
  authentication_session.requires_second_factor = False
192
195
  await authentication_session.save(update_fields=["requires_second_factor"])
193
- logger.info(
194
- f"Authentication session {authentication_session.id} second factor has been fulfilled."
195
- )
196
196
  return authentication_session
197
197
 
198
198
 
@@ -262,68 +262,6 @@ def requires_authentication(arg=None):
262
262
  return decorator(arg) if callable(arg) else decorator
263
263
 
264
264
 
265
- def attach_refresh_encoder(app: Sanic):
266
- """
267
- Automatically encodes the new/refreshed session returned during authentication when client's current session expires.
268
-
269
- Args:
270
- app: (Sanic): The main Sanic application instance.
271
- """
272
-
273
- @app.on_response
274
- async def refresh_encoder_middleware(request, response):
275
- if hasattr(request.ctx, "authentication_session"):
276
- authentication_session = request.ctx.authentication_session
277
- if authentication_session.is_refresh:
278
- authentication_session.encode(response)
279
-
280
-
281
- def create_initial_admin_account(app: Sanic) -> None:
282
- """
283
- Creates the initial admin account that can be logged into and has complete authoritative access.
284
-
285
- Args:
286
- app (Sanic): The main Sanic application instance.
287
- """
288
-
289
- @app.listener("before_server_start")
290
- async def create(app, loop):
291
- if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
292
- warnings.warn("Secret should be changed from default.")
293
- if security_config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]:
294
- warnings.warn("Initial admin email should be changed from default.")
295
- if (
296
- security_config.INITIAL_ADMIN_PASSWORD
297
- == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
298
- ):
299
- warnings.warn("Initial admin password should be changed from default.")
300
- try:
301
- role = await Role.filter(name="Admin").get()
302
- except DoesNotExist:
303
- role = await Role.create(
304
- description="Has root abilities, assign sparingly.",
305
- permissions="*:*",
306
- name="Admin",
307
- )
308
- try:
309
- account = await Account.filter(
310
- email=security_config.INITIAL_ADMIN_EMAIL
311
- ).get()
312
- await account.fetch_related("roles")
313
- if role not in account.roles:
314
- await account.roles.add(role)
315
- logger.warning("Initial admin account role has been reinstated.")
316
- except DoesNotExist:
317
- account = await Account.create(
318
- username="Admin",
319
- email=security_config.INITIAL_ADMIN_EMAIL,
320
- password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
321
- verified=True,
322
- )
323
- await account.roles.add(role)
324
- logger.info("Initial admin account created.")
325
-
326
-
327
265
  def validate_email(email: str) -> str:
328
266
  """
329
267
  Validates email format.
@@ -402,3 +340,77 @@ def validate_password(password: str) -> str:
402
340
  400,
403
341
  )
404
342
  return password
343
+
344
+
345
+ def initialize_security(app: Sanic, create_root=True) -> None:
346
+ """
347
+ Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
348
+
349
+ Args:
350
+ app (Sanic): The main Sanic application instance.
351
+ create_root (bool): Determines root account creation on initialization.
352
+ """
353
+
354
+ @app.on_response
355
+ async def refresh_encoder_middleware(request, response):
356
+ if hasattr(request.ctx, "authentication_session"):
357
+ authentication_session = request.ctx.authentication_session
358
+ if authentication_session.is_refresh:
359
+ authentication_session.encode(response)
360
+
361
+ @app.listener("before_server_start")
362
+ async def audit_configuration(app, loop):
363
+ if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
364
+ warnings.warn("Secret should be changed from default.", AuditWarning)
365
+ if not security_config.SESSION_HTTPONLY:
366
+ warnings.warn("HttpOnly should be enabled.", AuditWarning)
367
+ if not security_config.SESSION_SECURE:
368
+ warnings.warn("Secure should be enabled.", AuditWarning)
369
+ if security_config.SESSION_SAMESITE.lower() == "none":
370
+ warnings.warn("SameSite should not be set to none.", AuditWarning)
371
+ if (
372
+ create_root
373
+ and security_config.INITIAL_ADMIN_EMAIL
374
+ == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
375
+ ):
376
+ warnings.warn(
377
+ "Initial admin email should be changed from default.", AuditWarning
378
+ )
379
+ if (
380
+ create_root
381
+ and security_config.INITIAL_ADMIN_PASSWORD
382
+ == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
383
+ ):
384
+ warnings.warn(
385
+ "Initial admin password should be changed from default.", AuditWarning
386
+ )
387
+
388
+ @app.listener("before_server_start")
389
+ async def create_root_account(app, loop):
390
+ if not create_root:
391
+ return
392
+ try:
393
+ role = await Role.filter(name="Root").get()
394
+ except DoesNotExist:
395
+ role = await Role.create(
396
+ description="Has administrator abilities, assign sparingly.",
397
+ permissions="*:*",
398
+ name="Root",
399
+ )
400
+ try:
401
+ account = await Account.filter(
402
+ email=security_config.INITIAL_ADMIN_EMAIL
403
+ ).get()
404
+ await account.fetch_related("roles")
405
+ if role not in account.roles:
406
+ await account.roles.add(role)
407
+ logger.warning("Initial admin account role has been reinstated.")
408
+ except DoesNotExist:
409
+ account = await Account.create(
410
+ username="Root",
411
+ email=security_config.INITIAL_ADMIN_EMAIL,
412
+ password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
413
+ verified=True,
414
+ )
415
+ await account.roles.add(role)
416
+ logger.info("Initial admin account created.")
@@ -8,6 +8,7 @@ from tortoise.exceptions import DoesNotExist
8
8
  from sanic_security.authentication import authenticate
9
9
  from sanic_security.exceptions import AuthorizationError, AnonymousError
10
10
  from sanic_security.models import Role, Account, AuthenticationSession
11
+ from sanic_security.utils import get_ip
11
12
 
12
13
  """
13
14
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -66,6 +67,10 @@ async def check_permissions(
66
67
  ):
67
68
  if fnmatch(required_permission, role_permission):
68
69
  return authentication_session
70
+ logger.warning(
71
+ f"Client {get_ip(request)} with account {authentication_session.bearer.id} has insufficient permissions for "
72
+ "attempted action."
73
+ )
69
74
  raise AuthorizationError("Insufficient permissions required for this action.")
70
75
 
71
76
 
@@ -98,6 +103,10 @@ async def check_roles(request: Request, *required_roles: str) -> AuthenticationS
98
103
  for role in roles:
99
104
  if role.name in required_roles:
100
105
  return authentication_session
106
+ logger.warning(
107
+ f"Client {get_ip(request)} with account {authentication_session.bearer.id} has insufficient roles for "
108
+ "attempted action."
109
+ )
101
110
  raise AuthorizationError("Insufficient roles required for this action.")
102
111
 
103
112
 
@@ -120,7 +129,6 @@ async def assign_role(
120
129
  description=description, permissions=permissions, name=name
121
130
  )
122
131
  await account.roles.add(role)
123
- logger.info(f"Role {role.id} has been assigned to account {account.id}.")
124
132
  return role
125
133
 
126
134
 
@@ -27,11 +27,11 @@ SOFTWARE.
27
27
  DEFAULT_CONFIG = {
28
28
  "SECRET": "This is a big secret. Shhhhh",
29
29
  "PUBLIC_SECRET": None,
30
- "SESSION_SAMESITE": "strict",
30
+ "SESSION_SAMESITE": "Strict",
31
31
  "SESSION_SECURE": True,
32
32
  "SESSION_HTTPONLY": True,
33
33
  "SESSION_DOMAIN": None,
34
- "SESSION_PREFIX": "token",
34
+ "SESSION_PREFIX": "tkn",
35
35
  "SESSION_ENCODING_ALGORITHM": "HS256",
36
36
  "MAX_CHALLENGE_ATTEMPTS": 5,
37
37
  "CAPTCHA_SESSION_EXPIRATION": 60,
@@ -206,3 +206,9 @@ class AnonymousError(AuthorizationError):
206
206
 
207
207
  def __init__(self):
208
208
  super().__init__("Session is anonymous.")
209
+
210
+
211
+ class AuditWarning(Warning):
212
+ """
213
+ Raised when configuration is invalid.
214
+ """
sanic_security/models.py CHANGED
@@ -6,7 +6,6 @@ from typing import Union
6
6
  import jwt
7
7
  from captcha.image import ImageCaptcha
8
8
  from jwt import DecodeError
9
- from sanic.log import logger
10
9
  from sanic.request import Request
11
10
  from sanic.response import HTTPResponse, raw
12
11
  from tortoise import fields, Model
@@ -142,7 +141,6 @@ class Account(BaseModel):
142
141
  else:
143
142
  self.disabled = True
144
143
  await self.save(update_fields=["disabled"])
145
- logger.info(f"Account {self.id} has been disabled.")
146
144
 
147
145
  @property
148
146
  def json(self) -> dict:
@@ -320,7 +318,6 @@ class Session(BaseModel):
320
318
  if self.active:
321
319
  self.active = False
322
320
  await self.save(update_fields=["active"])
323
- logger.info(f"Session {self.id} has been deactivated.")
324
321
  else:
325
322
  raise DeactivatedError("Session is already deactivated.", 403)
326
323
 
@@ -516,9 +513,6 @@ class VerificationSession(Session):
516
513
  else:
517
514
  raise MaxedOutChallengeError()
518
515
  else:
519
- logger.info(
520
- f"Client has completed verification session {self.id} challenge."
521
- )
522
516
  await self.deactivate()
523
517
 
524
518
  @classmethod
@@ -530,9 +524,7 @@ class VerificationSession(Session):
530
524
 
531
525
 
532
526
  class TwoStepSession(VerificationSession):
533
- """
534
- Validates a client using a code sent via email or text.
535
- """
527
+ """Validates a client using a code sent via email or text."""
536
528
 
537
529
  @classmethod
538
530
  async def new(cls, request: Request, account: Account, **kwargs):
@@ -550,9 +542,7 @@ class TwoStepSession(VerificationSession):
550
542
 
551
543
 
552
544
  class CaptchaSession(VerificationSession):
553
- """
554
- Validates a client with a captcha challenge.
555
- """
545
+ """Validates a client with a captcha challenge."""
556
546
 
557
547
  @classmethod
558
548
  async def new(cls, request: Request, **kwargs):
@@ -612,6 +602,9 @@ class AuthenticationSession(Session):
612
602
  """
613
603
  Refreshes session if expired and within refresh date.
614
604
 
605
+ Args:
606
+ request (Request): Sanic request parameter.
607
+
615
608
  Raises:
616
609
  DeletedError
617
610
  ExpiredError
@@ -633,7 +626,6 @@ class AuthenticationSession(Session):
633
626
  ):
634
627
  self.active = False
635
628
  await self.save(update_fields=["active"])
636
- logger.info(f"Client has refreshed authentication session {self.id}.")
637
629
  return await self.new(request, self.bearer, True)
638
630
  else:
639
631
  raise e
@@ -10,9 +10,8 @@ from sanic_security.authentication import (
10
10
  register,
11
11
  requires_authentication,
12
12
  logout,
13
- create_initial_admin_account,
14
13
  fulfill_second_factor,
15
- attach_refresh_encoder,
14
+ initialize_security,
16
15
  )
17
16
  from sanic_security.authorization import (
18
17
  assign_role,
@@ -308,7 +307,6 @@ register_tortoise(
308
307
  modules={"models": ["sanic_security.models"]},
309
308
  generate_schemas=True,
310
309
  )
311
- attach_refresh_encoder(app)
312
- create_initial_admin_account(app)
310
+ initialize_security(app, True)
313
311
  if __name__ == "__main__":
314
- app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
312
+ app.run(host="127.0.0.1", port=8000, workers=1)
@@ -231,7 +231,7 @@ class LoginTest(TestCase):
231
231
  permitted_authorization_response = self.client.post(
232
232
  "http://127.0.0.1:8000/api/test/auth/roles",
233
233
  data={
234
- "role": "Admin",
234
+ "role": "Root",
235
235
  "permissions_required": "perm1:create,add, perm2:*",
236
236
  },
237
237
  )
@@ -1,7 +1,6 @@
1
1
  import functools
2
2
  from contextlib import suppress
3
3
 
4
- from sanic.log import logger
5
4
  from sanic.request import Request
6
5
 
7
6
  from sanic_security.exceptions import (
@@ -85,7 +84,7 @@ async def two_step_verification(request: Request) -> TwoStepSession:
85
84
  MaxedOutChallengeError
86
85
 
87
86
  Returns:
88
- two_step_session
87
+ two_step_session
89
88
  """
90
89
  two_step_session = await TwoStepSession.decode(request)
91
90
  two_step_session.validate()
@@ -157,7 +156,6 @@ async def verify_account(request: Request) -> TwoStepSession:
157
156
  await two_step_session.check_code(request.form.get("code"))
158
157
  two_step_session.bearer.verified = True
159
158
  await two_step_session.bearer.save(update_fields=["verified"])
160
- logger.info(f"Account {two_step_session.bearer.id} has been verified.")
161
159
  return two_step_session
162
160
 
163
161
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.12.7
3
+ Version: 1.13.0
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)
@@ -145,12 +145,12 @@ You can load environment variables with a different prefix via `config.load_envi
145
145
  |---------------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
146
146
  | **SECRET** | This is a big secret. Shhhhh | The secret used for generating and signing JWTs. This should be a string unique to your application. Keep it safe. |
147
147
  | **PUBLIC_SECRET** | None | The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application. |
148
- | **SESSION_SAMESITE** | strict | The SameSite attribute of session cookies. |
148
+ | **SESSION_SAMESITE** | Strict | The SameSite attribute of session cookies. |
149
149
  | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
150
150
  | **SESSION_HTTPONLY** | True | The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing. |
151
151
  | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
152
152
  | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
153
- | **SESSION_PREFIX** | token | Prefix attached to the beginning of session cookies. |
153
+ | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
154
154
  | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
155
155
  | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
156
156
  | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
@@ -166,19 +166,16 @@ You can load environment variables with a different prefix via `config.load_envi
166
166
  Sanic Security's authentication and verification functionality is session based. A new session will be created for the user after the user logs in or requests some form of verification (two-step, captcha). The session data is then encoded into a JWT and stored on a cookie on the user’s browser. The session cookie is then sent
167
167
  along with every subsequent request. The server can then compare the session stored on the cookie against the session information stored in the database to verify user’s identity and send a response with the corresponding state.
168
168
 
169
- The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
170
-
171
- ## Authentication
172
-
173
- * Initial Administrator Account
174
-
175
- Creates root account if it doesn't exist, you should modify its credentials in config!
176
-
169
+ * Initialize sanic-security as follows:
177
170
  ```python
178
- create_initial_admin_account(app)
171
+ initialize_security(app)
179
172
  if __name__ == "__main__":
180
- app.run(host="127.0.0.1", port=8000)
173
+ app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
181
174
  ```
175
+
176
+ The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
177
+
178
+ ## Authentication
182
179
 
183
180
  * Registration (With two-step account verification)
184
181
 
@@ -321,17 +318,6 @@ async def on_authenticate(request):
321
318
  return response
322
319
  ```
323
320
 
324
- * Refresh Encoder
325
-
326
- A new/refreshed session is returned during authentication when the client's current session expires and it
327
- requires encoding. This should be be done automatically via middleware.
328
-
329
- ```python
330
- attach_refresh_encoder(app)
331
- if __name__ == "__main__":
332
- app.run(host="127.0.0.1", port=8000)
333
- ```
334
-
335
321
  ## Captcha
336
322
 
337
323
  A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
@@ -0,0 +1,16 @@
1
+ sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sanic_security/authentication.py,sha256=ZY4WJDUXbwDdaH_2Ovc9gkR1jCgw52yB9enYp_1LypM,14334
3
+ sanic_security/authorization.py,sha256=XbRrnsx-Yqpiemf3bn_djIYIe1khdnfToB7DsBheLtk,7338
4
+ sanic_security/configuration.py,sha256=br2hI3MHsTBh3yfPer5f3bkKSWfQdCeqfLqWmaDNVoM,5510
5
+ sanic_security/exceptions.py,sha256=JiCaBR2kjE1Cj0fc_08y-32fqJJXa_1UCw205T4_RTY,5493
6
+ sanic_security/models.py,sha256=v3tJyL420HEdZXqJCq9uPPSivuYXuQNtqf9QC9wF0TU,22274
7
+ sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
8
+ sanic_security/verification.py,sha256=ebT7QaytHAsw-IKA13W9wyCoqoBAYKgmFA1QJ80N2bE,7476
9
+ sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ sanic_security/test/server.py,sha256=DmwP5dBs2Sq2qk4UZgWbAqzm96YXvLGI9jWeqzluy_c,12082
11
+ sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
+ sanic_security-1.13.0.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
+ sanic_security-1.13.0.dist-info/METADATA,sha256=VeYHXoqKPrbNAfub2NW3RT78Rt1WSUBCKj2gVJrHplE,23000
14
+ sanic_security-1.13.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
15
+ sanic_security-1.13.0.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
+ sanic_security-1.13.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=CzjbVh7uDjBs22oQCklcbmp5gTDW17AoYaa3lmyPnGk,13783
3
- sanic_security/authorization.py,sha256=80vuM_2oNgfhOMuct89R6UBhDYmSWQzIgocKxhKzReE,7030
4
- sanic_security/configuration.py,sha256=MKxYjq1q9RBRX2cMJkIe87ke0mLKa69RWoQ5MhVciho,5512
5
- sanic_security/exceptions.py,sha256=MTPF4tm_68Nmf_z06RHH_6DTiC_CNiLER1jzEoW1dFk,5398
6
- sanic_security/models.py,sha256=zWn9zXl-vwP8qJ-DzBuNNG7MCl1ggZX6yb6Zgh3Jfcs,22601
7
- sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
8
- sanic_security/verification.py,sha256=rNyZk_J53dOzk8qy5iG3yiMRuRcGaYeHwIwnFDH9TWw,7582
9
- sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- sanic_security/test/server.py,sha256=-dQAB75C-l9kfN7PnaYNDOLiLXaqg0euZAWF5oXMxeE,12164
11
- sanic_security/test/tests.py,sha256=jUZ5kgQF4rFx1bImKqHYR7UeoYAfjtNNXSQYTMfzlg4,21994
12
- sanic_security-1.12.7.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
- sanic_security-1.12.7.dist-info/METADATA,sha256=ZGVTjtv-OVMrs_3Ac__jKE1JgdhtIAyhMCW2n3VUW3Y,23393
14
- sanic_security-1.12.7.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
15
- sanic_security-1.12.7.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
- sanic_security-1.12.7.dist-info/RECORD,,