pypomes-iam 0.6.0__py3-none-any.whl → 0.6.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.

@@ -59,7 +59,7 @@ def action_login(iam_server: IamServer,
59
59
  errors=errors,
60
60
  logger=logger)
61
61
  if not errors:
62
- user_data[UserParam.LOGIN_EXPIRATION] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
62
+ user_data[UserParam.LOGIN_EXPIRATION] = (int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout) \
63
63
  if timeout else None
64
64
  redirect_uri: str = args.get(UserParam.REDIRECT_URI)
65
65
  user_data[UserParam.REDIRECT_URI] = redirect_uri
@@ -138,6 +138,7 @@ def action_token(iam_server: IamServer,
138
138
  user_id=user_id,
139
139
  errors=errors,
140
140
  logger=logger)
141
+ # retrieve the stored access token
141
142
  token: str = user_data[UserParam.ACCESS_TOKEN] if user_data else None
142
143
  if token:
143
144
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
@@ -148,7 +149,7 @@ def action_token(iam_server: IamServer,
148
149
  # access token has expired
149
150
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
150
151
  if refresh_token:
151
- refresh_expiration = user_data[UserParam.REFRESH_EXPIRATION]
152
+ refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
152
153
  if now < refresh_expiration:
153
154
  header_data: dict[str, str] = {
154
155
  "Content-Type": "application/json"
@@ -242,7 +243,7 @@ def action_callback(iam_server: IamServer,
242
243
  users.pop(oauth_state)
243
244
  code: str = args.get("code")
244
245
  header_data: dict[str, str] = {
245
- "Content-Type": "application/json"
246
+ "Content-Type": "application/x-www-form-urlencoded"
246
247
  }
247
248
  body_data: dict[str, Any] = {
248
249
  "grant_type": "authorization_code",
@@ -456,7 +457,7 @@ def __get_administrative_token(iam_server: IamServer,
456
457
  logger=logger)
457
458
  if registry and registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
458
459
  header_data: dict[str, str] = {
459
- "Content-Type": "application/json"
460
+ "Content-Type": "application/x-www-form-urlencoded"
460
461
  }
461
462
  body_data: dict[str, str] = {
462
463
  "grant-type": "password",
@@ -573,7 +574,7 @@ def __get_for_data(url: str,
573
574
  logger.debug(msg=f"GET success, {json.dumps(obj=result,
574
575
  ensure_ascii=False)}")
575
576
  else:
576
- # request resulted in error
577
+ # request failed, report the problem
577
578
  msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
578
579
  if hasattr(response, "content") and response.content:
579
580
  msg += f", content '{response.content}'"
@@ -615,7 +616,7 @@ def __post_data(url: str,
615
616
  headers=header_data,
616
617
  data=body_data)
617
618
  if response.status_code >= 400:
618
- # request resulted in error
619
+ # request failed, report the problem
619
620
  msg = f"POST failure, status {response.status_code}, reason {response.reason}"
620
621
  if hasattr(response, "content") and response.content:
621
622
  msg += f", content '{response.content}'"
@@ -735,7 +736,7 @@ def __post_for_token(iam_server: IamServer,
735
736
  logger.debug(msg=f"POST success, {json.dumps(obj=result,
736
737
  ensure_ascii=False)}")
737
738
  else:
738
- # request resulted in error
739
+ # request failed, report the problem
739
740
  err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
740
741
  if hasattr(response, "content") and response.content:
741
742
  err_msg += f", content '{response.content}'"
pypomes_iam/iam_common.py CHANGED
@@ -58,16 +58,17 @@ class UserParam(StrEnum):
58
58
  # Specifying configuration parameters with environment variables can be done in two ways:
59
59
  #
60
60
  # 1. for a single *IAM* server, specify the data set
61
- # - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed only if administrative duties are performed)
62
- # - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed only if administrative duties are performed)
61
+ # - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed if administrative duties are performed)
62
+ # - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed if administrative duties are performed)
63
63
  # - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
64
64
  # - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
65
65
  # - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
66
- # - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (optional)
67
- # - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (optional)
68
- # - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (optional)
69
- # - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (optional)
70
- # - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (optional)
66
+ # - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (required)
67
+ # - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (required)
68
+ # - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (required)
69
+ # - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (required)
70
+ # - *<APP_PREFIX>_IAM_ENDPOINT_PROVIDER* (optional, needed if requesting tokens to providers)
71
+ # - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (required)
71
72
  # - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
72
73
  # - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
73
74
  # - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
@@ -106,76 +106,137 @@ def provider_get_token(provider_id: str,
106
106
  # initialize the return variable
107
107
  result: str | None = None
108
108
 
109
- err_msg: str | None = None
110
109
  with _provider_lock:
111
110
  provider: dict[str, Any] = _provider_registry.get(provider_id)
112
111
  if provider:
113
- now: float = datetime.now(tz=TZ_LOCAL).timestamp()
114
- if now > provider.get(ProviderParam.ACCESS_EXPIRATION):
115
- user: str = provider.get(ProviderParam.USER)
116
- pwd: str = provider.get(ProviderParam.PWD)
117
- headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
118
- body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
119
- custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
120
- if custom_auth:
121
- body_data[custom_auth[0]] = user
122
- body_data[custom_auth[1]] = pwd
123
- else:
124
- enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
125
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
112
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
113
+ if now < provider.get(ProviderParam.ACCESS_EXPIRATION):
114
+ # retrieve the stored access token
115
+ result = provider.get(ProviderParam.ACCESS_TOKEN)
116
+ else:
117
+ # access token has expired
118
+ header_data: dict[str, str] | None = None
119
+ body_data: dict[str, str] | None = None
126
120
  url: str = provider.get(ProviderParam.URL)
127
- if logger:
128
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
129
- ensure_ascii=False)}")
130
- try:
131
- # typical return on a token request:
132
- # {
133
- # "token_type": "Bearer",
134
- # "access_token": <str>,
135
- # "expires_in": <number-of-seconds>,
136
- # optional data:
137
- # "refresh_token": <str>,
138
- # "refresh_expires_in": <number-of-seconds>
139
- # }
140
- response: requests.Response = requests.post(url=url,
141
- data=body_data,
142
- headers=headers_data,
143
- timeout=None)
144
- if response.status_code < 200 or response.status_code >= 300:
145
- # request resulted in error, report the problem
146
- err_msg = (f"POST failure, "
147
- f"status {response.status_code}, reason {response.reason}")
121
+ refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
122
+ if refresh_token:
123
+ # refresh token exists
124
+ refresh_expiration: int = provider.get(ProviderParam.REFRESH_EXPIRATION)
125
+ if now < refresh_expiration:
126
+ # refresh token has not expired
127
+ header_data: dict[str, str] = {
128
+ "Content-Type": "application/json"
129
+ }
130
+ body_data: dict[str, str] = {
131
+ "grant_type": "refresh_token",
132
+ "refresh_token": refresh_token
133
+ }
134
+ if not body_data:
135
+ # refresh token does not exist or has expired
136
+ user: str = provider.get(ProviderParam.USER)
137
+ pwd: str = provider.get(ProviderParam.PWD)
138
+ headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
139
+ body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
140
+ custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
141
+ if custom_auth:
142
+ body_data[custom_auth[0]] = user
143
+ body_data[custom_auth[1]] = pwd
148
144
  else:
149
- # request succeeded
150
- if logger:
151
- logger.debug(msg=f"POST success, status {response.status_code}")
152
- reply: dict[str, Any] = response.json()
153
- provider[ProviderParam.ACCESS_TOKEN] = reply.get("access_token")
154
- provider[ProviderParam.ACCESS_EXPIRATION] = now + int(reply.get("expires_in"))
155
- if reply.get(ProviderParam.REFRESH_TOKEN):
156
- provider[ProviderParam.REFRESH_TOKEN] = reply["refresh_token"]
157
- if reply.get("refresh_expires_in"):
158
- provider[ProviderParam.REFRESH_EXPIRATION] = now + int(reply.get("refresh_expires_in"))
159
- else:
160
- provider[ProviderParam.REFRESH_EXPIRATION] = sys.maxsize
161
- if logger:
162
- logger.debug(msg=f"POST {url}: status {response.status_code}")
163
- except Exception as e:
164
- # the operation raised an exception
165
- err_msg = exc_format(exc=e,
166
- exc_info=sys.exc_info())
167
- err_msg = f"POST error, '{err_msg}'"
168
- else:
169
- err_msg: str = f"Provider '{provider_id}' not registered"
170
-
171
- if err_msg:
172
- if isinstance(errors, list):
173
- errors.append(err_msg)
174
- if logger:
175
- logger.error(msg=err_msg)
176
- else:
177
- result = provider.get(ProviderParam.ACCESS_TOKEN)
145
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
146
+ headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
147
+
148
+ # obtain the token
149
+ token_data: dict[str, Any] = __post_for_token(url=url,
150
+ header_data=header_data,
151
+ body_data=body_data,
152
+ errors=errors,
153
+ logger=logger)
154
+ if token_data:
155
+ result = token_data.get("access_token")
156
+ provider[ProviderParam.ACCESS_TOKEN] = result
157
+ provider[ProviderParam.ACCESS_EXPIRATION] = now + token_data.get("expires_in")
158
+ refresh_token = token_data.get("refresh_token")
159
+ if refresh_token:
160
+ provider[ProviderParam.REFRESH_TOKEN] = refresh_token
161
+ refresh_exp: int = token_data.get("refresh_expires_in")
162
+ provider[ProviderParam.REFRESH_EXPIRATION] = (now + refresh_exp) \
163
+ if refresh_exp else sys.maxsize
164
+
165
+ elif logger or isinstance(errors, list):
166
+ msg: str = f"Unknown provider '{provider_id}'"
167
+ if logger:
168
+ logger.error(msg=msg)
169
+ if isinstance(errors, list):
170
+ errors.append(msg)
178
171
 
179
172
  return result
180
173
 
181
174
 
175
+ def __post_for_token(url: str,
176
+ header_data: dict[str, str],
177
+ body_data: dict[str, Any],
178
+ errors: list[str] | None,
179
+ logger: Logger | None) -> dict[str, Any] | None:
180
+ """
181
+ Send a *POST* request to *url* and return the token data obtained.
182
+
183
+ Token acquisition and token refresh are the two types of requests contemplated herein.
184
+ For the former, *header_data* and *body_data* will have contents customized to the specific provider,
185
+ whereas the latter's *body_data* will contain these two attributes:
186
+ - "grant_type": "refresh_token"
187
+ - "refresh_token": <current-refresh-token>
188
+
189
+ The typical data set returned contains the following attributes:
190
+ {
191
+ "token_type": "Bearer",
192
+ "access_token": <str>,
193
+ "expires_in": <number-of-seconds>,
194
+ "refresh_token": <str>,
195
+ "refesh_expires_in": <number-of-seconds>
196
+ }
197
+
198
+ :param url: the target URL
199
+ :param header_data: the data to send in the header of the request
200
+ :param body_data: the data to send in the body of the request
201
+ :param errors: incidental errors
202
+ :param logger: optional logger
203
+ :return: the token data, or *None* if error
204
+ """
205
+ # initialize the return variable
206
+ result: dict[str, Any] | None = None
207
+
208
+ # log the POST
209
+ if logger:
210
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
211
+ ensure_ascii=False)}")
212
+ try:
213
+ response: requests.Response = requests.post(url=url,
214
+ data=body_data,
215
+ headers=header_data,
216
+ timeout=None)
217
+ if response.status_code == 200:
218
+ # request succeeded
219
+ result = response.json()
220
+ if logger:
221
+ logger.debug(msg=f"POST success, status {response.status_code}")
222
+ else:
223
+ # request failed, report the problem
224
+ msg: str = (f"POST failure, "
225
+ f"status {response.status_code}, reason {response.reason}")
226
+ if hasattr(response, "content") and response.content:
227
+ msg += f", content '{response.content}'"
228
+ if logger:
229
+ logger.error(msg=msg)
230
+ if isinstance(errors, list):
231
+ errors.append(msg)
232
+ except Exception as e:
233
+ # the operation raised an exception
234
+ err_msg = exc_format(exc=e,
235
+ exc_info=sys.exc_info())
236
+ msg: str = f"POST error, {err_msg}"
237
+ if logger:
238
+ logger.debug(msg=msg)
239
+ if isinstance(errors, list):
240
+ errors.append(msg)
241
+
242
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A collection of Python pomes, penyeach (IAM modules)
5
5
  Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
6
6
  Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
- Requires-Dist: cachetools>=6.2.1
14
13
  Requires-Dist: flask>=3.1.2
15
14
  Requires-Dist: pyjwt>=2.10.1
16
15
  Requires-Dist: pypomes-core>=2.8.1
@@ -0,0 +1,11 @@
1
+ pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
+ pypomes_iam/iam_actions.py,sha256=DhDZcY6j3uSWnm5Q5SrA5jriTUCatLqJKd9K7IjA2i8,39283
3
+ pypomes_iam/iam_common.py,sha256=ki_-m6fqJqUbGjgTD41r9zaE-FOXgA_c_tLisIYYTfU,15457
4
+ pypomes_iam/iam_pomes.py,sha256=XkxpwwGivUR3Y1TKR6McrqLUnpFJhRvIrsEn9T_Ut9A,7351
5
+ pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
6
+ pypomes_iam/provider_pomes.py,sha256=3mMj5LQs53YEINUEOfFBAxOwOP3aOR_szlE4daEBLK0,10523
7
+ pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
+ pypomes_iam-0.6.2.dist-info/METADATA,sha256=XeUm7yxjoWFURVvoHDSoYkF5iROCC696PPuHydJ1UIs,661
9
+ pypomes_iam-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pypomes_iam-0.6.2.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
+ pypomes_iam-0.6.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
- pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
3
- pypomes_iam/iam_common.py,sha256=4sn9V4_fRXrljz41Dh7P6ng4aqFfp0pLnrq_rURDKt4,15363
4
- pypomes_iam/iam_pomes.py,sha256=XkxpwwGivUR3Y1TKR6McrqLUnpFJhRvIrsEn9T_Ut9A,7351
5
- pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
6
- pypomes_iam/provider_pomes.py,sha256=N0nL9_hgHmAjG9JKFoXC33zk8b1ckPG1veu1jTp-2JE,8045
7
- pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
- pypomes_iam-0.6.0.dist-info/METADATA,sha256=xl-01mkMhab71h8WMqADHpX5qxS5ngFfcMyhSouQwt0,694
9
- pypomes_iam-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.6.0.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.6.0.dist-info/RECORD,,