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/authorization.py
CHANGED
@@ -1,31 +1,34 @@
|
|
1
1
|
import functools
|
2
|
-
import logging
|
3
|
-
from fnmatch import fnmatch
|
4
2
|
|
3
|
+
from sanic.log import logger
|
5
4
|
from sanic.request import Request
|
6
5
|
from tortoise.exceptions import DoesNotExist
|
7
6
|
|
8
7
|
from sanic_security.authentication import authenticate
|
9
|
-
from sanic_security.exceptions import AuthorizationError
|
8
|
+
from sanic_security.exceptions import AuthorizationError, AnonymousError
|
10
9
|
from sanic_security.models import Role, Account, AuthenticationSession
|
11
10
|
from sanic_security.utils import get_ip
|
12
11
|
|
13
12
|
"""
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
13
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
14
|
+
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
17
|
+
in the Software without restriction, including without limitation the rights
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
20
|
+
furnished to do so, subject to the following conditions:
|
21
|
+
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
23
|
+
copies or substantial portions of the Software.
|
24
|
+
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
31
|
+
SOFTWARE.
|
29
32
|
"""
|
30
33
|
|
31
34
|
|
@@ -51,19 +54,48 @@ async def check_permissions(
|
|
51
54
|
UnverifiedError
|
52
55
|
DisabledError
|
53
56
|
AuthorizationError
|
57
|
+
AnonymousError
|
54
58
|
"""
|
55
59
|
authentication_session = await authenticate(request)
|
60
|
+
if authentication_session.anonymous:
|
61
|
+
logger.warning(
|
62
|
+
f"Client {get_ip(request)} attempted an unauthorized action anonymously."
|
63
|
+
)
|
64
|
+
raise AnonymousError
|
56
65
|
roles = await authentication_session.bearer.roles.filter(deleted=False).all()
|
57
66
|
for role in roles:
|
58
|
-
for
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
67
|
+
for role_permission in role.permissions:
|
68
|
+
for required_permission in required_permissions:
|
69
|
+
if check_wildcard(role_permission, required_permission):
|
70
|
+
return authentication_session
|
71
|
+
logger.warning(
|
72
|
+
f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action."
|
73
|
+
)
|
64
74
|
raise AuthorizationError("Insufficient permissions required for this action.")
|
65
75
|
|
66
76
|
|
77
|
+
def check_wildcard(wildcard: str, pattern: str):
|
78
|
+
"""
|
79
|
+
Evaluates if the wildcard matches the pattern.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
wildcard (str): A wildcard string (e.g., "a:b:c").
|
83
|
+
pattern (str): A wildcard pattern optional (`*`) or comma-separated values to match against (e.g., "a:b,c:*").
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
is_match
|
87
|
+
"""
|
88
|
+
wildcard_parts = [set(part.split(",")) for part in wildcard.split(":")]
|
89
|
+
pattern_parts = [set(part.split(",")) for part in pattern.split(":")]
|
90
|
+
for i, pattern_part in enumerate(pattern_parts):
|
91
|
+
if i >= len(wildcard_parts):
|
92
|
+
return False
|
93
|
+
wildcard_part = wildcard_parts[i]
|
94
|
+
if "*" not in wildcard_part and not wildcard_part.issuperset(pattern_part):
|
95
|
+
return False
|
96
|
+
return all("*" in part for part in wildcard_parts[len(pattern_parts) :])
|
97
|
+
|
98
|
+
|
67
99
|
async def check_roles(request: Request, *required_roles: str) -> AuthenticationSession:
|
68
100
|
"""
|
69
101
|
Authenticates client and determines if the account has sufficient roles for an action.
|
@@ -84,17 +116,55 @@ async def check_roles(request: Request, *required_roles: str) -> AuthenticationS
|
|
84
116
|
UnverifiedError
|
85
117
|
DisabledError
|
86
118
|
AuthorizationError
|
119
|
+
AnonymousError
|
87
120
|
"""
|
88
121
|
authentication_session = await authenticate(request)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
122
|
+
if authentication_session.anonymous:
|
123
|
+
logger.warning(
|
124
|
+
f"Client {get_ip(request)} attempted an unauthorized action anonymously."
|
125
|
+
)
|
126
|
+
raise AnonymousError
|
127
|
+
if set(required_roles) & {
|
128
|
+
role.name
|
129
|
+
for role in await authentication_session.bearer.roles.filter(
|
130
|
+
deleted=False
|
131
|
+
).all()
|
132
|
+
}:
|
133
|
+
return authentication_session
|
134
|
+
logger.warning(
|
135
|
+
f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action"
|
136
|
+
)
|
137
|
+
raise AuthorizationError("Insufficient roles required for this action")
|
138
|
+
|
139
|
+
|
140
|
+
async def assign_role(
|
141
|
+
name: str,
|
142
|
+
account: Account,
|
143
|
+
description: str = None,
|
144
|
+
*permissions: str,
|
145
|
+
) -> Role:
|
146
|
+
"""
|
147
|
+
Easy account role assignment, role being assigned to an account will be created if it doesn't exist.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
name (str): The name of the role associated with the account.
|
151
|
+
account (Account): The account associated with the created role.
|
152
|
+
description (str): The description of the role associated with the account.
|
153
|
+
*permissions (Tuple[str, ...]): The permissions of the role associated with the account, must be in wildcard format.
|
154
|
+
"""
|
155
|
+
try:
|
156
|
+
role = await Role.filter(name=name).get()
|
157
|
+
except DoesNotExist:
|
158
|
+
role = await Role.create(
|
159
|
+
name=name,
|
160
|
+
description=description,
|
161
|
+
permissions=permissions,
|
162
|
+
)
|
163
|
+
await account.roles.add(role)
|
164
|
+
return role
|
95
165
|
|
96
166
|
|
97
|
-
def
|
167
|
+
def requires_permission(*required_permissions: str):
|
98
168
|
"""
|
99
169
|
Authenticates client and determines if the account has sufficient permissions for an action.
|
100
170
|
|
@@ -105,8 +175,8 @@ def require_permissions(*required_permissions: str):
|
|
105
175
|
This method is not called directly and instead used as a decorator:
|
106
176
|
|
107
177
|
@app.post("api/auth/perms")
|
108
|
-
@
|
109
|
-
async def
|
178
|
+
@requires_permission("admin:update", "employee:add")
|
179
|
+
async def on_authorize(request):
|
110
180
|
return text("Account permitted.")
|
111
181
|
|
112
182
|
Raises:
|
@@ -118,21 +188,21 @@ def require_permissions(*required_permissions: str):
|
|
118
188
|
UnverifiedError
|
119
189
|
DisabledError
|
120
190
|
AuthorizationError
|
191
|
+
AnonymousError
|
121
192
|
"""
|
122
193
|
|
123
194
|
def decorator(func):
|
124
195
|
@functools.wraps(func)
|
125
196
|
async def wrapper(request, *args, **kwargs):
|
126
|
-
|
127
|
-
request, *required_permissions
|
128
|
-
)
|
197
|
+
await check_permissions(request, *required_permissions)
|
129
198
|
return await func(request, *args, **kwargs)
|
130
199
|
|
131
200
|
return wrapper
|
201
|
+
|
132
202
|
return decorator
|
133
203
|
|
134
204
|
|
135
|
-
def
|
205
|
+
def requires_role(*required_roles: str):
|
136
206
|
"""
|
137
207
|
Authenticates client and determines if the account has sufficient roles for an action.
|
138
208
|
|
@@ -143,8 +213,8 @@ def require_roles(*required_roles: str):
|
|
143
213
|
This method is not called directly and instead used as a decorator:
|
144
214
|
|
145
215
|
@app.post("api/auth/roles")
|
146
|
-
@
|
147
|
-
async def
|
216
|
+
@requires_role("Admin", "Moderator")
|
217
|
+
async def on_authorize(request):
|
148
218
|
return text("Account permitted")
|
149
219
|
|
150
220
|
Raises:
|
@@ -156,38 +226,15 @@ def require_roles(*required_roles: str):
|
|
156
226
|
UnverifiedError
|
157
227
|
DisabledError
|
158
228
|
AuthorizationError
|
229
|
+
AnonymousError
|
159
230
|
"""
|
160
231
|
|
161
232
|
def decorator(func):
|
162
233
|
@functools.wraps(func)
|
163
234
|
async def wrapper(request, *args, **kwargs):
|
164
|
-
|
165
|
-
request, *required_roles
|
166
|
-
)
|
235
|
+
await check_roles(request, *required_roles)
|
167
236
|
return await func(request, *args, **kwargs)
|
168
237
|
|
169
238
|
return wrapper
|
170
239
|
|
171
240
|
return decorator
|
172
|
-
|
173
|
-
|
174
|
-
async def assign_role(
|
175
|
-
name: str, account: Account, permissions: str = None, description: str = None
|
176
|
-
) -> Role:
|
177
|
-
"""
|
178
|
-
Easy account role assignment. Role being assigned to an account will be created if it doesn't exist.
|
179
|
-
|
180
|
-
Args:
|
181
|
-
name (str): The name of the role associated with the account.
|
182
|
-
account (Account): The account associated with the created role.
|
183
|
-
permissions (str): The permissions of the role associated with the account. Permissions must be separated via comma and in wildcard format.
|
184
|
-
description (str): The description of the role associated with the account.
|
185
|
-
"""
|
186
|
-
try:
|
187
|
-
role = await Role.filter(name=name).get()
|
188
|
-
except DoesNotExist:
|
189
|
-
role = await Role.create(
|
190
|
-
description=description, permissions=permissions, name=name
|
191
|
-
)
|
192
|
-
await account.roles.add(role)
|
193
|
-
return role
|
sanic_security/configuration.py
CHANGED
@@ -2,40 +2,47 @@ from os import environ
|
|
2
2
|
|
3
3
|
from sanic.utils import str_to_bool
|
4
4
|
|
5
|
-
|
6
5
|
"""
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
10
|
+
in the Software without restriction, including without limitation the rights
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
13
|
+
furnished to do so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
16
|
+
copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
SOFTWARE.
|
22
25
|
"""
|
23
26
|
|
24
|
-
|
25
27
|
DEFAULT_CONFIG = {
|
26
28
|
"SECRET": "This is a big secret. Shhhhh",
|
27
29
|
"PUBLIC_SECRET": None,
|
28
|
-
"
|
30
|
+
"OAUTH_CLIENT": None,
|
31
|
+
"OAUTH_SECRET": None,
|
32
|
+
"OAUTH_REDIRECT": None,
|
33
|
+
"SESSION_SAMESITE": "Strict",
|
29
34
|
"SESSION_SECURE": True,
|
30
35
|
"SESSION_HTTPONLY": True,
|
31
36
|
"SESSION_DOMAIN": None,
|
32
|
-
"SESSION_PREFIX": "
|
37
|
+
"SESSION_PREFIX": "tkn",
|
33
38
|
"SESSION_ENCODING_ALGORITHM": "HS256",
|
34
|
-
"MAX_CHALLENGE_ATTEMPTS":
|
35
|
-
"CAPTCHA_SESSION_EXPIRATION":
|
39
|
+
"MAX_CHALLENGE_ATTEMPTS": 3,
|
40
|
+
"CAPTCHA_SESSION_EXPIRATION": 180,
|
36
41
|
"CAPTCHA_FONT": "captcha-font.ttf",
|
37
|
-
"
|
38
|
-
"
|
42
|
+
"CAPTCHA_VOICE": "captcha-voice/",
|
43
|
+
"TWO_STEP_SESSION_EXPIRATION": 300,
|
44
|
+
"AUTHENTICATION_SESSION_EXPIRATION": 86400,
|
45
|
+
"AUTHENTICATION_REFRESH_EXPIRATION": 604800,
|
39
46
|
"ALLOW_LOGIN_WITH_USERNAME": False,
|
40
47
|
"INITIAL_ADMIN_EMAIL": "admin@example.com",
|
41
48
|
"INITIAL_ADMIN_PASSWORD": "admin123",
|
@@ -50,6 +57,9 @@ class Config(dict):
|
|
50
57
|
Attributes:
|
51
58
|
SECRET (str): The secret used by the hashing algorithm for generating and signing JWTs. This should be a string unique to your application. Keep it safe.
|
52
59
|
PUBLIC_SECRET (str): The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application.
|
60
|
+
OAUTH_CLIENT (str): The client ID provided by the OAuth provider, this is used to identify the application making the OAuth request.
|
61
|
+
OAUTH_SECRET (str): The client secret provided by the OAuth provider, this is used in conjunction with the client ID to authenticate the application.
|
62
|
+
OAUTH_REDIRECT (str): The redirect URI registered with the OAuth provider, This is the URI where the user will be redirected after a successful authentication.
|
53
63
|
SESSION_SAMESITE (str): The SameSite attribute of session cookies.
|
54
64
|
SESSION_SECURE (bool): The Secure attribute of session cookies.
|
55
65
|
SESSION_HTTPONLY (bool): The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing.
|
@@ -59,8 +69,10 @@ class Config(dict):
|
|
59
69
|
MAX_CHALLENGE_ATTEMPTS (str): The maximum amount of session challenge attempts allowed.
|
60
70
|
CAPTCHA_SESSION_EXPIRATION (int): The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration.
|
61
71
|
CAPTCHA_FONT (str): The file path to the font being used for captcha generation.
|
62
|
-
|
63
|
-
|
72
|
+
CAPTCHA_VOICE (str): The directory of the voice library being used for audio captcha generation.
|
73
|
+
TWO_STEP_SESSION_EXPIRATION (int): The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration.
|
74
|
+
AUTHENTICATION_SESSION_EXPIRATION (int): The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration.
|
75
|
+
AUTHENTICATION_REFRESH_EXPIRATION (int): The amount of seconds till authentication session refresh expiration. Setting to 0 will disable refresh mechanism.
|
64
76
|
ALLOW_LOGIN_WITH_USERNAME (bool): Allows login via username and email.
|
65
77
|
INITIAL_ADMIN_EMAIL (str): Email used when creating the initial admin account.
|
66
78
|
INITIAL_ADMIN_PASSWORD (str): Password used when creating the initial admin account.
|
@@ -69,6 +81,9 @@ class Config(dict):
|
|
69
81
|
|
70
82
|
SECRET: str
|
71
83
|
PUBLIC_SECRET: str
|
84
|
+
OAUTH_CLIENT: str
|
85
|
+
OAUTH_SECRET: str
|
86
|
+
OAUTH_REDIRECT: str
|
72
87
|
SESSION_SAMESITE: str
|
73
88
|
SESSION_SECURE: bool
|
74
89
|
SESSION_HTTPONLY: bool
|
@@ -78,8 +93,10 @@ class Config(dict):
|
|
78
93
|
MAX_CHALLENGE_ATTEMPTS: int
|
79
94
|
CAPTCHA_SESSION_EXPIRATION: int
|
80
95
|
CAPTCHA_FONT: str
|
96
|
+
CAPTCHA_VOICE: str
|
81
97
|
TWO_STEP_SESSION_EXPIRATION: int
|
82
98
|
AUTHENTICATION_SESSION_EXPIRATION: int
|
99
|
+
AUTHENTICATION_REFRESH_EXPIRATION: int
|
83
100
|
ALLOW_LOGIN_WITH_USERNAME: bool
|
84
101
|
INITIAL_ADMIN_EMAIL: str
|
85
102
|
INITIAL_ADMIN_PASSWORD: str
|
sanic_security/exceptions.py
CHANGED
@@ -3,21 +3,25 @@ from sanic.exceptions import SanicException
|
|
3
3
|
from sanic_security.utils import json
|
4
4
|
|
5
5
|
"""
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
6
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
10
|
+
in the Software without restriction, including without limitation the rights
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
13
|
+
furnished to do so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
16
|
+
copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
SOFTWARE.
|
21
25
|
"""
|
22
26
|
|
23
27
|
|
@@ -53,7 +57,25 @@ class DeletedError(SecurityError):
|
|
53
57
|
"""
|
54
58
|
|
55
59
|
def __init__(self, message):
|
56
|
-
super().__init__(message,
|
60
|
+
super().__init__(message, 404)
|
61
|
+
|
62
|
+
|
63
|
+
class CredentialsError(SecurityError):
|
64
|
+
"""
|
65
|
+
Raised when credentials are invalid.
|
66
|
+
"""
|
67
|
+
|
68
|
+
def __init__(self, message, code=400):
|
69
|
+
super().__init__(message, code)
|
70
|
+
|
71
|
+
|
72
|
+
class OAuthError(SecurityError):
|
73
|
+
"""
|
74
|
+
Raised when an error occurs during OAuth flow.
|
75
|
+
"""
|
76
|
+
|
77
|
+
def __init__(self, message, code=401):
|
78
|
+
super().__init__(message, code)
|
57
79
|
|
58
80
|
|
59
81
|
class AccountError(SecurityError):
|
@@ -106,7 +128,9 @@ class JWTDecodeError(SessionError):
|
|
106
128
|
Raised when client JWT is invalid.
|
107
129
|
"""
|
108
130
|
|
109
|
-
def __init__(
|
131
|
+
def __init__(
|
132
|
+
self, message="Session token invalid, not provided, or expired.", code=401
|
133
|
+
):
|
110
134
|
super().__init__(message, code)
|
111
135
|
|
112
136
|
|
@@ -115,7 +139,11 @@ class DeactivatedError(SessionError):
|
|
115
139
|
Raised when session is deactivated.
|
116
140
|
"""
|
117
141
|
|
118
|
-
def __init__(
|
142
|
+
def __init__(
|
143
|
+
self,
|
144
|
+
message: str = "Session has been deactivated.",
|
145
|
+
code: int = 401,
|
146
|
+
):
|
119
147
|
super().__init__(message, code)
|
120
148
|
|
121
149
|
|
@@ -124,8 +152,8 @@ class ExpiredError(SessionError):
|
|
124
152
|
Raised when session has expired.
|
125
153
|
"""
|
126
154
|
|
127
|
-
def __init__(self):
|
128
|
-
super().__init__(
|
155
|
+
def __init__(self, message="Session has expired."):
|
156
|
+
super().__init__(message)
|
129
157
|
|
130
158
|
|
131
159
|
class SecondFactorRequiredError(SessionError):
|
@@ -173,10 +201,16 @@ class AuthorizationError(SecurityError):
|
|
173
201
|
super().__init__(message, 403)
|
174
202
|
|
175
203
|
|
176
|
-
class
|
204
|
+
class AnonymousError(AuthorizationError):
|
177
205
|
"""
|
178
|
-
Raised when
|
206
|
+
Raised when attempting to authorize an anonymous user.
|
179
207
|
"""
|
180
208
|
|
181
|
-
def __init__(self
|
182
|
-
super().__init__(
|
209
|
+
def __init__(self):
|
210
|
+
super().__init__("Session is anonymous.")
|
211
|
+
|
212
|
+
|
213
|
+
class AuditWarning(Warning):
|
214
|
+
"""
|
215
|
+
Raised when configuration may be dangerous.
|
216
|
+
"""
|