pypomes-iam 0.3.0__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of pypomes-iam might be problematic. Click here for more details.
- pypomes_iam/__init__.py +5 -0
- pypomes_iam/iam_common.py +241 -302
- pypomes_iam/iam_pomes.py +279 -169
- pypomes_iam/iam_services.py +243 -0
- pypomes_iam/jusbr_pomes.py +20 -14
- pypomes_iam/keycloak_pomes.py +38 -21
- pypomes_iam/token_pomes.py +19 -4
- {pypomes_iam-0.3.0.dist-info → pypomes_iam-0.3.2.dist-info}/METADATA +1 -1
- pypomes_iam-0.3.2.dist-info/RECORD +12 -0
- pypomes_iam-0.3.0.dist-info/RECORD +0 -11
- {pypomes_iam-0.3.0.dist-info → pypomes_iam-0.3.2.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.3.0.dist-info → pypomes_iam-0.3.2.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_common.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
3
|
import sys
|
|
6
4
|
from cachetools import Cache
|
|
7
5
|
from datetime import datetime
|
|
8
6
|
from enum import StrEnum
|
|
9
|
-
from flask import Request
|
|
10
7
|
from logging import Logger
|
|
11
8
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
12
9
|
from pypomes_crypto import crypto_jwk_convert
|
|
@@ -14,21 +11,27 @@ from typing import Any, Final
|
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
class IamServer(StrEnum):
|
|
14
|
+
"""
|
|
15
|
+
Supported IAM servers.
|
|
16
|
+
"""
|
|
17
17
|
IAM_JUSRBR = "iam-jusbr",
|
|
18
18
|
IAM_KEYCLOAK = "iam-keycloak"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
# the logger for IAM operations
|
|
22
|
+
__IAM_LOGGER: Logger | None = None
|
|
23
|
+
|
|
21
24
|
# registry structure:
|
|
22
25
|
# { <IamServer>:
|
|
23
26
|
# {
|
|
24
27
|
# "client-id": <str>,
|
|
25
28
|
# "client-secret": <str>,
|
|
26
29
|
# "client-timeout": <int>,
|
|
30
|
+
# "recipient-attr": <str>,
|
|
27
31
|
# "public_key": <str>,
|
|
28
32
|
# "pk-lifetime": <int>,
|
|
29
33
|
# "pk-expiration": <int>,
|
|
30
34
|
# "base-url": <str>,
|
|
31
|
-
# "logger": <Logger>,
|
|
32
35
|
# "cache": <FIFOCache>,
|
|
33
36
|
# "redirect-uri": <str> <-- transient
|
|
34
37
|
# },
|
|
@@ -48,380 +51,316 @@ class IamServer(StrEnum):
|
|
|
48
51
|
# },
|
|
49
52
|
# ...
|
|
50
53
|
# }
|
|
51
|
-
|
|
54
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
def
|
|
55
|
-
args: dict[str, Any],
|
|
56
|
-
logger: Logger | None) -> dict[str, str]:
|
|
57
|
+
def _get_logger() -> Logger | None:
|
|
57
58
|
"""
|
|
58
|
-
|
|
59
|
+
Retrieve the registered logger for *IAM* operations.
|
|
59
60
|
|
|
60
|
-
:
|
|
61
|
-
:param args: the arguments passed when requesting the service
|
|
62
|
-
:param logger: optional logger
|
|
63
|
-
:return: the callback URL, with the appropriate parameters
|
|
61
|
+
:return: the registered logger for *IAM* operations.
|
|
64
62
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
70
|
-
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
71
|
-
user_id=oauth_state,
|
|
72
|
-
logger=logger)
|
|
73
|
-
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
74
|
-
user_data["login-id"] = user_id
|
|
75
|
-
timeout: int = _get_login_timeout(registry=registry)
|
|
76
|
-
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
|
|
77
|
-
redirect_uri: str = args.get("redirect-uri")
|
|
78
|
-
registry["redirect-uri"] = redirect_uri
|
|
79
|
-
|
|
80
|
-
# build the login url
|
|
81
|
-
return {
|
|
82
|
-
"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
83
|
-
f"?response_type=code&scope=openid"
|
|
84
|
-
f"&client_id={registry["client-id"]}"
|
|
85
|
-
f"&redirect_uri={redirect_uri}"
|
|
86
|
-
f"&state={oauth_state}")
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _service_logout(registry: dict[str, Any],
|
|
91
|
-
args: dict[str, Any],
|
|
92
|
-
logger: Logger | None) -> None:
|
|
63
|
+
return __IAM_LOGGER
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _register_logger(logger: Logger) -> None:
|
|
93
67
|
"""
|
|
94
|
-
|
|
68
|
+
Register the logger for *IAM* operations
|
|
95
69
|
|
|
96
|
-
:param
|
|
97
|
-
:param args: the arguments passed when requesting the service
|
|
98
|
-
:param logger: optional logger
|
|
70
|
+
:param logger: the logger to be rergistered
|
|
99
71
|
"""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if user_id:
|
|
103
|
-
cache: Cache = registry["cache"]
|
|
104
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
105
|
-
if user_id in users:
|
|
106
|
-
users.pop(user_id)
|
|
107
|
-
if logger:
|
|
108
|
-
logger.debug(msg=f"User '{user_id}' removed from the registry")
|
|
72
|
+
global __IAM_LOGGER
|
|
73
|
+
__IAM_LOGGER = logger
|
|
109
74
|
|
|
110
75
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
logger: Logger | None) -> tuple[str, str]:
|
|
76
|
+
def _get_public_key(iam_server: IamServer,
|
|
77
|
+
errors: list[str] | None,
|
|
78
|
+
logger: Logger | None) -> str:
|
|
115
79
|
"""
|
|
116
|
-
|
|
80
|
+
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
117
81
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
:param
|
|
82
|
+
The public key is saved in *iam_server*'s registry.
|
|
83
|
+
|
|
84
|
+
:param iam_server: the reference registered *IAM* server
|
|
85
|
+
:param errors: incidental error messages
|
|
121
86
|
:param logger: optional logger
|
|
87
|
+
:return: the public key in *PEM* format, or *None* if the server is unknown
|
|
122
88
|
"""
|
|
123
|
-
from .token_pomes import token_validate
|
|
124
|
-
|
|
125
89
|
# initialize the return variable
|
|
126
|
-
result:
|
|
127
|
-
|
|
128
|
-
# retrieve the users authentication data
|
|
129
|
-
cache: Cache = registry["cache"]
|
|
130
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
131
|
-
|
|
132
|
-
# validate the OAuth2 state
|
|
133
|
-
oauth_state: str = args.get("state")
|
|
134
|
-
user_data: dict[str, Any] | None = None
|
|
135
|
-
if oauth_state:
|
|
136
|
-
for user, data in users.items():
|
|
137
|
-
if user == oauth_state:
|
|
138
|
-
user_data = data
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
# exchange 'code' for the token
|
|
142
|
-
if user_data:
|
|
143
|
-
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
144
|
-
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
145
|
-
errors.append("Operation timeout")
|
|
146
|
-
else:
|
|
147
|
-
users.pop(oauth_state)
|
|
148
|
-
code: str = args.get("code")
|
|
149
|
-
body_data: dict[str, Any] = {
|
|
150
|
-
"grant_type": "authorization_code",
|
|
151
|
-
"code": code,
|
|
152
|
-
"redirect_uri": registry.get("redirect-uri"),
|
|
153
|
-
}
|
|
154
|
-
token = _post_for_token(registry=registry,
|
|
155
|
-
user_data=user_data,
|
|
156
|
-
body_data=body_data,
|
|
157
|
-
errors=errors,
|
|
158
|
-
logger=logger)
|
|
159
|
-
# retrieve the token's claims
|
|
160
|
-
if not errors:
|
|
161
|
-
public_key: str = _get_public_key(registry=registry,
|
|
162
|
-
logger=logger)
|
|
163
|
-
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
164
|
-
issuer=registry["base-url"],
|
|
165
|
-
public_key=public_key,
|
|
166
|
-
errors=errors,
|
|
167
|
-
logger=logger)
|
|
168
|
-
if not errors:
|
|
169
|
-
token_user: str = token_claims["payload"].get("preferred_username")
|
|
170
|
-
login_id = user_data.pop("login-id", None)
|
|
171
|
-
if not login_id or (login_id == token_user):
|
|
172
|
-
users[token_user] = user_data
|
|
173
|
-
result = (token_user, token)
|
|
174
|
-
else:
|
|
175
|
-
errors.append(f"Token was issued to user '{token_user}'")
|
|
176
|
-
else:
|
|
177
|
-
errors.append("Unknown state received")
|
|
90
|
+
result: str | None = None
|
|
178
91
|
|
|
179
|
-
|
|
180
|
-
|
|
92
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
93
|
+
errors=errors,
|
|
94
|
+
logger=logger)
|
|
95
|
+
if registry:
|
|
96
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
97
|
+
if now > registry["pk-expiration"]:
|
|
98
|
+
# obtain a new public key
|
|
99
|
+
url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
|
|
100
|
+
if logger:
|
|
101
|
+
logger.debug(msg=f"GET '{url}'")
|
|
102
|
+
try:
|
|
103
|
+
response: requests.Response = requests.get(url=url)
|
|
104
|
+
if response.status_code == 200:
|
|
105
|
+
# request succeeded
|
|
106
|
+
if logger:
|
|
107
|
+
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
108
|
+
reply: dict[str, Any] = response.json()
|
|
109
|
+
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
110
|
+
fmt="PEM")
|
|
111
|
+
registry["public-key"] = result
|
|
112
|
+
lifetime: int = registry["pk-lifetime"] or 0
|
|
113
|
+
registry["pk-expiration"] = now + lifetime
|
|
114
|
+
elif logger:
|
|
115
|
+
msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
|
|
116
|
+
if hasattr(response, "content") and response.content:
|
|
117
|
+
msg += f", content '{response.content}'"
|
|
118
|
+
logger.error(msg=msg)
|
|
119
|
+
if isinstance(errors, list):
|
|
120
|
+
errors.append(msg)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
# the operation raised an exception
|
|
123
|
+
msg = exc_format(exc=e,
|
|
124
|
+
exc_info=sys.exc_info())
|
|
125
|
+
if logger:
|
|
126
|
+
logger.error(msg=msg)
|
|
127
|
+
if isinstance(errors, list):
|
|
128
|
+
errors.append(msg)
|
|
129
|
+
else:
|
|
130
|
+
result = registry["public-key"]
|
|
181
131
|
|
|
182
132
|
return result
|
|
183
133
|
|
|
184
134
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
logger: Logger = None) -> str:
|
|
135
|
+
def _get_login_timeout(iam_server: IamServer,
|
|
136
|
+
errors: list[str] | None,
|
|
137
|
+
logger: Logger) -> int | None:
|
|
189
138
|
"""
|
|
190
|
-
Retrieve the
|
|
139
|
+
Retrieve the timeout currently applicable for the login operation.
|
|
191
140
|
|
|
192
|
-
:param
|
|
193
|
-
:param args: the arguments passed when requesting the service
|
|
141
|
+
:param iam_server: the reference registered *IAM* server
|
|
194
142
|
:param errors: incidental error messages
|
|
195
143
|
:param logger: optional logger
|
|
196
|
-
:return: the
|
|
144
|
+
:return: the current login timeout, or *None* if the server is unknown or none has been set.
|
|
197
145
|
"""
|
|
198
146
|
# initialize the return variable
|
|
199
|
-
result:
|
|
147
|
+
result: int | None = None
|
|
200
148
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
access_expiration: int = user_data.get("access-expiration")
|
|
209
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
210
|
-
if now < access_expiration:
|
|
211
|
-
result = token
|
|
212
|
-
else:
|
|
213
|
-
# access token has expired
|
|
214
|
-
refresh_token: str = user_data["refresh-token"]
|
|
215
|
-
if refresh_token:
|
|
216
|
-
refresh_expiration = user_data["refresh-expiration"]
|
|
217
|
-
if now < refresh_expiration:
|
|
218
|
-
body_data: dict[str, str] = {
|
|
219
|
-
"grant_type": "refresh_token",
|
|
220
|
-
"refresh_token": refresh_token
|
|
221
|
-
}
|
|
222
|
-
result = _post_for_token(registry=registry,
|
|
223
|
-
user_data=user_data,
|
|
224
|
-
body_data=body_data,
|
|
225
|
-
errors=errors,
|
|
226
|
-
logger=logger)
|
|
227
|
-
else:
|
|
228
|
-
# refresh token has expired
|
|
229
|
-
err_msg = "Access and refresh tokens expired"
|
|
230
|
-
else:
|
|
231
|
-
err_msg = "Access token expired, no refresh token available"
|
|
232
|
-
else:
|
|
233
|
-
err_msg = f"User '{user_id}' not authenticated"
|
|
234
|
-
|
|
235
|
-
if err_msg and (logger or isinstance(errors, list)):
|
|
236
|
-
err_msg: str = f"User '{user_id}' not authenticated"
|
|
237
|
-
if isinstance(errors, list):
|
|
238
|
-
errors.append(err_msg)
|
|
239
|
-
if logger:
|
|
240
|
-
logger.error(msg=err_msg)
|
|
241
|
-
logger.error(msg=err_msg)
|
|
149
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
150
|
+
errors=errors,
|
|
151
|
+
logger=logger)
|
|
152
|
+
if registry:
|
|
153
|
+
timeout: int = registry.get("client-timeout")
|
|
154
|
+
if isinstance(timeout, int) and timeout > 0:
|
|
155
|
+
result = timeout
|
|
242
156
|
|
|
243
157
|
return result
|
|
244
158
|
|
|
245
159
|
|
|
246
|
-
def
|
|
247
|
-
|
|
160
|
+
def _get_user_data(iam_server: IamServer,
|
|
161
|
+
user_id: str,
|
|
162
|
+
errors: list[str] | None,
|
|
163
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
248
164
|
"""
|
|
249
|
-
|
|
165
|
+
Retrieve the data for *user_id* from *iam_server*'s registry.
|
|
250
166
|
|
|
251
|
-
|
|
167
|
+
If an entry is not found for *user_id* in the registry, it is created.
|
|
168
|
+
It will remain there until the user is logged out.
|
|
252
169
|
|
|
253
|
-
:param
|
|
254
|
-
:
|
|
170
|
+
:param iam_server: the reference registered *IAM* server
|
|
171
|
+
:param errors: incidental error messages
|
|
172
|
+
:param logger: optional logger
|
|
173
|
+
:return: the data for *user_id* in *iam_server*'s registry, or *None* if the server is unknown
|
|
255
174
|
"""
|
|
256
175
|
# initialize the return variable
|
|
257
|
-
result: str | None = None
|
|
176
|
+
result: dict[str, Any] | None = None
|
|
258
177
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
178
|
+
cache: Cache = _get_iam_cache(iam_server=iam_server,
|
|
179
|
+
errors=errors,
|
|
180
|
+
logger=logger)
|
|
181
|
+
if cache:
|
|
182
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
183
|
+
result = users.get(user_id)
|
|
184
|
+
if not result:
|
|
185
|
+
result = {
|
|
186
|
+
"access-token": None,
|
|
187
|
+
"refresh-token": None,
|
|
188
|
+
"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
189
|
+
"refresh-expiration": sys.maxsize
|
|
190
|
+
}
|
|
191
|
+
users[user_id] = result
|
|
268
192
|
if logger:
|
|
269
|
-
logger.debug(msg=f"
|
|
270
|
-
reply: dict[str, Any] = response.json()
|
|
271
|
-
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
272
|
-
fmt="PEM")
|
|
273
|
-
registry["public-key"] = result
|
|
274
|
-
lifetime: int = registry["pk-lifetime"] or 0
|
|
275
|
-
registry["pk-expiration"] = now + lifetime
|
|
193
|
+
logger.debug(msg=f"Entry for '{user_id}' added to {iam_server}'s registry")
|
|
276
194
|
elif logger:
|
|
277
|
-
msg
|
|
278
|
-
if hasattr(response, "content") and response.content:
|
|
279
|
-
msg += f", content '{response.content}'"
|
|
280
|
-
logger.error(msg=msg)
|
|
281
|
-
else:
|
|
282
|
-
result = registry["public-key"]
|
|
195
|
+
logger.debug(msg=f"Entry for '{user_id}' obtained from {iam_server}'s registry")
|
|
283
196
|
|
|
284
197
|
return result
|
|
285
198
|
|
|
286
199
|
|
|
287
|
-
def
|
|
200
|
+
def _get_iam_server(endpoint: str,
|
|
201
|
+
errors: list[str] | None,
|
|
202
|
+
logger: Logger | None) -> IamServer | None:
|
|
288
203
|
"""
|
|
289
|
-
Retrieve
|
|
204
|
+
Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
|
|
290
205
|
|
|
291
|
-
:param
|
|
292
|
-
:
|
|
206
|
+
:param endpoint: the service's invocation endpoint
|
|
207
|
+
:param errors: incidental error messages
|
|
208
|
+
:param logger: optional logger
|
|
209
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
293
210
|
"""
|
|
294
|
-
|
|
295
|
-
|
|
211
|
+
# declare the return variable
|
|
212
|
+
result: IamServer | None
|
|
296
213
|
|
|
214
|
+
if endpoint.startswith("jusbr"):
|
|
215
|
+
result = IamServer.IAM_JUSRBR
|
|
216
|
+
elif endpoint.startswith("keycloak"):
|
|
217
|
+
result = IamServer.IAM_KEYCLOAK
|
|
218
|
+
else:
|
|
219
|
+
result = None
|
|
220
|
+
msg: str = f"Unknown endpoind {endpoint}"
|
|
221
|
+
if logger:
|
|
222
|
+
logger.error(msg=msg)
|
|
223
|
+
if isinstance(errors, list):
|
|
224
|
+
errors.append(msg)
|
|
297
225
|
|
|
298
|
-
|
|
299
|
-
user_id: str,
|
|
300
|
-
logger: Logger | None) -> dict[str, Any]:
|
|
301
|
-
"""
|
|
302
|
-
Retrieve the data for *user_id* from *registry*.
|
|
226
|
+
return result
|
|
303
227
|
|
|
304
|
-
If an entry is not found for *user_id* in the registry, it is created.
|
|
305
|
-
It will remain there until the user is logged out.
|
|
306
228
|
|
|
307
|
-
|
|
308
|
-
|
|
229
|
+
def _get_iam_registry(iam_server: IamServer,
|
|
230
|
+
errors: list[str] | None,
|
|
231
|
+
logger: Logger | None) -> dict[str, Any]:
|
|
309
232
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
233
|
+
Retrieve the registry associated with *iam_server*.
|
|
234
|
+
|
|
235
|
+
:param iam_server: the reference registered *IAM* server
|
|
236
|
+
:param errors: incidental error messages
|
|
237
|
+
:param logger: optional logger
|
|
238
|
+
:return: the registry associated with *iam_server*, or *None* if the server is unknown
|
|
239
|
+
"""
|
|
240
|
+
# declare the return variable
|
|
241
|
+
result: dict[str, Any] | None
|
|
242
|
+
|
|
243
|
+
match iam_server:
|
|
244
|
+
case IamServer.IAM_JUSRBR:
|
|
245
|
+
result = _IAM_SERVERS[IamServer.IAM_JUSRBR]
|
|
246
|
+
case IamServer.IAM_KEYCLOAK:
|
|
247
|
+
result = _IAM_SERVERS[IamServer.IAM_KEYCLOAK]
|
|
248
|
+
case _:
|
|
249
|
+
result = None
|
|
250
|
+
msg = f"Unknown IAM server '{iam_server}'"
|
|
251
|
+
if logger:
|
|
252
|
+
logger.error(msg=msg)
|
|
253
|
+
if isinstance(errors, list):
|
|
254
|
+
errors.append(msg)
|
|
325
255
|
|
|
326
256
|
return result
|
|
327
257
|
|
|
328
258
|
|
|
329
|
-
def
|
|
330
|
-
|
|
259
|
+
def _get_iam_cache(iam_server: IamServer,
|
|
260
|
+
errors: list[str] | None,
|
|
261
|
+
logger: Logger | None) -> Cache:
|
|
262
|
+
"""
|
|
263
|
+
Retrieve the cache storage in *iam_server*'s registry.
|
|
264
|
+
|
|
265
|
+
:param iam_server: the reference registered *IAM* server
|
|
266
|
+
:param errors: incidental error messages
|
|
267
|
+
:param logger: optional logger
|
|
268
|
+
:return: the cache storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
269
|
+
"""
|
|
270
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
271
|
+
errors=errors,
|
|
272
|
+
logger=logger)
|
|
273
|
+
return registry["cache"] if registry else None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _post_for_token(iam_server: IamServer,
|
|
331
277
|
body_data: dict[str, Any],
|
|
332
278
|
errors: list[str] | None,
|
|
333
|
-
logger: Logger | None) -> str | None:
|
|
279
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
334
280
|
"""
|
|
335
|
-
Send a POST request to obtain the authentication token data, and return the
|
|
281
|
+
Send a POST request to obtain the authentication token data, and return the data received.
|
|
336
282
|
|
|
337
|
-
For token
|
|
283
|
+
For token acquisition, *body_data* will have the attributes:
|
|
338
284
|
- "grant_type": "authorization_code"
|
|
339
285
|
- "code": <16-character-random-code>
|
|
340
286
|
- "redirect_uri": <redirect-uri>
|
|
341
|
-
|
|
287
|
+
|
|
288
|
+
For token refresh, *body_data* will have the attributes:
|
|
342
289
|
- "grant_type": "refresh_token"
|
|
343
290
|
- "refresh_token": <current-refresh-token>
|
|
344
291
|
|
|
292
|
+
For token exchange, *body_data* will have the attributes:
|
|
293
|
+
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
294
|
+
- "subject_token": <token-to-be-exchanged>,
|
|
295
|
+
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
296
|
+
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
297
|
+
- "audience": <client-id>,
|
|
298
|
+
- "subject_issuer": "oidc"
|
|
299
|
+
|
|
300
|
+
These attributes are then added to *body_data*:
|
|
301
|
+
- "client_id": <client-id>,
|
|
302
|
+
- "client_secret": <client-secret>,
|
|
303
|
+
|
|
345
304
|
If the operation is successful, the token data is stored in the registry.
|
|
346
305
|
Otherwise, *errors* will contain the appropriate error message.
|
|
347
306
|
|
|
348
|
-
:param
|
|
349
|
-
:param user_data: the user's data in the registry
|
|
307
|
+
:param iam_server: the reference registered *IAM* server
|
|
350
308
|
:param body_data: the data to send in the body of the request
|
|
351
309
|
:param errors: incidental errors
|
|
352
310
|
:param logger: optional logger
|
|
353
311
|
:return: the access token obtained, or *None* if error
|
|
354
312
|
"""
|
|
355
313
|
# initialize the return variable
|
|
356
|
-
result: str | None = None
|
|
357
|
-
|
|
358
|
-
# complete the data to send in body of request
|
|
359
|
-
body_data["client_id"] = registry["client-id"]
|
|
360
|
-
client_secret: str = registry["client-secret"]
|
|
361
|
-
if client_secret:
|
|
362
|
-
body_data["client_secret"] = client_secret
|
|
314
|
+
result: dict[str, Any] | None = None
|
|
363
315
|
|
|
364
|
-
#
|
|
316
|
+
# PBTAIN THE iam SERVER'S REGISTRY
|
|
317
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
318
|
+
errors=errors,
|
|
319
|
+
logger=logger)
|
|
365
320
|
err_msg: str | None = None
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
# "access_token": <str>,
|
|
376
|
-
# "expires_in": <number-of-seconds>,
|
|
377
|
-
# "refresh_token": <str>,
|
|
378
|
-
# "refesh_expires_in": <number-of-seconds>
|
|
379
|
-
# }
|
|
380
|
-
response: requests.Response = requests.post(url=url,
|
|
381
|
-
data=body_data)
|
|
382
|
-
if response.status_code == 200:
|
|
383
|
-
# request succeeded
|
|
384
|
-
if logger:
|
|
385
|
-
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
386
|
-
reply: dict[str, Any] = response.json()
|
|
387
|
-
result = reply.get("access_token")
|
|
388
|
-
user_data["access-token"] = result
|
|
389
|
-
# on token refresh, keep current refresh token if a new one is not provided
|
|
390
|
-
user_data["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
391
|
-
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
392
|
-
refresh_expiration: int = user_data.get("refresh_expires_in")
|
|
393
|
-
user_data["refresh-expiration"] = (now + refresh_expiration) if refresh_expiration else sys.maxsize
|
|
394
|
-
else:
|
|
395
|
-
# request resulted in error
|
|
396
|
-
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
397
|
-
if hasattr(response, "content") and response.content:
|
|
398
|
-
err_msg += f", content '{response.content}'"
|
|
399
|
-
if response.status_code == 400 and body_data.get("grant_type") == "refresh_token":
|
|
400
|
-
# refresh token is no longer valid
|
|
401
|
-
user_data["refresh-token"] = None
|
|
402
|
-
except Exception as e:
|
|
403
|
-
# the operation raised an exception
|
|
404
|
-
err_msg = exc_format(exc=e,
|
|
405
|
-
exc_info=sys.exc_info())
|
|
406
|
-
err_msg = f"POST '{url}': error '{err_msg}'"
|
|
407
|
-
|
|
408
|
-
if err_msg:
|
|
409
|
-
if isinstance(errors, list):
|
|
410
|
-
errors.append(err_msg)
|
|
321
|
+
if registry:
|
|
322
|
+
# complete the data to send in body of request
|
|
323
|
+
body_data["client_id"] = registry["client-id"]
|
|
324
|
+
client_secret: str = registry["client-secret"]
|
|
325
|
+
if client_secret:
|
|
326
|
+
body_data["client_secret"] = client_secret
|
|
327
|
+
|
|
328
|
+
# obtain the token
|
|
329
|
+
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
411
330
|
if logger:
|
|
412
|
-
logger.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
331
|
+
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
332
|
+
ensure_ascii=False)}")
|
|
333
|
+
try:
|
|
334
|
+
# typical return on a token request:
|
|
335
|
+
# {
|
|
336
|
+
# "token_type": "Bearer",
|
|
337
|
+
# "access_token": <str>,
|
|
338
|
+
# "expires_in": <number-of-seconds>,
|
|
339
|
+
# "refresh_token": <str>,
|
|
340
|
+
# "refesh_expires_in": <number-of-seconds>
|
|
341
|
+
# }
|
|
342
|
+
response: requests.Response = requests.post(url=url,
|
|
343
|
+
data=body_data)
|
|
344
|
+
if response.status_code == 200:
|
|
345
|
+
# request succeeded
|
|
346
|
+
if logger:
|
|
347
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
348
|
+
result = response.json()
|
|
349
|
+
else:
|
|
350
|
+
# request resulted in error
|
|
351
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
352
|
+
if hasattr(response, "content") and response.content:
|
|
353
|
+
err_msg += f", content '{response.content}'"
|
|
354
|
+
if logger:
|
|
355
|
+
logger.error(msg=err_msg)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# the operation raised an exception
|
|
358
|
+
err_msg = exc_format(exc=e,
|
|
359
|
+
exc_info=sys.exc_info())
|
|
360
|
+
if logger:
|
|
361
|
+
logger.error(msg=err_msg)
|
|
420
362
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"""
|
|
363
|
+
if err_msg and isinstance(errors, list):
|
|
364
|
+
errors.append(err_msg)
|
|
424
365
|
|
|
425
|
-
|
|
426
|
-
ensure_ascii=False)
|
|
427
|
-
return f"Request {request.method}:{request.path}, params {params}"
|
|
366
|
+
return result
|