sanic-security 1.11.7__py3-none-any.whl → 1.16.7__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 +192 -203
- sanic_security/authorization.py +110 -64
- sanic_security/configuration.py +42 -25
- sanic_security/exceptions.py +58 -24
- sanic_security/models.py +287 -161
- sanic_security/oauth.py +238 -0
- sanic_security/test/server.py +174 -112
- sanic_security/test/tests.py +137 -103
- sanic_security/utils.py +67 -28
- sanic_security/verification.py +59 -46
- sanic_security-1.16.7.dist-info/LICENSE +21 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.16.7.dist-info}/METADATA +685 -591
- sanic_security-1.16.7.dist-info/RECORD +17 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.16.7.dist-info}/WHEEL +2 -1
- sanic_security-1.16.7.dist-info/top_level.txt +1 -0
- sanic_security-1.11.7.dist-info/LICENSE +0 -661
- sanic_security-1.11.7.dist-info/RECORD +0 -15
sanic_security/test/tests.py
CHANGED
@@ -7,28 +7,30 @@ import httpx
|
|
7
7
|
from sanic_security.configuration import Config
|
8
8
|
|
9
9
|
"""
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
10
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
14
|
+
in the Software without restriction, including without limitation the rights
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
17
|
+
furnished to do so, subject to the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
20
|
+
copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
|
+
SOFTWARE.
|
25
29
|
"""
|
26
30
|
|
27
31
|
|
28
32
|
class RegistrationTest(TestCase):
|
29
|
-
"""
|
30
|
-
Registration tests.
|
31
|
-
"""
|
33
|
+
"""Registration tests."""
|
32
34
|
|
33
35
|
def setUp(self):
|
34
36
|
self.client = httpx.Client()
|
@@ -58,9 +60,7 @@ class RegistrationTest(TestCase):
|
|
58
60
|
return registration_response
|
59
61
|
|
60
62
|
def test_registration(self):
|
61
|
-
"""
|
62
|
-
Account registration and login.
|
63
|
-
"""
|
63
|
+
"""Account registration and login."""
|
64
64
|
registration_response = self.register(
|
65
65
|
"account_registration@register.test",
|
66
66
|
"account_registration",
|
@@ -76,9 +76,7 @@ class RegistrationTest(TestCase):
|
|
76
76
|
assert login_response.status_code == 200, login_response.text
|
77
77
|
|
78
78
|
def test_invalid_registration(self):
|
79
|
-
"""
|
80
|
-
Registration with an intentionally invalid email, username, and phone.
|
81
|
-
"""
|
79
|
+
"""Registration with an intentionally invalid email, username, and phone."""
|
82
80
|
invalid_email_registration_response = self.register(
|
83
81
|
"invalid_register.test", "invalid_register", False, True
|
84
82
|
)
|
@@ -91,12 +89,6 @@ class RegistrationTest(TestCase):
|
|
91
89
|
assert (
|
92
90
|
invalid_phone_registration_response.status_code == 400
|
93
91
|
), invalid_phone_registration_response.text
|
94
|
-
invalid_username_registration_response = self.register(
|
95
|
-
"invalid_user@register.test", "_inVal!d_", False, True
|
96
|
-
)
|
97
|
-
assert (
|
98
|
-
invalid_username_registration_response.status_code == 400
|
99
|
-
), invalid_username_registration_response.text
|
100
92
|
too_many_characters_registration_response = self.register(
|
101
93
|
"too_long_user@register.test",
|
102
94
|
"this_username_is_too_long_to_be_registered_with",
|
@@ -108,9 +100,7 @@ class RegistrationTest(TestCase):
|
|
108
100
|
), too_many_characters_registration_response.text
|
109
101
|
|
110
102
|
def test_registration_disabled(self):
|
111
|
-
"""
|
112
|
-
Registration and login with a disabled account.
|
113
|
-
"""
|
103
|
+
"""Registration and login with a disabled account."""
|
114
104
|
registration_response = self.register(
|
115
105
|
"disabled@register.test", "disabled", True, True
|
116
106
|
)
|
@@ -122,9 +112,7 @@ class RegistrationTest(TestCase):
|
|
122
112
|
assert "DisabledError" in login_response.text, login_response.text
|
123
113
|
|
124
114
|
def test_registration_unverified(self):
|
125
|
-
"""
|
126
|
-
Registration and login with an unverified account.
|
127
|
-
"""
|
115
|
+
"""Registration and login with an unverified account."""
|
128
116
|
registration_response = self.register(
|
129
117
|
"unverified@register.test", "unverified", False, False
|
130
118
|
)
|
@@ -136,9 +124,7 @@ class RegistrationTest(TestCase):
|
|
136
124
|
assert "UnverifiedError" in login_response.text, login_response.text
|
137
125
|
|
138
126
|
def test_registration_unverified_disabled(self):
|
139
|
-
"""
|
140
|
-
Registration and login with an unverified and disabled account.
|
141
|
-
"""
|
127
|
+
"""Registration and login with an unverified and disabled account."""
|
142
128
|
registration_response = self.register(
|
143
129
|
"unverified_disabled@register.test", "unverified_disabled", True, False
|
144
130
|
)
|
@@ -151,9 +137,7 @@ class RegistrationTest(TestCase):
|
|
151
137
|
|
152
138
|
|
153
139
|
class LoginTest(TestCase):
|
154
|
-
"""
|
155
|
-
Login tests.
|
156
|
-
"""
|
140
|
+
"""Login tests."""
|
157
141
|
|
158
142
|
def setUp(self):
|
159
143
|
self.client = httpx.Client()
|
@@ -162,9 +146,7 @@ class LoginTest(TestCase):
|
|
162
146
|
self.client.close()
|
163
147
|
|
164
148
|
def test_login(self):
|
165
|
-
"""
|
166
|
-
Login with an email and password.
|
167
|
-
"""
|
149
|
+
"""Login with an email and password."""
|
168
150
|
self.client.post(
|
169
151
|
"http://127.0.0.1:8000/api/test/account",
|
170
152
|
data={"email": "email_pass@login.test", "username": "email_pass"},
|
@@ -180,9 +162,7 @@ class LoginTest(TestCase):
|
|
180
162
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
181
163
|
|
182
164
|
def test_login_with_username(self):
|
183
|
-
"""
|
184
|
-
Login with a username instead of an email and password.
|
185
|
-
"""
|
165
|
+
"""Login with a username instead of an email and password."""
|
186
166
|
self.client.post(
|
187
167
|
"http://127.0.0.1:8000/api/test/account",
|
188
168
|
data={"email": "user_pass@login.test", "username": "user_pass"},
|
@@ -198,9 +178,7 @@ class LoginTest(TestCase):
|
|
198
178
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
199
179
|
|
200
180
|
def test_invalid_login(self):
|
201
|
-
"""
|
202
|
-
Login with an intentionally incorrect password and into a non existent account.
|
203
|
-
"""
|
181
|
+
"""Login with an intentionally incorrect password and into a non-existent account."""
|
204
182
|
self.client.post(
|
205
183
|
"http://127.0.0.1:8000/api/test/account",
|
206
184
|
data={"email": "incorrect_pass@login.test", "username": "incorrect_pass"},
|
@@ -221,9 +199,7 @@ class LoginTest(TestCase):
|
|
221
199
|
), unavailable_account_login_response
|
222
200
|
|
223
201
|
def test_logout(self):
|
224
|
-
"""
|
225
|
-
Logout of logged in account and attempt to authenticate.
|
226
|
-
"""
|
202
|
+
"""Logout of logged in account and attempt to authenticate."""
|
227
203
|
self.client.post(
|
228
204
|
"http://127.0.0.1:8000/api/test/account",
|
229
205
|
data={"email": "logout@login.test", "username": "logout"},
|
@@ -240,9 +216,7 @@ class LoginTest(TestCase):
|
|
240
216
|
assert authenticate_response.status_code == 401, authenticate_response.text
|
241
217
|
|
242
218
|
def test_initial_admin_login(self):
|
243
|
-
"""
|
244
|
-
Initial admin account login and authorization.
|
245
|
-
"""
|
219
|
+
"""Initial admin account login and authorization."""
|
246
220
|
login_response = self.client.post(
|
247
221
|
"http://127.0.0.1:8000/api/test/auth/login",
|
248
222
|
auth=("admin@login.test", "admin123"),
|
@@ -251,8 +225,8 @@ class LoginTest(TestCase):
|
|
251
225
|
permitted_authorization_response = self.client.post(
|
252
226
|
"http://127.0.0.1:8000/api/test/auth/roles",
|
253
227
|
data={
|
254
|
-
"role": "
|
255
|
-
"permissions_required": "perm1:create,add, perm2:*",
|
228
|
+
"role": "Root",
|
229
|
+
"permissions_required": ["perm1:create,add", "perm2:*"],
|
256
230
|
},
|
257
231
|
)
|
258
232
|
assert (
|
@@ -260,9 +234,7 @@ class LoginTest(TestCase):
|
|
260
234
|
), permitted_authorization_response.text
|
261
235
|
|
262
236
|
def test_two_factor_login(self):
|
263
|
-
"""
|
264
|
-
Test login with two-factor authentication requirement.
|
265
|
-
"""
|
237
|
+
"""Test login with two-factor authentication requirement."""
|
266
238
|
self.client.post(
|
267
239
|
"http://127.0.0.1:8000/api/test/account",
|
268
240
|
data={"email": "two-factor@login.test", "username": "two-factor"},
|
@@ -290,11 +262,22 @@ class LoginTest(TestCase):
|
|
290
262
|
)
|
291
263
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
292
264
|
|
265
|
+
def test_anonymous_login(self):
|
266
|
+
"""Test login of anonymous user."""
|
267
|
+
anon_login_response = self.client.post(
|
268
|
+
"http://127.0.0.1:8000/api/test/auth/login/anon"
|
269
|
+
)
|
270
|
+
assert anon_login_response.status_code == 200, anon_login_response.text
|
271
|
+
authenticate_response = self.client.post(
|
272
|
+
"http://127.0.0.1:8000/api/test/auth",
|
273
|
+
)
|
274
|
+
assert authenticate_response.status_code == 200, authenticate_response.text
|
275
|
+
logout_response = self.client.post("http://127.0.0.1:8000/api/test/auth/logout")
|
276
|
+
assert logout_response.status_code == 200, logout_response.text
|
277
|
+
|
293
278
|
|
294
279
|
class VerificationTest(TestCase):
|
295
|
-
"""
|
296
|
-
Two-step verification and captcha tests.
|
297
|
-
"""
|
280
|
+
"""Two-step verification and captcha tests."""
|
298
281
|
|
299
282
|
def setUp(self):
|
300
283
|
self.client = httpx.Client()
|
@@ -303,9 +286,7 @@ class VerificationTest(TestCase):
|
|
303
286
|
self.client.close()
|
304
287
|
|
305
288
|
def test_captcha(self):
|
306
|
-
"""
|
307
|
-
Captcha request and attempt.
|
308
|
-
"""
|
289
|
+
"""Captcha request and attempt."""
|
309
290
|
captcha_request_response = self.client.get(
|
310
291
|
"http://127.0.0.1:8000/api/test/capt/request"
|
311
292
|
)
|
@@ -325,9 +306,7 @@ class VerificationTest(TestCase):
|
|
325
306
|
), captcha_attempt_response.text
|
326
307
|
|
327
308
|
def test_two_step_verification(self):
|
328
|
-
"""
|
329
|
-
Two-step verification request and attempt.
|
330
|
-
"""
|
309
|
+
"""Two-step verification request and attempt."""
|
331
310
|
self.client.post(
|
332
311
|
"http://127.0.0.1:8000/api/test/account",
|
333
312
|
data={"email": "two_step@verification.test", "username": "two_step"},
|
@@ -346,26 +325,26 @@ class VerificationTest(TestCase):
|
|
346
325
|
assert (
|
347
326
|
two_step_verification_invalid_attempt_response.status_code == 401
|
348
327
|
), two_step_verification_invalid_attempt_response.text
|
328
|
+
two_step_verification_no_email_request_response = self.client.post(
|
329
|
+
"http://127.0.0.1:8000/api/test/two-step/request",
|
330
|
+
)
|
331
|
+
assert (
|
332
|
+
two_step_verification_no_email_request_response.status_code == 200
|
333
|
+
), two_step_verification_no_email_request_response.text
|
349
334
|
two_step_verification_attempt_response = self.client.post(
|
350
335
|
"http://127.0.0.1:8000/api/test/two-step",
|
351
336
|
data={
|
352
|
-
"code": json.loads(
|
337
|
+
"code": json.loads(
|
338
|
+
two_step_verification_no_email_request_response.text
|
339
|
+
)["data"]
|
353
340
|
},
|
354
341
|
)
|
355
342
|
assert (
|
356
343
|
two_step_verification_attempt_response.status_code == 200
|
357
344
|
), two_step_verification_attempt_response.text
|
358
|
-
two_step_verification_no_email_request_response = self.client.post(
|
359
|
-
"http://127.0.0.1:8000/api/test/two-step/request",
|
360
|
-
)
|
361
|
-
assert (
|
362
|
-
two_step_verification_no_email_request_response.status_code == 200
|
363
|
-
), two_step_verification_no_email_request_response.text
|
364
345
|
|
365
346
|
def test_account_verification(self):
|
366
|
-
"""
|
367
|
-
Account registration and verification process with successful login.
|
368
|
-
"""
|
347
|
+
"""Account registration and verification process with successful login."""
|
369
348
|
registration_response = self.client.post(
|
370
349
|
"http://127.0.0.1:8000/api/test/auth/register",
|
371
350
|
data={
|
@@ -385,9 +364,7 @@ class VerificationTest(TestCase):
|
|
385
364
|
|
386
365
|
|
387
366
|
class AuthorizationTest(TestCase):
|
388
|
-
"""
|
389
|
-
Role and permissions based authorization tests.
|
390
|
-
"""
|
367
|
+
"""Role and permissions based authorization tests."""
|
391
368
|
|
392
369
|
def setUp(self):
|
393
370
|
self.client = httpx.Client()
|
@@ -396,9 +373,7 @@ class AuthorizationTest(TestCase):
|
|
396
373
|
self.client.close()
|
397
374
|
|
398
375
|
def test_permissions_authorization(self):
|
399
|
-
"""
|
400
|
-
Authorization with permissions.
|
401
|
-
"""
|
376
|
+
"""Authorization with permissions."""
|
402
377
|
self.client.post(
|
403
378
|
"http://127.0.0.1:8000/api/test/account",
|
404
379
|
data={"email": "permissions@authorization.test", "username": "permissions"},
|
@@ -411,24 +386,45 @@ class AuthorizationTest(TestCase):
|
|
411
386
|
"http://127.0.0.1:8000/api/test/auth/roles/assign",
|
412
387
|
data={
|
413
388
|
"name": "AuthTestPerms",
|
414
|
-
"permissions": "perm1:create,
|
389
|
+
"permissions": "perm1:create,update, perm2:delete,retrieve, perm3:*",
|
390
|
+
},
|
391
|
+
)
|
392
|
+
permitted_authorization_response = self.client.post(
|
393
|
+
"http://127.0.0.1:8000/api/test/auth/roles",
|
394
|
+
data={
|
395
|
+
"role": "AuthTestPerms",
|
396
|
+
"permissions_required": "perm1:create,update, perm3:retrieve",
|
415
397
|
},
|
416
398
|
)
|
399
|
+
assert (
|
400
|
+
permitted_authorization_response.status_code == 200
|
401
|
+
), permitted_authorization_response.text
|
417
402
|
permitted_authorization_response = self.client.post(
|
418
403
|
"http://127.0.0.1:8000/api/test/auth/roles",
|
419
404
|
data={
|
420
405
|
"role": "AuthTestPerms",
|
421
|
-
"permissions_required": "perm1:
|
406
|
+
"permissions_required": "perm1:retrieve, perm2:delete",
|
422
407
|
},
|
423
408
|
)
|
424
409
|
assert (
|
425
410
|
permitted_authorization_response.status_code == 200
|
426
411
|
), permitted_authorization_response.text
|
412
|
+
|
413
|
+
prohibited_authorization_response = self.client.post(
|
414
|
+
"http://127.0.0.1:8000/api/test/auth/roles",
|
415
|
+
data={
|
416
|
+
"role": "AuthTestPerms",
|
417
|
+
"permissions_required": "perm1:create,retrieve",
|
418
|
+
},
|
419
|
+
)
|
420
|
+
assert (
|
421
|
+
prohibited_authorization_response.status_code == 403
|
422
|
+
), prohibited_authorization_response.text
|
427
423
|
prohibited_authorization_response = self.client.post(
|
428
424
|
"http://127.0.0.1:8000/api/test/auth/roles",
|
429
425
|
data={
|
430
426
|
"role": "AuthTestPerms",
|
431
|
-
"permissions_required": "
|
427
|
+
"permissions_required": "perm1:delete, perm2:create",
|
432
428
|
},
|
433
429
|
)
|
434
430
|
assert (
|
@@ -436,9 +432,7 @@ class AuthorizationTest(TestCase):
|
|
436
432
|
), prohibited_authorization_response.text
|
437
433
|
|
438
434
|
def test_roles_authorization(self):
|
439
|
-
"""
|
440
|
-
Authorization with roles.
|
441
|
-
"""
|
435
|
+
"""Authorization with roles."""
|
442
436
|
self.client.post(
|
443
437
|
"http://127.0.0.1:8000/api/test/account",
|
444
438
|
data={"email": "roles@authorization.test", "username": "roles"},
|
@@ -468,11 +462,27 @@ class AuthorizationTest(TestCase):
|
|
468
462
|
prohibited_authorization_response.status_code == 403
|
469
463
|
), prohibited_authorization_response.text
|
470
464
|
|
465
|
+
def test_anonymous_authorization(self):
|
466
|
+
"""Authorization with anonymous client."""
|
467
|
+
anon_login_response = self.client.post(
|
468
|
+
"http://127.0.0.1:8000/api/test/auth/login/anon"
|
469
|
+
)
|
470
|
+
assert anon_login_response.status_code == 200, anon_login_response.text
|
471
|
+
authenticate_response = self.client.post(
|
472
|
+
"http://127.0.0.1:8000/api/test/auth",
|
473
|
+
)
|
474
|
+
assert authenticate_response.status_code == 200, authenticate_response.text
|
475
|
+
prohibited_authorization_response = self.client.post(
|
476
|
+
"http://127.0.0.1:8000/api/test/auth/roles",
|
477
|
+
data={"role": "AuthTestPerms"},
|
478
|
+
)
|
479
|
+
assert (
|
480
|
+
prohibited_authorization_response.status_code == 403
|
481
|
+
), prohibited_authorization_response.text
|
482
|
+
|
471
483
|
|
472
484
|
class MiscTest(TestCase):
|
473
|
-
"""
|
474
|
-
Miscellaneous tests that cannot be categorized.
|
475
|
-
"""
|
485
|
+
"""Miscellaneous tests that cannot be categorized."""
|
476
486
|
|
477
487
|
def setUp(self):
|
478
488
|
self.client = httpx.Client()
|
@@ -481,18 +491,14 @@ class MiscTest(TestCase):
|
|
481
491
|
self.client.close()
|
482
492
|
|
483
493
|
def test_environment_variable_load(self):
|
484
|
-
"""
|
485
|
-
Config loads environment variables.
|
486
|
-
"""
|
494
|
+
"""Config loads environment variables."""
|
487
495
|
os.environ["SANIC_SECURITY_SECRET"] = "test-secret"
|
488
496
|
security_config = Config()
|
489
497
|
security_config.load_environment_variables()
|
490
498
|
assert security_config.SECRET == "test-secret"
|
491
499
|
|
492
500
|
def test_get_associated_sessions(self):
|
493
|
-
"""
|
494
|
-
Retrieve sessions associated to logged in account.
|
495
|
-
"""
|
501
|
+
"""Retrieve sessions associated to logged in account."""
|
496
502
|
self.client.post(
|
497
503
|
"http://127.0.0.1:8000/api/test/account",
|
498
504
|
data={
|
@@ -505,9 +511,37 @@ class MiscTest(TestCase):
|
|
505
511
|
auth=("get_associated_sessions@misc.test", "password"),
|
506
512
|
)
|
507
513
|
assert login_response.status_code == 200, login_response.text
|
508
|
-
retrieve_associated_response = self.client.
|
514
|
+
retrieve_associated_response = self.client.get(
|
509
515
|
"http://127.0.0.1:8000/api/test/auth/associated"
|
510
516
|
)
|
511
517
|
assert (
|
512
518
|
retrieve_associated_response.status_code == 200
|
513
519
|
), retrieve_associated_response.text
|
520
|
+
|
521
|
+
def test_authentication_refresh(self):
|
522
|
+
"""Test automatic authentication refresh."""
|
523
|
+
self.client.post(
|
524
|
+
"http://127.0.0.1:8000/api/test/account",
|
525
|
+
data={
|
526
|
+
"email": "refreshed@misc.test",
|
527
|
+
"username": "refreshed",
|
528
|
+
},
|
529
|
+
)
|
530
|
+
login_response = self.client.post(
|
531
|
+
"http://127.0.0.1:8000/api/test/auth/login",
|
532
|
+
auth=("refreshed@misc.test", "password"),
|
533
|
+
)
|
534
|
+
assert login_response.status_code == 200, login_response.text
|
535
|
+
expire_response = self.client.post("http://127.0.0.1:8000/api/test/auth/expire")
|
536
|
+
assert expire_response.status_code == 200, expire_response.text
|
537
|
+
authenticate_refresh_response = self.client.post(
|
538
|
+
"http://127.0.0.1:8000/api/test/auth",
|
539
|
+
)
|
540
|
+
assert (
|
541
|
+
authenticate_refresh_response.status_code == 200
|
542
|
+
), authenticate_refresh_response.text
|
543
|
+
assert json.loads(authenticate_refresh_response.text)["data"]["refresh"] is True
|
544
|
+
authenticate_response = self.client.post(
|
545
|
+
"http://127.0.0.1:8000/api/test/auth",
|
546
|
+
) # Since session refresh handling is complete, it will be returned as a regular session now.
|
547
|
+
assert authenticate_response.status_code == 200, authenticate_response.text
|
sanic_security/utils.py
CHANGED
@@ -1,29 +1,44 @@
|
|
1
1
|
import datetime
|
2
2
|
import random
|
3
|
-
import
|
3
|
+
from string import ascii_uppercase, digits
|
4
4
|
|
5
|
+
from argon2 import PasswordHasher
|
6
|
+
from captcha.audio import AudioCaptcha
|
7
|
+
from captcha.image import ImageCaptcha
|
5
8
|
from sanic.request import Request
|
6
9
|
from sanic.response import json as sanic_json, HTTPResponse
|
10
|
+
from sanic.utils import str_to_bool as sanic_str_to_bool
|
7
11
|
|
12
|
+
from sanic_security.configuration import config
|
8
13
|
|
9
14
|
"""
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
Copyright (c) 2020-Present Nicholas Aidan Stewart
|
16
|
+
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
|
+
of this software and associated documentation files (the "Software"), to deal
|
19
|
+
in the Software without restriction, including without limitation the rights
|
20
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
21
|
+
copies of the Software, and to permit persons to whom the Software is
|
22
|
+
furnished to do so, subject to the following conditions:
|
23
|
+
|
24
|
+
The above copyright notice and this permission notice shall be included in all
|
25
|
+
copies or substantial portions of the Software.
|
26
|
+
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
33
|
+
SOFTWARE.
|
25
34
|
"""
|
26
35
|
|
36
|
+
image_generator: ImageCaptcha = ImageCaptcha(
|
37
|
+
190, 90, fonts=config.CAPTCHA_FONT.replace(" ", "").split(",")
|
38
|
+
)
|
39
|
+
audio_generator: AudioCaptcha = AudioCaptcha(voicedir=config.CAPTCHA_VOICE)
|
40
|
+
password_hasher: PasswordHasher = PasswordHasher()
|
41
|
+
|
27
42
|
|
28
43
|
def get_ip(request: Request) -> str:
|
29
44
|
"""
|
@@ -38,31 +53,33 @@ def get_ip(request: Request) -> str:
|
|
38
53
|
return request.remote_addr or request.ip
|
39
54
|
|
40
55
|
|
41
|
-
def get_code() -> str:
|
56
|
+
def get_code(digits_only: bool = False) -> str:
|
42
57
|
"""
|
43
58
|
Generates random code to be used for verification.
|
44
59
|
|
60
|
+
Args:
|
61
|
+
digits_only: Determines if code should only contain digits.
|
62
|
+
|
45
63
|
Returns:
|
46
64
|
code
|
47
65
|
"""
|
48
|
-
return "".join(
|
66
|
+
return "".join(
|
67
|
+
random.choice(("" if digits_only else ascii_uppercase) + digits)
|
68
|
+
for _ in range(6)
|
69
|
+
)
|
49
70
|
|
50
71
|
|
51
|
-
def
|
72
|
+
def is_expired(date):
|
52
73
|
"""
|
53
|
-
|
74
|
+
Checks if current date has surpassed the date passed into the function.
|
54
75
|
|
55
76
|
Args:
|
56
|
-
|
57
|
-
data (Any): Raw information to be used by client.
|
58
|
-
status_code (int): HTTP response code.
|
77
|
+
date: The date being checked for expiration.
|
59
78
|
|
60
79
|
Returns:
|
61
|
-
|
80
|
+
is_expired
|
62
81
|
"""
|
63
|
-
return
|
64
|
-
{"message": message, "code": status_code, "data": data}, status=status_code
|
65
|
-
)
|
82
|
+
return date and datetime.datetime.now(datetime.timezone.utc) >= date
|
66
83
|
|
67
84
|
|
68
85
|
def get_expiration_date(seconds: int) -> datetime.datetime:
|
@@ -76,7 +93,29 @@ def get_expiration_date(seconds: int) -> datetime.datetime:
|
|
76
93
|
expiration_date
|
77
94
|
"""
|
78
95
|
return (
|
79
|
-
datetime.datetime.
|
96
|
+
datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)
|
80
97
|
if seconds > 0
|
81
98
|
else None
|
82
99
|
)
|
100
|
+
|
101
|
+
|
102
|
+
def str_to_bool(val: str) -> bool:
|
103
|
+
"""Returns false if val is None instead of raising ValueError (Sanic's implementation)."""
|
104
|
+
return sanic_str_to_bool(val) if val else False
|
105
|
+
|
106
|
+
|
107
|
+
def json(message: str, data, status_code: int = 200) -> HTTPResponse:
|
108
|
+
"""
|
109
|
+
A preformatted Sanic json response.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
message (str): Message describing data or relaying human-readable information.
|
113
|
+
data (Any): Raw information to be used by client.
|
114
|
+
status_code (int): HTTP response code.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
json
|
118
|
+
"""
|
119
|
+
return sanic_json(
|
120
|
+
{"message": message, "code": status_code, "data": data}, status=status_code
|
121
|
+
)
|