pypomes-iam 0.7.6__py3-none-any.whl → 0.8.0__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.
pypomes_iam/__init__.py CHANGED
@@ -1,37 +1,47 @@
1
1
  from .iam_actions import (
2
- action_callback, action_exchange,
3
- action_login, action_logout, action_token
2
+ iam_callback, iam_exchange,
3
+ iam_login, iam_logout, iam_get_token
4
4
  )
5
5
  from .iam_common import (
6
6
  IamServer, IamParam
7
7
  )
8
8
  from .iam_pomes import (
9
- iam_setup, iam_get_env_parameters, iam_get_token
9
+ iam_setup_server, iam_setup_endpoints
10
10
  )
11
11
  from .iam_services import (
12
- jwt_required, logger_register
12
+ jwt_required, iam_setup_logger,
13
+ service_setup_server, service_get_token,
14
+ service_login, service_logout,
15
+ service_callback, service_exchange,
16
+ service_callback_exchange
13
17
  )
14
18
  from .provider_pomes import (
15
- provider_register, provider_get_token
19
+ service_get_token, provider_get_token,
20
+ provider_setup_endpoint, provider_setup_logger, provider_setup_server
16
21
  )
17
22
  from .token_pomes import (
18
- token_validate
23
+ token_get_claims, token_get_values, token_validate
19
24
  )
20
25
 
21
26
  __all__ = [
22
27
  # iam_actions
23
- "action_callback", "action_exchange",
24
- "action_login", "action_logout", "action_token",
28
+ "iam_callback", "iam_exchange",
29
+ "iam_login", "iam_logout", "iam_get_token",
25
30
  # iam_commons
26
31
  "IamServer", "IamParam",
27
32
  # iam_pomes
28
- "iam_setup", "iam_get_env_parameters", "iam_get_token",
33
+ "iam_setup_server", "iam_setup_endpoints",
29
34
  # iam_services
30
- "jwt_required", "logger_register",
35
+ "jwt_required", "iam_setup_logger",
36
+ "service_setup_server", "service_get_token",
37
+ "service_login", "service_logout",
38
+ "service_callback", "service_exchange",
39
+ "service_callback_exchange",
31
40
  # provider_pomes
32
- "provider_register", "provider_get_token",
41
+ "provider_setup_server", "provider_get_token",
42
+ "provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
33
43
  # token_pomes
34
- "token_validate"
44
+ "token_get_claims", "token_get_values", "token_validate"
35
45
  ]
36
46
 
37
47
  from importlib.metadata import version
@@ -13,13 +13,13 @@ from .iam_common import (
13
13
  _get_iam_users, _get_iam_registry, _get_public_key,
14
14
  _get_login_timeout, _get_user_data, _iam_server_from_issuer
15
15
  )
16
- from .token_pomes import token_get_claims, token_validate
16
+ from .token_pomes import token_get_values, token_validate
17
17
 
18
18
 
19
- def action_login(iam_server: IamServer,
20
- args: dict[str, Any],
21
- errors: list[str] = None,
22
- logger: Logger = None) -> str:
19
+ def iam_login(iam_server: IamServer,
20
+ args: dict[str, Any],
21
+ errors: list[str] = None,
22
+ logger: Logger = None) -> str:
23
23
  """
24
24
  Build the URL for redirecting the request to *iam_server*'s authentication page.
25
25
 
@@ -32,6 +32,11 @@ def action_login(iam_server: IamServer,
32
32
  returned by *iam_server* upon login. On success, the appropriate URL for invoking
33
33
  the IAM server's authentication page is returned.
34
34
 
35
+ if 'target_idp' is provided as an attribute in *args*, the OAuth2 state variable included in the
36
+ returned URL will be postfixed with the string *#idp=<target-idp>*. At the callback endpoint,
37
+ this instructs *iam_server* to act as a broker, forwading the authentication process to the
38
+ *IAM* server *target-idp*.
39
+
35
40
  :param iam_server: the reference registered *IAM* server
36
41
  :param args: the arguments passed when requesting the service
37
42
  :param errors: incidental error messages
@@ -51,7 +56,7 @@ def action_login(iam_server: IamServer,
51
56
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
52
57
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
53
58
  if target_idp:
54
- oauth_state += f"idp={target_idp}"
59
+ oauth_state += f"#idp={target_idp}"
55
60
 
56
61
  with _iam_lock:
57
62
  # retrieve the user data from the IAM server's registry
@@ -88,10 +93,10 @@ def action_login(iam_server: IamServer,
88
93
  return result
89
94
 
90
95
 
91
- def action_logout(iam_server: IamServer,
92
- args: dict[str, Any],
93
- errors: list[str] = None,
94
- logger: Logger = None) -> None:
96
+ def iam_logout(iam_server: IamServer,
97
+ args: dict[str, Any],
98
+ errors: list[str] = None,
99
+ logger: Logger = None) -> None:
95
100
  """
96
101
  Logout the user, by removing all data associating it from *iam_server*'s registry.
97
102
 
@@ -119,23 +124,29 @@ def action_logout(iam_server: IamServer,
119
124
  logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
120
125
 
121
126
 
122
- def action_token(iam_server: IamServer,
123
- args: dict[str, Any],
124
- errors: list[str] = None,
125
- logger: Logger = None) -> str:
127
+ def iam_get_token(iam_server: IamServer,
128
+ args: dict[str, Any],
129
+ errors: list[str] = None,
130
+ logger: Logger = None) -> dict[str, str]:
126
131
  """
127
132
  Retrieve the authentication token for the user, from *iam_server*.
128
133
 
129
134
  The user is identified by the attribute *user-id* or *login*, provided in *args*.
130
135
 
136
+ On success, the returned *dict* will contain the following JSON:
137
+ {
138
+ "access-token": <token>,
139
+ "user-id": <user-identification
140
+ }
141
+
131
142
  :param iam_server: the reference registered *IAM* server
132
143
  :param args: the arguments passed when requesting the service
133
144
  :param errors: incidental error messages
134
145
  :param logger: optional logger
135
- :return: the token for user indicated, or *None* if error
146
+ :return: the user identification and token issued, or *None* if error
136
147
  """
137
148
  # initialize the return variable
138
- result: str | None = None
149
+ result: dict[str, str] | None = None
139
150
 
140
151
  # obtain the user's identification
141
152
  user_id: str = args.get("user-id") or args.get("login")
@@ -154,7 +165,10 @@ def action_token(iam_server: IamServer,
154
165
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
155
166
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
156
167
  if now < access_expiration:
157
- result = token
168
+ result = {
169
+ "access-token": token,
170
+ "user-id": user_id
171
+ }
158
172
  else:
159
173
  # access token has expired
160
174
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
@@ -182,7 +196,10 @@ def action_token(iam_server: IamServer,
182
196
  now=now,
183
197
  errors=errors,
184
198
  logger=logger)
185
- result = token_info[1]
199
+ result = {
200
+ "access-token": token_info[1],
201
+ "user-id": user_id
202
+ }
186
203
  else:
187
204
  # refresh token is no longer valid
188
205
  user_data[UserParam.REFRESH_TOKEN] = None
@@ -210,10 +227,10 @@ def action_token(iam_server: IamServer,
210
227
  return result
211
228
 
212
229
 
213
- def action_callback(iam_server: IamServer,
214
- args: dict[str, Any],
215
- errors: list[str] = None,
216
- logger: Logger = None) -> tuple[str, str] | None:
230
+ def iam_callback(iam_server: IamServer,
231
+ args: dict[str, Any],
232
+ errors: list[str] = None,
233
+ logger: Logger = None) -> tuple[str, str] | None:
217
234
  """
218
235
  Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
219
236
 
@@ -221,6 +238,10 @@ def action_callback(iam_server: IamServer,
221
238
  - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
222
239
  - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
223
240
 
241
+ if *state* is postfixed with the string *#idp=<target-idp>*, this instructs *iam_server* to act as a broker,
242
+ forwarding the authentication process to the *IAM* server *target-idp*. This mechanism fully dispenses with
243
+ the flows 'callback-exchange', and 'callback' followed by 'exchange'.
244
+
224
245
  :param iam_server: the reference registered *IAM* server
225
246
  :param args: the arguments passed when requesting the service
226
247
  :param errors: incidental errors
@@ -250,7 +271,7 @@ def action_callback(iam_server: IamServer,
250
271
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
251
272
  errors.append("Operation timeout")
252
273
  else:
253
- pos: int = oauth_state.rfind("idp=")
274
+ pos: int = oauth_state.rfind("#idp=")
254
275
  target_idp: str = oauth_state[pos+4:] if pos > 0 else None
255
276
  target_iam = IamServer(target_idp) if target_idp in IamServer else None
256
277
  target_data: dict[str, Any] = user_data.copy() if target_iam else None
@@ -315,10 +336,10 @@ def action_callback(iam_server: IamServer,
315
336
  return result
316
337
 
317
338
 
318
- def action_exchange(iam_server: IamServer,
319
- args: dict[str, Any],
320
- errors: list[str] = None,
321
- logger: Logger = None) -> tuple[str, str]:
339
+ def iam_exchange(iam_server: IamServer,
340
+ args: dict[str, Any],
341
+ errors: list[str] = None,
342
+ logger: Logger = None) -> tuple[str, str]:
322
343
  """
323
344
  Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
324
345
 
@@ -349,12 +370,10 @@ def action_exchange(iam_server: IamServer,
349
370
 
350
371
  # obtain the token to be exchanged
351
372
  token: str = args.get("access-token") if user_id else None
352
- token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
353
- errors=errors,
354
- logger=logger) if token else None
355
- token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
373
+ token_issuer: tuple[str] = token_get_values(token=token,
374
+ keys=("iss",),
356
375
  errors=errors,
357
- logger=logger) if token_claims else None
376
+ logger=logger)
358
377
  if not errors:
359
378
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
360
379
  with _iam_lock:
@@ -367,6 +386,7 @@ def action_exchange(iam_server: IamServer,
367
386
  __assert_link(iam_server=iam_server,
368
387
  user_id=user_id,
369
388
  token=token,
389
+ token_issuer=token_issuer[0],
370
390
  errors=errors,
371
391
  logger=logger)
372
392
  if not errors:
@@ -412,6 +432,7 @@ def action_exchange(iam_server: IamServer,
412
432
  def __assert_link(iam_server: IamServer,
413
433
  user_id: str,
414
434
  token: str,
435
+ token_issuer: str,
415
436
  errors: list[str] | None,
416
437
  logger: Logger | None) -> None:
417
438
  """
@@ -439,7 +460,7 @@ def __assert_link(iam_server: IamServer,
439
460
  # obtain the internal user identification for 'user_id'
440
461
  if logger:
441
462
  logger.debug(msg="Obtaining internal identification "
442
- f"for user {user_id} in IAM server {iam_server}")
463
+ f"for user '{user_id}' in IAM server '{iam_server}'")
443
464
  url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
444
465
  header_data: dict[str, str] = {
445
466
  "Authorization": f"Bearer {admin_token}",
@@ -455,12 +476,12 @@ def __assert_link(iam_server: IamServer,
455
476
  errors=errors,
456
477
  logger=logger)
457
478
  if users:
458
- # verify whether the 'oidc' protocol is referred to in an
459
- # association between 'user_id' and the internal user identification
479
+ # verify whether the IAM server that issued the token is a federated identity provider
480
+ # in the associations between 'user_id' and the internal user identification
460
481
  internal_id: str = users[0].get("id")
461
482
  if logger:
462
- logger.debug(msg="Obtaining the providers federated with "
463
- f"IAM server '{iam_server}' for internal identification '{internal_id}'")
483
+ logger.debug(msg="Obtaining the providers federated in IAM server "
484
+ f"'{iam_server}', for internal identification '{internal_id}'")
464
485
  url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
465
486
  f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
466
487
  providers: list[dict[str, Any]] = __get_for_data(url=url,
@@ -469,13 +490,9 @@ def __assert_link(iam_server: IamServer,
469
490
  errors=errors,
470
491
  logger=logger)
471
492
  no_link: bool = True
472
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
473
- errors=errors,
474
- logger=logger)
475
- issuer: str = claims["payload"]["iss"] if claims else None
476
- provider_name: str = _iam_server_from_issuer(issuer=issuer,
493
+ provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
477
494
  errors=errors,
478
- logger=logger) if issuer else None
495
+ logger=logger)
479
496
  if provider_name:
480
497
  for provider in providers:
481
498
  if provider.get("identityProvider") == provider_name:
@@ -483,17 +500,17 @@ def __assert_link(iam_server: IamServer,
483
500
  break
484
501
  if no_link:
485
502
  # link the identities
486
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
487
- errors=errors,
488
- logger=logger)
489
- if claims:
490
- token_sub: str = claims["payload"]["sub"]
503
+ token_sub: tuple[str] = token_get_values(token=token,
504
+ keys=("sub",),
505
+ errors=errors,
506
+ logger=logger)
507
+ if token_sub:
491
508
  if logger:
492
509
  logger.debug(msg="Creating an association between identifications "
493
- f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
510
+ f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
494
511
  url += f"/{provider_name}"
495
512
  json_data: dict[str, Any] = {
496
- "userId": token_sub,
513
+ "userId": token_sub[0],
497
514
  "userName": user_id
498
515
  }
499
516
  __post_json(url=url,
pypomes_iam/iam_common.py CHANGED
@@ -3,7 +3,10 @@ import sys
3
3
  from datetime import datetime
4
4
  from enum import StrEnum, auto
5
5
  from logging import Logger
6
- from pypomes_core import TZ_LOCAL, exc_format
6
+ from pypomes_core import (
7
+ APP_PREFIX, TZ_LOCAL, exc_format,
8
+ env_get_str, env_get_int, env_get_enums
9
+ )
7
10
  from pypomes_crypto import crypto_jwk_convert
8
11
  from threading import RLock
9
12
  from typing import Any, Final
@@ -21,12 +24,14 @@ class IamParam(StrEnum):
21
24
  """
22
25
  Parameters for configuring *IAM* servers.
23
26
  """
27
+
24
28
  ADMIN_ID = "admin-id"
25
29
  ADMIN_SECRET = "admin-secret"
26
30
  CLIENT_ID = "client-id"
27
31
  CLIENT_REALM = "client-realm"
28
32
  CLIENT_SECRET = "client-secret"
29
33
  ENDPOINT_CALLBACK = "endpoint-callback"
34
+ ENDPOINT_CALLBACK_EXCHANGE = "endpoint-callback-exchange"
30
35
  ENDPOINT_LOGIN = "endpoint-login"
31
36
  ENDPOINT_LOGOUT = "endpoint_logout"
32
37
  ENDPOINT_TOKEN = "endpoint-token"
@@ -34,8 +39,9 @@ class IamParam(StrEnum):
34
39
  LOGIN_TIMEOUT = "login-timeout"
35
40
  PK_EXPIRATION = "pk-expiration"
36
41
  PK_LIFETIME = "pk-lifetime"
37
- PUBLIC_KEY = "public-key"
38
42
  RECIPIENT_ATTR = "recipient-attr"
43
+ # dynamic attributes
44
+ PUBLIC_KEY = "public-key"
39
45
  URL_BASE = "url-base"
40
46
  USERS = "users"
41
47
 
@@ -54,31 +60,65 @@ class UserParam(StrEnum):
54
60
  REDIRECT_URI = "redirect-uri"
55
61
 
56
62
 
57
- # The configuration parameters for the IAM servers are specified dynamically dynamically with *iam_setup()*
58
- # Specifying configuration parameters with environment variables can be done in two ways:
59
- #
60
- # 1. for a single *IAM* server, specify the data set
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
- # - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
64
- # - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
65
- # - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
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)
72
- # - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
73
- # - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
74
- # - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
75
- # - *<APP_PREFIX>_IAM_URL_BASE* (required)
76
- #
77
- # 2. for multiple *IAM* servers, specify the data set above for each server,
78
- # respectively replacing *IAM* with a name in *IamServer* (currently, *JUSBR* and *KEYCLOAK* are supported).
79
- #
80
- # 3. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
81
- # as they are reserved for internal use
63
+ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
64
+ """
65
+ Obtain the configuration data for select *IAM* servers.
66
+
67
+ The configuration parameters for the IAM servers are specified dynamically with environment variables,
68
+ or dynamically with calls to *iam_setup_server()*. Specifying configuration parameters with environment
69
+ variables can be done by following these steps:
70
+
71
+ 1. Specify *<APP_PREFIX>_IAM_SERVERS* with a list of names among the values found in *IamServer* class
72
+ (currently, *jusbr* and *keycloak* are supported), and the data set below for each server, where
73
+ *<IAM>* stands for the server's name as presented in *IamServer* class:
74
+ - *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
75
+ - *<APP_PREFIX>_<IAM>_ADMIN_PWD* (optional, required if administrative duties are performed)
76
+ - *<APP_PREFIX>_<IAM>_CLIENT_ID* (required)
77
+ - *<APP_PREFIX>_<IAM>_CLIENT_REALM* (required)
78
+ - *<APP_PREFIX>_<IAM>_CLIENT_SECRET* (required)
79
+ - *<APP_PREFIX>_<IAM>_LOGIN_TIMEOUT* (optional, defaults to no timeout)
80
+ - *<APP_PREFIX>_<IAM>_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
81
+ - *<APP_PREFIX>_<IAM>_RECIPIENT_ATTR* (required)
82
+ - *<APP_PREFIX>_<IAM>_URL_BASE* (required)
83
+
84
+ 2. A group of special environment variables identifying endpoints for authentication services may be specified,
85
+ following the same scheme as presented in item *1* above. These are not part of the *IAM* server's setup,
86
+ but are meant to be used by function *iam_setup_endpoints()*, wherein the values in those variables
87
+ would represent default values for its parameters, respectively:
88
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK*
89
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK_EXCHANGE*
90
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_EXCHANGE*
91
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGIN*
92
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGOUT*
93
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_TOKEN*
94
+
95
+ :return: the configuration data for the select *IAM* servers.
96
+ """
97
+ # initialize the return variable
98
+ result: dict[IamServer, dict[IamParam, Any]] = {}
99
+
100
+ servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
101
+ enum_class=IamServer) or []
102
+ for server in servers:
103
+ prefix = server.name
104
+ result[server] = {
105
+ IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
106
+ IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
107
+ IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
108
+ IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
109
+ IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
110
+ IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
111
+ IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME"),
112
+ IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
113
+ IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
114
+ # dynamically set
115
+ IamParam.PK_EXPIRATION: 0,
116
+ IamParam.PUBLIC_KEY: None,
117
+ IamParam.USERS: {}
118
+ }
119
+
120
+ return result
121
+
82
122
 
83
123
  # registry structure:
84
124
  # { <IamServer>:
@@ -91,6 +131,7 @@ class UserParam(StrEnum):
91
131
  # "client-realm": <str,
92
132
  # "client-timeout": <int>,
93
133
  # "recipient-attr": <str>,
134
+ # # dynamic attributes
94
135
  # "public-key": <str>,
95
136
  # "pk-lifetime": <int>,
96
137
  # "pk-expiration": <int>,
@@ -112,10 +153,10 @@ class UserParam(StrEnum):
112
153
  # },
113
154
  # ...
114
155
  # }
115
- _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
156
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
116
157
 
117
158
 
118
- # the lock protecting the data in '_IAM_SERVERS'
159
+ # the lock protecting the data in '_<IAM>_SERVERS'
119
160
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
120
161
  _iam_lock: Final[RLock] = RLock()
121
162