pycrossauth 0.0.2__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.
- crossauth_backend/__init__.py +59 -0
- crossauth_backend/auth.py +132 -0
- crossauth_backend/common/__init__.py +2 -0
- crossauth_backend/common/error.py +498 -0
- crossauth_backend/common/interfaces.py +287 -0
- crossauth_backend/common/jwt.py +41 -0
- crossauth_backend/common/logger.py +202 -0
- crossauth_backend/cookieauth.py +575 -0
- crossauth_backend/crypto.py +457 -0
- crossauth_backend/oauth/__init__.py +2 -0
- crossauth_backend/oauth/client.py +1063 -0
- crossauth_backend/oauth/clientmanager.py +157 -0
- crossauth_backend/oauth/resserver.py +34 -0
- crossauth_backend/oauth/tokenconsumer.py +412 -0
- crossauth_backend/oauth/wellknown.py +108 -0
- crossauth_backend/session.py +450 -0
- crossauth_backend/storage.py +626 -0
- crossauth_backend/storageimpl/__init__.py +2 -0
- crossauth_backend/storageimpl/inmemorystorage.py +132 -0
- crossauth_backend/storageimpl/sqlalchemystorage.py +294 -0
- crossauth_backend/utils.py +150 -0
- crossauth_fastapi/__init__.py +23 -0
- crossauth_fastapi/fastapioauthclient.py +2185 -0
- crossauth_fastapi/fastapiresserver.py +300 -0
- crossauth_fastapi/fastapiserver.py +437 -0
- crossauth_fastapi/fastapiserverbase.py +62 -0
- crossauth_fastapi/fastapisession.py +654 -0
- crossauth_fastapi/fastapisessionadapter.py +94 -0
- pycrossauth-0.0.2.dist-info/LICENSE +203 -0
- pycrossauth-0.0.2.dist-info/METADATA +257 -0
- pycrossauth-0.0.2.dist-info/NOTICE +5 -0
- pycrossauth-0.0.2.dist-info/RECORD +34 -0
- pycrossauth-0.0.2.dist-info/WHEEL +5 -0
- pycrossauth-0.0.2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
|
|
2
|
+
from typing import Any, Dict, Optional, TypedDict, NotRequired
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from nulltype import NullType
|
|
6
|
+
from crossauth_backend.common.logger import CrossauthLogger, j
|
|
7
|
+
|
|
8
|
+
class Key(TypedDict):
|
|
9
|
+
"""
|
|
10
|
+
A key (eg session ID, email reset token) as stored in a database table.
|
|
11
|
+
|
|
12
|
+
The fields defined here are the ones used by Crossauth. You may add
|
|
13
|
+
others.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
value : str
|
|
17
|
+
""" The value of the keykey.
|
|
18
|
+
In a cookie, the value part of cookiename=value; options...
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
created : datetime
|
|
22
|
+
""" The datetime/time the key was created, in local time on the server """
|
|
23
|
+
|
|
24
|
+
expires : datetime | NullType
|
|
25
|
+
""" The datetime/time the key expires """
|
|
26
|
+
|
|
27
|
+
userid : NotRequired[str | int | NullType]
|
|
28
|
+
""" the user this key is for (or undefined for an anonymous session ID)
|
|
29
|
+
|
|
30
|
+
It accepts the value null as usually this is the value stored in the
|
|
31
|
+
database, rather than undefined. Some functions need to differentiate
|
|
32
|
+
between a null value as opposed to the value not being defined (eg for
|
|
33
|
+
a partial updatetime).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
lastactive : NotRequired[datetime]
|
|
37
|
+
""" The datetime/time key was last used (eg last time a request was made
|
|
38
|
+
with this value as a session ID)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
data : NotRequired[str]
|
|
42
|
+
""" Additional key-specific data (eg new email address for email change).
|
|
43
|
+
|
|
44
|
+
While application specific, any data Crossauth puts in this field
|
|
45
|
+
is a strified JSON, with its own key so it can co-exist with
|
|
46
|
+
other data.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
class PartialKey(TypedDict, total = False):
|
|
50
|
+
"""
|
|
51
|
+
Same as :class:`crossauth_backend.Key` but all fields are NotRequired
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
value : str
|
|
55
|
+
created : datetime
|
|
56
|
+
expires : datetime | NullType
|
|
57
|
+
userid : Optional[str | int | NullType]
|
|
58
|
+
lastactive : Optional[datetime]
|
|
59
|
+
data : Optional[str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ApiKey(Key):
|
|
63
|
+
"""
|
|
64
|
+
An API key is a str that can be used in place of a username and
|
|
65
|
+
password. These are not automatically created, like OAuth access tokens.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name : str
|
|
69
|
+
""" A name for the key, unique to the user """
|
|
70
|
+
|
|
71
|
+
def get_json_data(key: Key) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Parses the data field in the key as a JSON object
|
|
74
|
+
|
|
75
|
+
:param crossauth_backend.Key key: the key to extract the data field from
|
|
76
|
+
|
|
77
|
+
:return: the data as dict
|
|
78
|
+
"""
|
|
79
|
+
if ("data" not in key):
|
|
80
|
+
return {}
|
|
81
|
+
try:
|
|
82
|
+
return json.loads(key["data"])
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
CrossauthLogger.logger().warn(j({"msg": "data in key is not JSON"}))
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
class UserInputFields(TypedDict):
|
|
88
|
+
"""
|
|
89
|
+
Describes a user as fetched from the user storage (eg, database table),
|
|
90
|
+
excluding auto-generated fields such as an auto-generated ID
|
|
91
|
+
|
|
92
|
+
This is extendible with additional fields - provide them to the
|
|
93
|
+
:class:`UserStorage` class as `extraFields`.
|
|
94
|
+
|
|
95
|
+
You may want to do this if you want to pass additional user data back to the
|
|
96
|
+
caller, eg real name.
|
|
97
|
+
|
|
98
|
+
The fields defined here are the ones used by Crossauth. You may add
|
|
99
|
+
others.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
username : str
|
|
103
|
+
"""The username. This may be an email address or anything else,
|
|
104
|
+
application-specific.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
state : str
|
|
108
|
+
"""
|
|
109
|
+
You are free to define your own states. The ones Crossauth recognises
|
|
110
|
+
are defined in :class:`UserState`.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
email : NotRequired[str]
|
|
114
|
+
"""
|
|
115
|
+
You can optionally include an email address field in your user table.
|
|
116
|
+
If your username is an email address, you do not need a separate field.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
admin : NotRequired[bool]
|
|
120
|
+
"""
|
|
121
|
+
Whether or not the user has administrator priviledges (and can acess
|
|
122
|
+
admin-only functions).
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
factor1 : str
|
|
126
|
+
"""
|
|
127
|
+
Factor for primary authentication.
|
|
128
|
+
|
|
129
|
+
Should match the name of an authenticator
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
factor2 : NotRequired[str]
|
|
133
|
+
"""
|
|
134
|
+
Factor for second factor authentication.
|
|
135
|
+
|
|
136
|
+
Should match the name of an authenticator
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PartialUserInputFields(TypedDict, total=False):
|
|
141
|
+
"""
|
|
142
|
+
Same as UserInputFields but all fields are not required
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
username : str
|
|
146
|
+
state : str
|
|
147
|
+
email : NotRequired[str]
|
|
148
|
+
admin : NotRequired[bool]
|
|
149
|
+
factor1 : NotRequired[str]
|
|
150
|
+
factor2 : NotRequired[str]
|
|
151
|
+
|
|
152
|
+
class PartialUser(PartialUserInputFields, total=False):
|
|
153
|
+
"""
|
|
154
|
+
Same as User but all fields are not required
|
|
155
|
+
"""
|
|
156
|
+
id : str | int
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class User(UserInputFields):
|
|
160
|
+
"""
|
|
161
|
+
This adds ID to :class:`UserInputFields`.
|
|
162
|
+
|
|
163
|
+
If your `username` field is
|
|
164
|
+
unique and immutable, you can omit ID (passing username anywhere an ID)
|
|
165
|
+
is expected. However, if you want users to be able to change their username,
|
|
166
|
+
you should include ID field and make that immutable instead.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
""" ID fied, which may be auto-generated """
|
|
170
|
+
id : str | int
|
|
171
|
+
|
|
172
|
+
class UserSecretsInputFields(TypedDict, total=False):
|
|
173
|
+
"""
|
|
174
|
+
Secrets, such as a password, are not in the User object to prevent them
|
|
175
|
+
accidentally being leaked to the frontend. All functions that return
|
|
176
|
+
secrets return them in this separate object.
|
|
177
|
+
|
|
178
|
+
The fields in this class are the ones that are not autogenerated by the
|
|
179
|
+
database.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
password : str
|
|
183
|
+
totpsecret : str
|
|
184
|
+
otp: str
|
|
185
|
+
expiry: int
|
|
186
|
+
|
|
187
|
+
class PartialUserSecrets(UserSecretsInputFields, total=False):
|
|
188
|
+
"""
|
|
189
|
+
Same as UserSecrets except all fields are NotRequired
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
userid : str|int
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class UserSecrets(UserSecretsInputFields):
|
|
196
|
+
"""
|
|
197
|
+
This adds the user ID toi :class:`UserSecretsInputFields`.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
userid : str|int
|
|
201
|
+
|
|
202
|
+
class OAuthClient(TypedDict):
|
|
203
|
+
"""
|
|
204
|
+
OAuth client data as stored in a database table
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
client_id : str
|
|
208
|
+
"""The client_id, which is auto-generated and immutable """
|
|
209
|
+
|
|
210
|
+
confidential : bool
|
|
211
|
+
"""Whether or not the client is confidential (and can therefore
|
|
212
|
+
keep the client secret secret) """
|
|
213
|
+
|
|
214
|
+
client_name : str
|
|
215
|
+
"""
|
|
216
|
+
A user-friendly name for the client (not used as part of the OAuth
|
|
217
|
+
API).
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
client_secret : NotRequired[str|NullType]
|
|
221
|
+
"""
|
|
222
|
+
Client secret, which is autogenerated.
|
|
223
|
+
|
|
224
|
+
If there is no client secret, it should be set to `undefined`.
|
|
225
|
+
|
|
226
|
+
This field allows `null` as well as `undefined` this is used, for
|
|
227
|
+
example, when partially updating a client and you specifically
|
|
228
|
+
want to set the secret to undefined, as opposed to just not wishing
|
|
229
|
+
to change the value. Other than that, this value is always either
|
|
230
|
+
a str or `undefined`.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
redirect_uri : list[str]
|
|
234
|
+
"""
|
|
235
|
+
An array of value redirect URIs for the client.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
valid_flow : list[str]
|
|
239
|
+
"""
|
|
240
|
+
An array of OAuth flows allowed for this client.
|
|
241
|
+
|
|
242
|
+
See :class:`OAuthFlows`.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
userid : NotRequired[str|int|NullType]
|
|
247
|
+
"""
|
|
248
|
+
ID of the user who owns this client, which may be `undefined`
|
|
249
|
+
for not being owned by a specific user.
|
|
250
|
+
|
|
251
|
+
This field allows `null` as well as `undefined` this is used, for
|
|
252
|
+
example, when partially updating a client and you specifically
|
|
253
|
+
want to set the user ID to undefined, as opposed to just not wishing
|
|
254
|
+
to change the value. Other than that, this value is always either
|
|
255
|
+
a str or number (depending on the ID type in your user storage)
|
|
256
|
+
or `undefined`.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
class UserState:
|
|
260
|
+
"""
|
|
261
|
+
These are used valiues for `state` in a :class:`crossauth_backend.User` object
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
active = "active"
|
|
265
|
+
disabled = "disabled"
|
|
266
|
+
awaiting_two_factor_setup = "awaitingtwofactorsetup"
|
|
267
|
+
awaiting_email_verification = "awaitingemailverification"
|
|
268
|
+
password_change_needed = "passwordchangeneeded"
|
|
269
|
+
password_reset_needed = "passwordresetneeded"
|
|
270
|
+
factor2_reset_needed = "factor2resetneeded"
|
|
271
|
+
password_and_factor2_reset_needed = "passwordandfactor2resetneeded"
|
|
272
|
+
|
|
273
|
+
class KeyPrefix:
|
|
274
|
+
"""
|
|
275
|
+
These are used prefixes for keye in a key storage entry
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
session = "s:"
|
|
279
|
+
password_reset_token = "p:"
|
|
280
|
+
email_verification_token = "e:"
|
|
281
|
+
api_key = "api:"
|
|
282
|
+
authorization_code = "authz:"
|
|
283
|
+
access_token = "access:"
|
|
284
|
+
refresh_token = "refresh:"
|
|
285
|
+
mfa_token = "omfa:"
|
|
286
|
+
device_code = "dc:"
|
|
287
|
+
user_code = "uc:"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Dict, Optional, Any
|
|
5
|
+
|
|
6
|
+
from crossauth_backend.common.error import CrossauthError, ErrorCode
|
|
7
|
+
from crossauth_backend.common.logger import CrossauthLogger, j
|
|
8
|
+
|
|
9
|
+
class JWT:
|
|
10
|
+
"""
|
|
11
|
+
Encapsulates the payload of a JWT, with both the token and
|
|
12
|
+
decoded JSON payload.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, token: Optional[str] = None, payload: Dict[str, Any] = {}):
|
|
16
|
+
"""
|
|
17
|
+
Constructor. Pass either `token` or `payload`.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
token: the string JWT token - the payload will be parsed from it
|
|
21
|
+
payload: the JSON payload. The payload will be set but not
|
|
22
|
+
the string `token`.
|
|
23
|
+
"""
|
|
24
|
+
self.token: Optional[str] = token
|
|
25
|
+
|
|
26
|
+
if payload:
|
|
27
|
+
self.payload = payload
|
|
28
|
+
elif self.token:
|
|
29
|
+
parts = self.token.split(".")
|
|
30
|
+
if len(parts) != 3:
|
|
31
|
+
raise CrossauthError(ErrorCode.InvalidToken, "JWT not in correct format")
|
|
32
|
+
try:
|
|
33
|
+
self.payload = json.loads(base64.urlsafe_b64decode(parts[1] + '==').decode('utf-8'))
|
|
34
|
+
except Exception as e:
|
|
35
|
+
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
36
|
+
if len(parts) != 3:
|
|
37
|
+
raise CrossauthError(ErrorCode.InvalidToken, "JWT payload not in correct format")
|
|
38
|
+
json_string = base64.urlsafe_b64decode(parts[1] + '==').decode('utf-8')
|
|
39
|
+
self.payload = json.loads(json_string)
|
|
40
|
+
else:
|
|
41
|
+
self.payload = {}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
|
|
2
|
+
from .. import CrossauthError
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Mapping
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
class CrossauthLoggerInterface:
|
|
10
|
+
"""
|
|
11
|
+
You can implement your own logger. Crossauth only needs these functions
|
|
12
|
+
and variables to be present.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def error(self, output: Any) -> None:
|
|
16
|
+
""" Report a message at error level """
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def warn(self, output: Any) -> None:
|
|
20
|
+
""" Report a message at warning level """
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def info(self, output: Any) -> None:
|
|
24
|
+
""" Report a message at info level """
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def debug(self, output: Any) -> None:
|
|
28
|
+
""" Report a message at debug level """
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
NoLogging = 0
|
|
32
|
+
Error = 1
|
|
33
|
+
Warn = 2
|
|
34
|
+
Info = 3
|
|
35
|
+
Debug = 4
|
|
36
|
+
|
|
37
|
+
level: int = NoLogging
|
|
38
|
+
|
|
39
|
+
class CrossauthLogger(CrossauthLoggerInterface):
|
|
40
|
+
"""
|
|
41
|
+
A very simple logging class with no dependencies.
|
|
42
|
+
|
|
43
|
+
Logs to console.
|
|
44
|
+
|
|
45
|
+
The logging API is designed so that you can replace this with other common loggers, eg Pino.
|
|
46
|
+
To change it, use the global :func:`CrossauthLogger.set_logger` function. This has a parameter to tell
|
|
47
|
+
Crossauth whether your logger accepts JSON input or not.
|
|
48
|
+
|
|
49
|
+
When writing logs, we use the helper function :func:`j` to send JSON to the logger if it is
|
|
50
|
+
supprted, and a stringified JSON otherwise.
|
|
51
|
+
|
|
52
|
+
**Crossauth logs**
|
|
53
|
+
|
|
54
|
+
All Crossauth log messages are JSON (or stringified JSON, depending on whether the logger supports
|
|
55
|
+
JSON input - this one does). The following fields may be present depending on context
|
|
56
|
+
(`msg` is always present):
|
|
57
|
+
|
|
58
|
+
- `msg` : main contents of the log
|
|
59
|
+
- `err` : an error object. If a subclass of Error, it wil contain at least `message` and
|
|
60
|
+
a stack trace in `stack`. If the error is of type :class:`crossauth_backend.CrossauthError`
|
|
61
|
+
it also will also contain `code` and `http_status`.
|
|
62
|
+
- `hashedSessionCookie` : for security reasons, session cookies are not included in logs.
|
|
63
|
+
However, so that errors can be correlated with each other, a hash
|
|
64
|
+
of it is included in errors originating from a session.
|
|
65
|
+
- `hashedCsrfCookie` : for security reasons, csrf cookies are not included in logs.
|
|
66
|
+
However, so that errors can be correlated with each other, a hash
|
|
67
|
+
of it is included in errors originating from a session.
|
|
68
|
+
- `user` : username
|
|
69
|
+
- `emailMessageId` : internal id of any email that is sent
|
|
70
|
+
- `email` : email address
|
|
71
|
+
- `userid` : sometimes provided in addition to username, or when username not available
|
|
72
|
+
- `hahedApiKey` : a hash of an API key. The unhashed version is not logged for security,
|
|
73
|
+
but a hash of it is logged for correlation purposes.
|
|
74
|
+
- `header` : an HTTP header that relates to an error (eg `Authorization`), only if
|
|
75
|
+
it is non-secret or invalid
|
|
76
|
+
- `accessTokenHash` : hash of the JTI of an access token. For security reasons, the
|
|
77
|
+
unhashed version is not logged.
|
|
78
|
+
- `method`: request method (GET, PUT etc)
|
|
79
|
+
- `url` : relevant URL
|
|
80
|
+
- `ip` : relevant IP address
|
|
81
|
+
- `scope` : OAuth scope
|
|
82
|
+
- `error_code` : Crossauth error code
|
|
83
|
+
- `error_code_name` : String version of Crossauth error code
|
|
84
|
+
- `http_status` : HTTP status that will be returned
|
|
85
|
+
- `port` port service is running on (only for starting a service)
|
|
86
|
+
- `prefix` prefix for endpoints (only when starting a service)
|
|
87
|
+
- `authorized` whether or not a valid OAuth access token was provided
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
levelName = ["NONE", "ERROR", "WARN", "INFO", "DEBUG"]
|
|
91
|
+
|
|
92
|
+
_instance : CrossauthLoggerInterface
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def logger() -> CrossauthLoggerInterface:
|
|
96
|
+
""" Returns the static logger instance"""
|
|
97
|
+
global _crossauth_logger, _crossauth_logger_accepts_json
|
|
98
|
+
return _crossauth_logger
|
|
99
|
+
|
|
100
|
+
def __init__(self, level: int|None = None):
|
|
101
|
+
"""
|
|
102
|
+
Constructor
|
|
103
|
+
|
|
104
|
+
:param int|None level the level to report to
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
if level is not None:
|
|
108
|
+
self.level = level
|
|
109
|
+
elif "CROSSAUTH_LOG_LEVEL" in os.environ:
|
|
110
|
+
level_name = os.environ["CROSSAUTH_LOG_LEVEL"].upper()
|
|
111
|
+
if level_name in CrossauthLogger.levelName:
|
|
112
|
+
self.level = CrossauthLogger.levelName.index(level_name)
|
|
113
|
+
else:
|
|
114
|
+
self.level = CrossauthLogger.Error
|
|
115
|
+
else:
|
|
116
|
+
self.level = CrossauthLogger.Error
|
|
117
|
+
CrossauthLogger.rossauth_logger_accepts_json = True
|
|
118
|
+
|
|
119
|
+
def set_level(self, level: int) -> None:
|
|
120
|
+
""" Set the level to report down to """
|
|
121
|
+
self.level = level
|
|
122
|
+
|
|
123
|
+
def _log(self, level: int, output: Any) -> None:
|
|
124
|
+
if level <= self.level:
|
|
125
|
+
if isinstance(output, str):
|
|
126
|
+
print(f"Crossauth {CrossauthLogger.levelName[level]} {self._current_time_iso()} {output}")
|
|
127
|
+
else:
|
|
128
|
+
print(json.dumps({"level": CrossauthLogger.levelName[level], "time": self._current_time_iso(), **output}))
|
|
129
|
+
|
|
130
|
+
def error(self, output: Any) -> None:
|
|
131
|
+
""" Log an error """
|
|
132
|
+
self._log(CrossauthLogger.Error, output)
|
|
133
|
+
|
|
134
|
+
def warn(self, output: Any) -> None:
|
|
135
|
+
""" Log a warning """
|
|
136
|
+
self._log(CrossauthLogger.Warn, output)
|
|
137
|
+
|
|
138
|
+
def info(self, output: Any) -> None:
|
|
139
|
+
""" Log an info message """
|
|
140
|
+
self._log(CrossauthLogger.Info, output)
|
|
141
|
+
|
|
142
|
+
def debug(self, output: Any) -> None:
|
|
143
|
+
""" Log a debug message """
|
|
144
|
+
self._log(CrossauthLogger.Debug, output)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def set_logger(logger: CrossauthLoggerInterface, accepts_json: bool) -> None:
|
|
148
|
+
""" Set the static logger instance """
|
|
149
|
+
global _crossauth_logger, _crossauth_logger_accepts_json
|
|
150
|
+
_crossauth_logger = logger
|
|
151
|
+
_crossauth_logger_accepts_json = accepts_json
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _current_time_iso() -> str:
|
|
155
|
+
from datetime import datetime
|
|
156
|
+
return datetime.now().isoformat()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def j(arg: Mapping[str, Any] | str) -> Dict[str, Any] | str:
|
|
160
|
+
""" Helper function that returns JSON if the error log supports it,
|
|
161
|
+
otherwise a string """
|
|
162
|
+
global _crossauth_logger_accepts_json
|
|
163
|
+
argcopy : Mapping[str,Any] = {}
|
|
164
|
+
if (type(arg) == str):
|
|
165
|
+
argcopy = {"msg": arg}
|
|
166
|
+
elif (isinstance(arg, Mapping)):
|
|
167
|
+
argcopy = {**arg}
|
|
168
|
+
if isinstance(arg, dict) and "cerr" in arg and isinstance(arg["cerr"], CrossauthError):
|
|
169
|
+
argcopy["error_code"] = arg["cerr"].code.value
|
|
170
|
+
argcopy["error_code_name"] = arg["cerr"].code_name
|
|
171
|
+
argcopy["http_status"] = arg["cerr"].http_status
|
|
172
|
+
if "msg" not in argcopy:
|
|
173
|
+
argcopy["msg"] = argcopy["cerr"].message
|
|
174
|
+
|
|
175
|
+
if isinstance(arg, dict) and "err" in arg and isinstance(arg["err"], CrossauthError):
|
|
176
|
+
argcopy["error_code"] = arg["err"].code.value
|
|
177
|
+
argcopy["error_code_name"] = arg["err"].code_name
|
|
178
|
+
argcopy["http_status"] = arg["err"].http_status
|
|
179
|
+
if "msg" not in argcopy:
|
|
180
|
+
if ("cerr" in argcopy):
|
|
181
|
+
argcopy["msg"] = argcopy["cerr"].message
|
|
182
|
+
elif ("err" in argcopy):
|
|
183
|
+
argcopy["msg"] = argcopy["err"].message
|
|
184
|
+
argcopy["stack"] = str(traceback.format_exception(arg["err"]))
|
|
185
|
+
|
|
186
|
+
elif isinstance(arg, dict) and "err" in arg and isinstance(arg["err"], Exception):
|
|
187
|
+
argcopy["stack"] = str(traceback.format_exception(arg["err"]))
|
|
188
|
+
if "msg" not in argcopy:
|
|
189
|
+
argcopy["msg"] = str(str(arg["err"]))
|
|
190
|
+
|
|
191
|
+
if ("err" in argcopy): del argcopy["err"]
|
|
192
|
+
if ("cerr" in argcopy): del argcopy["cerr"]
|
|
193
|
+
|
|
194
|
+
if (type(arg) == str):
|
|
195
|
+
return arg
|
|
196
|
+
if (_crossauth_logger_accepts_json):
|
|
197
|
+
return argcopy
|
|
198
|
+
return json.dumps(argcopy)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_crossauth_logger = CrossauthLogger(None)
|
|
202
|
+
_crossauth_logger_accepts_json = True
|