sanic-security 1.16.12__py3-none-any.whl → 1.17.1__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,363 +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
- 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
- """
220
- authentication_session = await AuthenticationSession.decode(request)
221
- try:
222
- authentication_session.validate()
223
- if not authentication_session.anonymous:
224
- authentication_session.bearer.validate()
225
- except ExpiredError:
226
- authentication_session = await authentication_session.refresh(request)
227
- authentication_session.is_refresh = True
228
- request.ctx.session = authentication_session
229
- return authentication_session
230
-
231
-
232
- def requires_authentication(arg=None):
233
- """
234
- Validates client's authentication session and account. New/Refreshed session automatically returned if client's
235
- session expired during authentication.
236
-
237
- Example:
238
- This method is not called directly and instead used as a decorator:
239
-
240
- @app.post('api/authenticate')
241
- @requires_authentication
242
- async def on_authenticate(request):
243
- return text('User is authenticated!')
244
-
245
- Raises:
246
- NotFoundError
247
- JWTDecodeError
248
- DeletedError
249
- DeactivatedError
250
- UnverifiedError
251
- DisabledError
252
- SecondFactorRequiredError
253
- ExpiredError
254
- """
255
-
256
- def decorator(func):
257
- @functools.wraps(func)
258
- async def wrapper(request, *args, **kwargs):
259
- await authenticate(request)
260
- return await func(request, *args, **kwargs)
261
-
262
- return wrapper
263
-
264
- return decorator(arg) if callable(arg) else decorator
265
-
266
-
267
- def validate_password(password: str) -> str:
268
- """
269
- Validates password formatting requirements.
270
-
271
- Args:
272
- password (str): Password being validated.
273
-
274
- Returns:
275
- password
276
-
277
- Raises:
278
- CredentialsError
279
- """
280
- if not re.search(r"^(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&+=!]).*$", password):
281
- raise CredentialsError(
282
- "Password must contain one capital letter, one number, and one special character",
283
- 400,
284
- )
285
- return password
286
-
287
-
288
- def initialize_security(app: Sanic, create_root: bool = True) -> None:
289
- """
290
- Audits configuration, creates root administrator account, and attaches session middleware.
291
-
292
- Args:
293
- app (Sanic): Sanic application instance.
294
- create_root (bool): Determines root account creation on initialization.
295
- """
296
-
297
- @app.listener("before_server_start")
298
- async def audit_configuration(app, loop):
299
- if config.SECRET == DEFAULT_CONFIG["SECRET"]:
300
- warnings.warn("Secret should be changed from default.", AuditWarning, 2)
301
- if not config.SESSION_HTTPONLY:
302
- warnings.warn("HttpOnly should be enabled.", AuditWarning, 2)
303
- if not config.SESSION_SECURE:
304
- warnings.warn("Secure should be enabled.", AuditWarning, 2)
305
- if not config.SESSION_SAMESITE or config.SESSION_SAMESITE.lower() == "none":
306
- warnings.warn("SameSite should not be none.", AuditWarning, 2)
307
- if not config.SESSION_DOMAIN:
308
- warnings.warn("Domain should not be none.", AuditWarning, 2)
309
- if (
310
- create_root
311
- and config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
312
- ):
313
- warnings.warn(
314
- "Initial admin email should be changed from default.", AuditWarning, 2
315
- )
316
- if (
317
- create_root
318
- and config.INITIAL_ADMIN_PASSWORD
319
- == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
320
- ):
321
- warnings.warn(
322
- "Initial admin password should be changed from default.",
323
- AuditWarning,
324
- 2,
325
- )
326
-
327
- @app.listener("before_server_start")
328
- async def create_root_account(app, loop):
329
- if not create_root:
330
- return
331
- try:
332
- role = await Role.filter(name="Root").get()
333
- except DoesNotExist:
334
- role = await Role.create(
335
- description="Has administrator abilities, assign sparingly.",
336
- permissions=["*:*"],
337
- name="Root",
338
- )
339
- try:
340
- account = await Account.filter(email=config.INITIAL_ADMIN_EMAIL).get()
341
- await account.fetch_related("roles")
342
- if role not in account.roles:
343
- await account.roles.add(role)
344
- logger.warning("Initial admin account role has been reinstated.")
345
- except DoesNotExist:
346
- account = await Account.create(
347
- username="Root",
348
- email=config.INITIAL_ADMIN_EMAIL,
349
- password=password_hasher.hash(config.INITIAL_ADMIN_PASSWORD),
350
- verified=True,
351
- )
352
- await account.roles.add(role)
353
- logger.info("Initial admin account created.")
354
-
355
- @app.on_response
356
- async def session_middleware(request, response):
357
- if hasattr(request.ctx, "session"):
358
- if getattr(request.ctx.session, "is_refresh", False):
359
- request.ctx.session.encode(response)
360
- elif not request.ctx.session.active:
361
- response.delete_cookie(
362
- f"{config.SESSION_PREFIX}_{request.ctx.session.__class__.__name__[:7].lower()}"
363
- )
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, tag="2fa")
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
+ )