sanic-security 1.11.6__py3-none-any.whl → 1.16.6__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- sanic_security/authentication.py +192 -203
- sanic_security/authorization.py +111 -64
- sanic_security/configuration.py +42 -25
- sanic_security/exceptions.py +58 -24
- sanic_security/models.py +287 -159
- 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.6.dist-info/LICENSE +21 -0
- {sanic_security-1.11.6.dist-info → sanic_security-1.16.6.dist-info}/METADATA +685 -591
- sanic_security-1.16.6.dist-info/RECORD +17 -0
- {sanic_security-1.11.6.dist-info → sanic_security-1.16.6.dist-info}/WHEEL +2 -1
- sanic_security-1.16.6.dist-info/top_level.txt +1 -0
- sanic_security-1.11.6.dist-info/LICENSE +0 -661
- sanic_security-1.11.6.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
|
+
)
|