sanic-security 1.16.11__py3-none-any.whl → 1.17.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.
@@ -1,362 +1,379 @@
1
- import functools
2
- import re
3
- import warnings
4
-
5
- from argon2.exceptions import VerificationError, InvalidHashError
6
- from sanic import Sanic
7
- from sanic.log import logger
8
- from sanic.request import Request
9
- from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
10
-
11
- from sanic_security.configuration import config, DEFAULT_CONFIG
12
- from sanic_security.exceptions import (
13
- CredentialsError,
14
- DeactivatedError,
15
- SecondFactorFulfilledError,
16
- ExpiredError,
17
- AuditWarning,
18
- )
19
- from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
20
- from sanic_security.utils import get_ip, password_hasher
21
-
22
- """
23
- Copyright (c) 2020-present Nicholas Aidan Stewart
24
-
25
- Permission is hereby granted, free of charge, to any person obtaining a copy
26
- of this software and associated documentation files (the "Software"), to deal
27
- in the Software without restriction, including without limitation the rights
28
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29
- copies of the Software, and to permit persons to whom the Software is
30
- furnished to do so, subject to the following conditions:
31
-
32
- The above copyright notice and this permission notice shall be included in all
33
- copies or substantial portions of the Software.
34
-
35
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41
- SOFTWARE.
42
- """
43
-
44
-
45
- async def register(
46
- request: Request, verified: bool = False, disabled: bool = False
47
- ) -> Account:
48
- """
49
- Registers a new account that can be logged into.
50
-
51
- Args:
52
- request (Request): Sanic request parameter. Request body should contain form-data with the following argument(s): email, username, password, phone (including country code).
53
- verified (bool): Sets the verification requirement for the account being registered.
54
- disabled (bool): Renders the account being registered unusable.
55
-
56
- Returns:
57
- account
58
-
59
- Raises:
60
- CredentialsError
61
- """
62
- try:
63
- account = await Account.create(
64
- email=request.form.get("email").lower(),
65
- username=request.form.get("username"),
66
- password=password_hasher.hash(
67
- validate_password(request.form.get("password"))
68
- ),
69
- phone=request.form.get("phone"),
70
- verified=verified,
71
- disabled=disabled,
72
- )
73
- logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
74
- return account
75
- except ValidationError as e:
76
- raise CredentialsError(
77
- "Username must be 3-32 characters long."
78
- if "username" in e.args[0]
79
- else "Invalid email or phone number."
80
- )
81
- except IntegrityError as e:
82
- raise CredentialsError(
83
- f"An account with this {"username" if "username" in str(e.args[0]) else "email or phone number"} "
84
- "may already exist.",
85
- 409,
86
- )
87
-
88
-
89
- async def login(
90
- request: Request,
91
- account: Account = None,
92
- require_second_factor: bool = False,
93
- password: str = None,
94
- ) -> AuthenticationSession:
95
- """
96
- Login with email or username (if enabled) and password.
97
-
98
- Args:
99
- request (Request): Sanic request parameter, login credentials are retrieved via the authorization header.
100
- account (Account): Account being logged into, overrides account retrieved via email or username.
101
- require_second_factor (bool): Determines authentication session second factor requirement on login.
102
- password (str): Overrides user's password attempt retrieved via the authorization header.
103
-
104
- Returns:
105
- authentication_session
106
-
107
- Raises:
108
- CredentialsError
109
- NotFoundError
110
- DeletedError
111
- UnverifiedError
112
- DisabledError
113
- """
114
- if not account:
115
- account, password = await Account.get_via_header(request)
116
- elif not password:
117
- raise CredentialsError("Password parameter is empty.")
118
- try:
119
- password_hasher.verify(account.password, password)
120
- if password_hasher.check_needs_rehash(account.password):
121
- account.password = password_hasher.hash(password)
122
- await account.save(update_fields=["password"])
123
- account.validate()
124
- authentication_session = await AuthenticationSession.new(
125
- request, account, requires_second_factor=require_second_factor
126
- )
127
- logger.info(
128
- f"Client {get_ip(request)} has logged in with authentication session {authentication_session.id}."
129
- )
130
- return authentication_session
131
- except (VerificationError, InvalidHashError):
132
- logger.warning(
133
- f"Client {get_ip(request)} has failed to log into account {account.id}."
134
- )
135
- raise CredentialsError("Incorrect password.", 401)
136
-
137
-
138
- async def logout(request: Request) -> AuthenticationSession:
139
- """
140
- Deactivates client's authentication session.
141
-
142
- Args:
143
- request (Request): Sanic request parameter.
144
-
145
- Raises:
146
- NotFoundError
147
- JWTDecodeError
148
- DeactivatedError
149
-
150
- Returns:
151
- authentication_session
152
- """
153
- authentication_session = await AuthenticationSession.decode(request)
154
- if not authentication_session.active:
155
- raise DeactivatedError("Already logged out.", 403)
156
- authentication_session.active = False
157
- await authentication_session.save(update_fields=["active"])
158
- logger.info(
159
- f"Client {get_ip(request)} has logged out with authentication session {authentication_session.id}."
160
- )
161
- return authentication_session
162
-
163
-
164
- async def fulfill_second_factor(request: Request) -> AuthenticationSession:
165
- """
166
- Fulfills client authentication session's second factor requirement via two-step session code.
167
-
168
- Args:
169
- request (Request): Sanic request parameter. Request body should contain form-data with the following argument(s): code.
170
-
171
- Raises:
172
- NotFoundError
173
- JWTDecodeError
174
- DeletedError
175
- ExpiredError
176
- DeactivatedError
177
- ChallengeError
178
- MaxedOutChallengeError
179
- SecondFactorFulfilledError
180
-
181
- Returns:
182
- authentication_session
183
- """
184
- authentication_session = await AuthenticationSession.decode(request)
185
- if not authentication_session.requires_second_factor:
186
- raise SecondFactorFulfilledError
187
- two_step_session = await TwoStepSession.decode(request)
188
- two_step_session.validate()
189
- await two_step_session.check_code(request.form.get("code"))
190
- authentication_session.requires_second_factor = False
191
- await authentication_session.save(update_fields=["requires_second_factor"])
192
- logger.info(
193
- f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} second factor."
194
- )
195
- return authentication_session
196
-
197
-
198
- async def authenticate(request: Request) -> AuthenticationSession:
199
- """
200
- Validates client's authentication session and account. New/Refreshed session automatically returned if client's
201
- session expired during authentication.
202
-
203
- Args:
204
- request (Request): Sanic request parameter.
205
-
206
- Returns:
207
- authentication_session
208
-
209
- Raises:
210
- NotFoundError
211
- JWTDecodeError
212
- DeletedError
213
- DeactivatedError
214
- UnverifiedError
215
- DisabledError
216
- SecondFactorRequiredError
217
- ExpiredError
218
- """
219
- authentication_session = await AuthenticationSession.decode(request)
220
- try:
221
- authentication_session.validate()
222
- if not authentication_session.anonymous:
223
- authentication_session.bearer.validate()
224
- except ExpiredError:
225
- authentication_session = await authentication_session.refresh(request)
226
- authentication_session.is_refresh = True
227
- request.ctx.session = authentication_session
228
- return authentication_session
229
-
230
-
231
- def requires_authentication(arg=None):
232
- """
233
- Validates client's authentication session and account. New/Refreshed session automatically returned if client's
234
- session expired during authentication.
235
-
236
- Example:
237
- This method is not called directly and instead used as a decorator:
238
-
239
- @app.post('api/authenticate')
240
- @requires_authentication
241
- async def on_authenticate(request):
242
- return text('User is authenticated!')
243
-
244
- Raises:
245
- NotFoundError
246
- JWTDecodeError
247
- DeletedError
248
- DeactivatedError
249
- UnverifiedError
250
- DisabledError
251
- SecondFactorRequiredError
252
- ExpiredError
253
- """
254
-
255
- def decorator(func):
256
- @functools.wraps(func)
257
- async def wrapper(request, *args, **kwargs):
258
- await authenticate(request)
259
- return await func(request, *args, **kwargs)
260
-
261
- return wrapper
262
-
263
- return decorator(arg) if callable(arg) else decorator
264
-
265
-
266
- def validate_password(password: str) -> str:
267
- """
268
- Validates password requirements.
269
-
270
- Args:
271
- password (str): Password being validated.
272
-
273
- Returns:
274
- password
275
-
276
- Raises:
277
- CredentialsError
278
- """
279
- if not re.search(r"^(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&+=!]).*$", password):
280
- raise CredentialsError(
281
- "Password must contain one capital letter, one number, and one special character",
282
- 400,
283
- )
284
- return password
285
-
286
-
287
- def initialize_security(app: Sanic, create_root: bool = True) -> None:
288
- """
289
- Audits configuration, creates root administrator account, and attaches session middleware.
290
-
291
- Args:
292
- app (Sanic): Sanic application instance.
293
- create_root (bool): Determines root account creation on initialization.
294
- """
295
-
296
- @app.listener("before_server_start")
297
- async def audit_configuration(app, loop):
298
- if config.SECRET == DEFAULT_CONFIG["SECRET"]:
299
- warnings.warn("Secret should be changed from default.", AuditWarning, 2)
300
- if not config.SESSION_HTTPONLY:
301
- warnings.warn("HttpOnly should be enabled.", AuditWarning, 2)
302
- if not config.SESSION_SECURE:
303
- warnings.warn("Secure should be enabled.", AuditWarning, 2)
304
- if not config.SESSION_SAMESITE or config.SESSION_SAMESITE.lower() == "none":
305
- warnings.warn("SameSite should not be none.", AuditWarning, 2)
306
- if not config.SESSION_DOMAIN:
307
- warnings.warn("Domain should not be none.", AuditWarning, 2)
308
- if (
309
- create_root
310
- and config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
311
- ):
312
- warnings.warn(
313
- "Initial admin email should be changed from default.", AuditWarning, 2
314
- )
315
- if (
316
- create_root
317
- and config.INITIAL_ADMIN_PASSWORD
318
- == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
319
- ):
320
- warnings.warn(
321
- "Initial admin password should be changed from default.",
322
- AuditWarning,
323
- 2,
324
- )
325
-
326
- @app.listener("before_server_start")
327
- async def create_root_account(app, loop):
328
- if not create_root:
329
- return
330
- try:
331
- role = await Role.filter(name="Root").get()
332
- except DoesNotExist:
333
- role = await Role.create(
334
- description="Has administrator abilities, assign sparingly.",
335
- permissions=["*:*"],
336
- name="Root",
337
- )
338
- try:
339
- account = await Account.filter(email=config.INITIAL_ADMIN_EMAIL).get()
340
- await account.fetch_related("roles")
341
- if role not in account.roles:
342
- await account.roles.add(role)
343
- logger.warning("Initial admin account role has been reinstated.")
344
- except DoesNotExist:
345
- account = await Account.create(
346
- username="Root",
347
- email=config.INITIAL_ADMIN_EMAIL,
348
- password=password_hasher.hash(config.INITIAL_ADMIN_PASSWORD),
349
- verified=True,
350
- )
351
- await account.roles.add(role)
352
- logger.info("Initial admin account created.")
353
-
354
- @app.on_response
355
- async def session_middleware(request, response):
356
- if hasattr(request.ctx, "session"):
357
- if getattr(request.ctx.session, "is_refresh", False):
358
- request.ctx.session.encode(response)
359
- elif not request.ctx.session.active:
360
- response.delete_cookie(
361
- f"{config.SESSION_PREFIX}_{request.ctx.session.__class__.__name__[:7].lower()}"
362
- )
1
+ import functools
2
+ import re
3
+ import warnings
4
+
5
+ from argon2.exceptions import VerificationError, InvalidHashError
6
+ from sanic import Sanic
7
+ from sanic.log import logger
8
+ from sanic.request import Request
9
+ from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
10
+
11
+ from sanic_security.configuration import config, DEFAULT_CONFIG
12
+ from sanic_security.exceptions import (
13
+ CredentialsError,
14
+ DeactivatedError,
15
+ ExpiredError,
16
+ AuditWarning,
17
+ )
18
+ from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
19
+ from sanic_security.utils import get_ip, password_hasher
20
+
21
+ """
22
+ Copyright (c) 2020-present Nicholas Aidan Stewart
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
41
+ """
42
+
43
+
44
+ async def register(
45
+ request: Request, verified: bool = False, disabled: bool = False
46
+ ) -> Account:
47
+ """
48
+ Registers a new account that can be logged into.
49
+
50
+ Args:
51
+ request (Request): Sanic request parameter. Request body should contain form-data with the following argument(s): email, username, password, phone (including country code).
52
+ verified (bool): Sets the verification requirement for the account being registered.
53
+ disabled (bool): Renders the account being registered unusable until manual activation.
54
+
55
+ Returns:
56
+ account
57
+
58
+ Raises:
59
+ CredentialsError
60
+ """
61
+ try:
62
+ account = await Account.create(
63
+ email=request.form.get("email").lower(),
64
+ username=request.form.get("username"),
65
+ password=password_hasher.hash(
66
+ validate_password(request.form.get("password"))
67
+ ),
68
+ phone=request.form.get("phone"),
69
+ verified=verified,
70
+ disabled=disabled,
71
+ )
72
+ logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
73
+ return account
74
+ except ValidationError as e:
75
+ raise CredentialsError(
76
+ "Username must be 3-32 characters long."
77
+ if "username" in e.args[0]
78
+ else "Invalid email or phone number."
79
+ )
80
+ except IntegrityError as e:
81
+ raise CredentialsError(
82
+ f"An account with this {"username" if "username" in str(e.args[0]) else "email or phone number"} "
83
+ "may already exist.",
84
+ 409,
85
+ )
86
+
87
+
88
+ async def login(
89
+ request: Request,
90
+ *,
91
+ require_second_factor: bool = False,
92
+ email: str = None,
93
+ password: str = None,
94
+ ) -> AuthenticationSession:
95
+ """
96
+ Login with email or username (if enabled) and password.
97
+
98
+ Args:
99
+ request (Request): Sanic request parameter, login credentials are retrieved via the authorization header.
100
+ require_second_factor (bool): Determines authentication session second factor requirement on login.
101
+ email (str): Email (or username) of account being logged into, overrides account retrieved via authorization header.
102
+ password (str): Overrides user's password attempt retrieved via the authorization header.
103
+
104
+ Returns:
105
+ authentication_session
106
+
107
+ Raises:
108
+ CredentialsError
109
+ NotFoundError
110
+ DeletedError
111
+ UnverifiedError
112
+ DisabledError
113
+ """
114
+ if not email:
115
+ account, password = await Account.get_via_header(request)
116
+ elif not password:
117
+ raise CredentialsError("Password parameter is empty.")
118
+ else:
119
+ account = await Account.get_via_credential(email)
120
+ try:
121
+ password_hasher.verify(account.password, password)
122
+ if password_hasher.check_needs_rehash(account.password):
123
+ account.password = password_hasher.hash(password)
124
+ await account.save(update_fields=["password"])
125
+ account.validate()
126
+ authentication_session = await AuthenticationSession.new(
127
+ request, account, requires_second_factor=require_second_factor
128
+ )
129
+ logger.info(
130
+ f"Client {get_ip(request)} has logged in with authentication session {authentication_session.id}."
131
+ )
132
+ return authentication_session
133
+ except (VerificationError, InvalidHashError):
134
+ logger.warning(
135
+ f"Client {get_ip(request)} has failed to log into account {account.id}."
136
+ )
137
+ raise CredentialsError("Incorrect password.", 401)
138
+
139
+
140
+ async def logout(request: Request) -> AuthenticationSession:
141
+ """
142
+ Deactivates client's authentication session.
143
+
144
+ Args:
145
+ request (Request): Sanic request parameter.
146
+
147
+ Raises:
148
+ NotFoundError
149
+ JWTDecodeError
150
+ DeactivatedError
151
+
152
+ Returns:
153
+ authentication_session
154
+ """
155
+ authentication_session = await AuthenticationSession.decode(request)
156
+ if not authentication_session.active:
157
+ raise DeactivatedError("Already logged out.", 403)
158
+ authentication_session.active = False
159
+ await authentication_session.save(update_fields=["active"])
160
+ logger.info(
161
+ f"Client {get_ip(request)} has logged out with authentication session {authentication_session.id}."
162
+ )
163
+ return authentication_session
164
+
165
+
166
+ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
167
+ """
168
+ Fulfills client authentication session's second factor requirement via two-step session code.
169
+
170
+ Args:
171
+ request (Request): Sanic request parameter. Request body should contain form-data with the following argument(s): code.
172
+
173
+ Raises:
174
+ NotFoundError
175
+ JWTDecodeError
176
+ DeletedError
177
+ ExpiredError
178
+ DeactivatedError
179
+ ChallengeError
180
+ MaxedOutChallengeError
181
+
182
+ Returns:
183
+ authentication_session
184
+ """
185
+ authentication_session = await AuthenticationSession.decode(request)
186
+ if not authentication_session.requires_second_factor:
187
+ raise DeactivatedError("Session second factor requirement already met.", 403)
188
+ two_step_session = await TwoStepSession.decode(request)
189
+ two_step_session.validate()
190
+ await two_step_session.check_code(request.form.get("code"))
191
+ authentication_session.requires_second_factor = False
192
+ await authentication_session.save(update_fields=["requires_second_factor"])
193
+ logger.info(
194
+ f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} second factor."
195
+ )
196
+ return authentication_session
197
+
198
+
199
+ async def authenticate(request: Request) -> AuthenticationSession:
200
+ """
201
+ Validates client's authentication session and account. New/Refreshed session automatically returned if client's
202
+ session expired during authentication.
203
+
204
+ Args:
205
+ request (Request): Sanic request parameter.
206
+
207
+ Returns:
208
+ authentication_session
209
+
210
+ Raises:
211
+ NotFoundError
212
+ JWTDecodeError
213
+ DeletedError
214
+ DeactivatedError
215
+ UnverifiedError
216
+ DisabledError
217
+ SecondFactorRequiredError
218
+ ExpiredError
219
+ CredentialsError
220
+ """
221
+
222
+ authentication_jwt = AuthenticationSession.decode_raw(request)
223
+ if not await AuthenticationSession.filter(
224
+ bearer=authentication_jwt["bearer"],
225
+ ip=get_ip(request),
226
+ user_agent=request.headers.get("user-agent"),
227
+ active=True,
228
+ deleted=False,
229
+ ).exists():
230
+ logger.warning(
231
+ f"Unrecognized client {get_ip(request)} attempted to utilize authentication session {authentication_jwt["id"]}."
232
+ )
233
+ raise CredentialsError("Client is unrecognized.")
234
+ authentication_session = await AuthenticationSession.decode(
235
+ request, authentication_jwt
236
+ )
237
+ try:
238
+ authentication_session.validate()
239
+ if not authentication_session.anonymous:
240
+ authentication_session.bearer.validate()
241
+ except ExpiredError:
242
+ authentication_session = await authentication_session.refresh(request)
243
+ authentication_session.is_refresh = True
244
+ request.ctx.session = authentication_session
245
+ return authentication_session
246
+
247
+
248
+ def requires_authentication(arg=None):
249
+ """
250
+ Validates client's authentication session and account. New/Refreshed session automatically returned if client's
251
+ session expired during authentication.
252
+
253
+ Example:
254
+ This method is not called directly and instead used as a decorator:
255
+
256
+ @app.post('api/authenticate')
257
+ @requires_authentication
258
+ async def on_authenticate(request):
259
+ return text('User is authenticated!')
260
+
261
+ Raises:
262
+ NotFoundError
263
+ JWTDecodeError
264
+ DeletedError
265
+ DeactivatedError
266
+ UnverifiedError
267
+ DisabledError
268
+ SecondFactorRequiredError
269
+ ExpiredError
270
+ """
271
+
272
+ def decorator(func):
273
+ @functools.wraps(func)
274
+ async def wrapper(request, *args, **kwargs):
275
+ await authenticate(request)
276
+ return await func(request, *args, **kwargs)
277
+
278
+ return wrapper
279
+
280
+ return decorator(arg) if callable(arg) else decorator
281
+
282
+
283
+ def validate_password(password: str) -> str:
284
+ """
285
+ Validates password formatting requirements.
286
+
287
+ Args:
288
+ password (str): Password being validated.
289
+
290
+ Returns:
291
+ password
292
+
293
+ Raises:
294
+ CredentialsError
295
+ """
296
+ if not re.search(r"^(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&+=!]).*$", password):
297
+ raise CredentialsError(
298
+ "Password must contain one capital letter, one number, and one special character",
299
+ 400,
300
+ )
301
+ return password
302
+
303
+
304
+ def initialize_security(app: Sanic, create_root: bool = True) -> None:
305
+ """
306
+ Audits configuration, creates root administrator account, and attaches session middleware.
307
+
308
+ Args:
309
+ app (Sanic): Sanic application instance.
310
+ create_root (bool): Determines root account creation on initialization.
311
+ """
312
+
313
+ @app.listener("before_server_start")
314
+ async def audit_configuration(app, loop):
315
+ if config.SECRET == DEFAULT_CONFIG["SECRET"]:
316
+ warnings.warn("Secret should be changed from default.", AuditWarning, 2)
317
+ if not config.SESSION_HTTPONLY:
318
+ warnings.warn("HttpOnly should be enabled.", AuditWarning, 2)
319
+ if not config.SESSION_SECURE:
320
+ warnings.warn("Secure should be enabled.", AuditWarning, 2)
321
+ if not config.SESSION_SAMESITE or config.SESSION_SAMESITE.lower() == "none":
322
+ warnings.warn("SameSite should not be none.", AuditWarning, 2)
323
+ if not config.SESSION_DOMAIN:
324
+ warnings.warn("Domain should not be none.", AuditWarning, 2)
325
+ if (
326
+ create_root
327
+ and config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
328
+ ):
329
+ warnings.warn(
330
+ "Initial admin email should be changed from default.", AuditWarning, 2
331
+ )
332
+ if (
333
+ create_root
334
+ and config.INITIAL_ADMIN_PASSWORD
335
+ == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
336
+ ):
337
+ warnings.warn(
338
+ "Initial admin password should be changed from default.",
339
+ AuditWarning,
340
+ 2,
341
+ )
342
+
343
+ @app.listener("before_server_start")
344
+ async def create_root_account(app, loop):
345
+ if not create_root:
346
+ return
347
+ try:
348
+ role = await Role.filter(name="Root").get()
349
+ except DoesNotExist:
350
+ role = await Role.create(
351
+ description="Has administrator abilities, assign sparingly.",
352
+ permissions=["*:*"],
353
+ name="Root",
354
+ )
355
+ try:
356
+ account = await Account.filter(email=config.INITIAL_ADMIN_EMAIL).get()
357
+ await account.fetch_related("roles")
358
+ if role not in account.roles:
359
+ await account.roles.add(role)
360
+ logger.warning("Initial admin account role has been reinstated.")
361
+ except DoesNotExist:
362
+ account = await Account.create(
363
+ username="Root",
364
+ email=config.INITIAL_ADMIN_EMAIL,
365
+ password=password_hasher.hash(config.INITIAL_ADMIN_PASSWORD),
366
+ verified=True,
367
+ )
368
+ await account.roles.add(role)
369
+ logger.info("Initial admin account created.")
370
+
371
+ @app.on_response
372
+ async def session_middleware(request, response):
373
+ if hasattr(request.ctx, "session"):
374
+ if getattr(request.ctx.session, "is_refresh", False):
375
+ request.ctx.session.encode(response)
376
+ elif not request.ctx.session.active:
377
+ response.delete_cookie(
378
+ f"{config.SESSION_PREFIX}_{request.ctx.session.__class__.__name__[:7].lower()}"
379
+ )