boto3-refresh-session 1.3.11__py3-none-any.whl → 7.0.1__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 boto3-refresh-session might be problematic. Click here for more details.

@@ -0,0 +1,276 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """STS assume-role refreshable session implementation."""
6
+
7
+ __all__ = ["STSRefreshableSession"]
8
+
9
+ from typing import Callable
10
+
11
+ from ..exceptions import BRSConfigurationError, BRSValidationError, BRSWarning
12
+ from ..utils import (
13
+ MFA_SERIAL_PATTERN,
14
+ ROLE_ARN_PATTERN,
15
+ ROLE_SESSION_NAME_PATTERN,
16
+ AssumeRoleParams,
17
+ BaseRefreshableSession,
18
+ Identity,
19
+ STSClientParams,
20
+ TemporaryCredentials,
21
+ refreshable_session,
22
+ )
23
+
24
+
25
+ @refreshable_session
26
+ class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
27
+ """A :class:`boto3.session.Session` object that automatically refreshes
28
+ temporary AWS credentials using an IAM role that is assumed via STS.
29
+
30
+ Parameters
31
+ ----------
32
+ assume_role_kwargs : AssumeRoleParams
33
+ Required keyword arguments for :meth:`STS.Client.assume_role` (i.e.
34
+ boto3 STS client). ``RoleArn`` is required. ``RoleSessionName`` will
35
+ default to 'boto3-refresh-session' if not provided.
36
+
37
+ For MFA authentication, two modalities are supported:
38
+
39
+ 1. **Dynamic tokens (recommended)**: Provide ``SerialNumber`` in
40
+ ``assume_role_kwargs`` and pass ``mfa_token_provider`` callable.
41
+ The provider callable will be invoked on each refresh to obtain
42
+ fresh MFA tokens. Do not include ``TokenCode`` in this case.
43
+
44
+ 2. **Static/injectable tokens**: Provide both ``SerialNumber`` and
45
+ ``TokenCode`` in ``assume_role_kwargs``. You are responsible for
46
+ updating ``assume_role_kwargs["TokenCode"]`` before the token
47
+ expires.
48
+ sts_client_kwargs : STSClientParams, optional
49
+ Optional keyword arguments for the :class:`STS.Client` object. Do not
50
+ provide values for ``service_name`` as they are unnecessary. Default
51
+ is None.
52
+ mfa_token_provider : Callable[[], str], optional
53
+ An optional callable that returns a string representing a fresh MFA
54
+ token code. If provided, this will be called during each credential
55
+ refresh to obtain a new token, which overrides any ``TokenCode`` in
56
+ ``assume_role_kwargs``. When using this parameter, ``SerialNumber``
57
+ must be provided in ``assume_role_kwargs``. Default is None.
58
+ mfa_token_provider_kwargs : dict, optional
59
+ Optional keyword arguments to pass to the ``mfa_token_provider``
60
+ callable. Default is None.
61
+ defer_refresh : bool, optional
62
+ If ``True`` then temporary credentials are not automatically refreshed
63
+ until they are explicitly needed. If ``False`` then temporary
64
+ credentials refresh immediately upon expiration. It is highly
65
+ recommended that you use ``True``. Default is ``True``.
66
+ advisory_timeout : int, optional
67
+ USE THIS ARGUMENT WITH CAUTION!!!
68
+
69
+ Botocore will attempt to refresh credentials early according to
70
+ this value (in seconds), but will continue using the existing
71
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
72
+ mandatory_timeout : int, optional
73
+ USE THIS ARGUMENT WITH CAUTION!!!
74
+
75
+ Botocore requires a successful refresh before continuing. If
76
+ refresh fails in this window (in seconds), API calls may fail.
77
+ Default is 10 minutes (600 seconds).
78
+ cache_clients : bool, optional
79
+ If ``True`` then clients created by this session will be cached and
80
+ reused for subsequent calls to :meth:`client()` with the same
81
+ parameter signatures. Due to the memory overhead of clients, the
82
+ default is ``True`` in order to protect system resources.
83
+
84
+ Other Parameters
85
+ ----------------
86
+ kwargs : dict
87
+ Optional keyword arguments for the :class:`boto3.session.Session`
88
+ object.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ assume_role_kwargs: AssumeRoleParams,
94
+ sts_client_kwargs: STSClientParams | None = None,
95
+ mfa_token_provider: Callable[[], str] | None = None,
96
+ mfa_token_provider_kwargs: dict | None = None,
97
+ **kwargs,
98
+ ):
99
+ # ensuring 'refresh_method' is not set manually
100
+ if "refresh_method" in kwargs:
101
+ BRSWarning.warn(
102
+ "'refresh_method' cannot be set manually. "
103
+ "Reverting to 'sts-assume-role'."
104
+ )
105
+ del kwargs["refresh_method"]
106
+
107
+ # verifying 'RoleArn' is provided in 'assume_role_kwargs'
108
+ if "RoleArn" not in assume_role_kwargs:
109
+ raise BRSConfigurationError(
110
+ "'RoleArn' must be provided in 'assume_role_kwargs'!",
111
+ param="RoleArn",
112
+ )
113
+
114
+ # verifying 'RoleArn' format
115
+ if not ROLE_ARN_PATTERN.match(assume_role_kwargs["RoleArn"]):
116
+ raise BRSValidationError(
117
+ "'RoleArn' in 'assume_role_kwargs' is not a valid AWS "
118
+ "Role ARN!",
119
+ param="RoleArn",
120
+ value=assume_role_kwargs.get("RoleArn"),
121
+ )
122
+
123
+ # setting default 'RoleSessionName' if not provided
124
+ if "RoleSessionName" not in assume_role_kwargs:
125
+ BRSWarning.warn(
126
+ "'RoleSessionName' not provided in "
127
+ "'assume_role_kwargs'! Defaulting to "
128
+ "'boto3-refresh-session'."
129
+ )
130
+ assume_role_kwargs["RoleSessionName"] = "boto3-refresh-session"
131
+
132
+ # verifying 'RoleSessionName' format
133
+ if not ROLE_SESSION_NAME_PATTERN.match(
134
+ assume_role_kwargs["RoleSessionName"]
135
+ ):
136
+ raise BRSValidationError(
137
+ "'RoleSessionName' in 'assume_role_kwargs' is not valid! "
138
+ "It must be 2-64 characters long and can contain only "
139
+ "alphanumeric characters and the following symbols: "
140
+ "'+=,.@-'.",
141
+ param="RoleSessionName",
142
+ value=assume_role_kwargs.get("RoleSessionName"),
143
+ )
144
+
145
+ # store MFA token provider
146
+ try:
147
+ # verifying type of mfa_token_provider
148
+ assert (
149
+ isinstance(mfa_token_provider, Callable)
150
+ or mfa_token_provider is None
151
+ )
152
+ self.mfa_token_provider = mfa_token_provider
153
+ except AssertionError as err:
154
+ raise BRSValidationError(
155
+ "'mfa_token_provider' must be a callable that returns a "
156
+ "string representing an MFA token code!",
157
+ param="mfa_token_provider",
158
+ ) from err
159
+
160
+ # storing mfa_token_provider_kwargs
161
+ self.mfa_token_provider_kwargs = (
162
+ mfa_token_provider_kwargs if mfa_token_provider_kwargs else {}
163
+ )
164
+
165
+ # verifying 'SerialNumber' format if provided
166
+ if "SerialNumber" in assume_role_kwargs:
167
+ if not MFA_SERIAL_PATTERN.match(
168
+ assume_role_kwargs["SerialNumber"]
169
+ ):
170
+ raise BRSValidationError(
171
+ "'SerialNumber' in 'assume_role_kwargs' is not a valid "
172
+ "AWS MFA device ARN!",
173
+ param="SerialNumber",
174
+ value=assume_role_kwargs.get("SerialNumber"),
175
+ )
176
+
177
+ # ensure SerialNumber is set appropriately with mfa_token_provider
178
+ if (
179
+ self.mfa_token_provider
180
+ and "SerialNumber" not in assume_role_kwargs
181
+ ):
182
+ raise BRSConfigurationError(
183
+ "'SerialNumber' must be provided in 'assume_role_kwargs' "
184
+ "when using 'mfa_token_provider'!",
185
+ param="SerialNumber",
186
+ )
187
+
188
+ # ensure SerialNumber and TokenCode are set without mfa_token_provider
189
+ if (
190
+ self.mfa_token_provider is None
191
+ and (
192
+ "SerialNumber" in assume_role_kwargs
193
+ and "TokenCode" not in assume_role_kwargs
194
+ )
195
+ or (
196
+ "SerialNumber" not in assume_role_kwargs
197
+ and "TokenCode" in assume_role_kwargs
198
+ )
199
+ ):
200
+ raise BRSConfigurationError(
201
+ "'SerialNumber' and 'TokenCode' must be provided in "
202
+ "'assume_role_kwargs' when 'mfa_token_provider' is not set!",
203
+ param="SerialNumber/TokenCode",
204
+ )
205
+
206
+ # warn if TokenCode provided with mfa_token_provider
207
+ if self.mfa_token_provider and "TokenCode" in assume_role_kwargs:
208
+ BRSWarning.warn(
209
+ "'TokenCode' provided in 'assume_role_kwargs' will be "
210
+ "ignored and overridden by 'mfa_token_provider' on each "
211
+ "refresh."
212
+ )
213
+
214
+ # initializing assume role kwargs attribute
215
+ self.assume_role_kwargs = assume_role_kwargs
216
+
217
+ # initializing BRSSession
218
+ super().__init__(refresh_method="sts-assume-role", **kwargs)
219
+
220
+ if sts_client_kwargs is not None:
221
+ # overwriting 'service_name' if if appears in sts_client_kwargs
222
+ if "service_name" in sts_client_kwargs:
223
+ BRSWarning.warn(
224
+ "'sts_client_kwargs' cannot contain values for "
225
+ "'service_name'. Reverting to service_name = 'sts'."
226
+ )
227
+ del sts_client_kwargs["service_name"]
228
+ self._sts_client = self.client(
229
+ service_name="sts", **sts_client_kwargs
230
+ )
231
+ else:
232
+ self._sts_client = self.client(service_name="sts")
233
+
234
+ def _get_credentials(self) -> TemporaryCredentials:
235
+ params = dict(self.assume_role_kwargs)
236
+
237
+ # override TokenCode with fresh token from provider if configured
238
+ if self.mfa_token_provider:
239
+ params["TokenCode"] = self.mfa_token_provider(
240
+ **self.mfa_token_provider_kwargs
241
+ )
242
+
243
+ # validating TokenCode format
244
+ if (token_code := params.get("TokenCode")) is not None:
245
+ if (
246
+ not isinstance(token_code, str)
247
+ or len(token_code) != 6
248
+ or not token_code.isdigit()
249
+ ):
250
+ raise BRSValidationError(
251
+ "'TokenCode' must be a 6-digit string per AWS MFA "
252
+ "token specifications!",
253
+ param="TokenCode",
254
+ )
255
+
256
+ temporary_credentials = self._sts_client.assume_role(**params)[
257
+ "Credentials"
258
+ ]
259
+
260
+ return {
261
+ "access_key": temporary_credentials.get("AccessKeyId"),
262
+ "secret_key": temporary_credentials.get("SecretAccessKey"),
263
+ "token": temporary_credentials.get("SessionToken"),
264
+ "expiry_time": temporary_credentials.get("Expiration").isoformat(),
265
+ }
266
+
267
+ def get_identity(self) -> Identity:
268
+ """Returns metadata about the identity assumed.
269
+
270
+ Returns
271
+ -------
272
+ Identity
273
+ Dict containing caller identity according to AWS STS.
274
+ """
275
+
276
+ return self._sts_client.get_caller_identity()
@@ -1,151 +1,86 @@
1
- from __future__ import annotations
2
-
3
- __all__ = ["RefreshableSession"]
4
-
5
- from abc import ABC, abstractmethod
6
- from typing import Any, Callable, ClassVar, Literal, get_args
7
-
8
- from boto3.session import Session
9
- from botocore.credentials import (
10
- DeferredRefreshableCredentials,
11
- RefreshableCredentials,
12
- )
13
-
14
- from .exceptions import BRSError, BRSWarning
15
-
16
- #: Type alias for all currently available credential refresh methods.
17
- Method = Literal["sts", "ecs", "custom"]
18
- RefreshMethod = Literal["sts-assume-role", "ecs-container-metadata", "custom"]
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
19
4
 
5
+ """Public factory for constructing refreshable boto3 sessions."""
20
6
 
21
- class BaseRefreshableSession(ABC, Session):
22
- """Abstract base class for implementing refreshable AWS sessions.
23
-
24
- Provides a common interface and factory registration mechanism
25
- for subclasses that generate temporary credentials using various
26
- AWS authentication methods (e.g., STS).
27
-
28
- Subclasses must implement ``_get_credentials()`` and ``get_identity()``.
29
- They should also register themselves using the ``method=...`` argument
30
- to ``__init_subclass__``.
31
-
32
- Parameters
33
- ----------
34
- registry : dict[str, type[BaseRefreshableSession]]
35
- Class-level registry mapping method names to registered session types.
36
- """
37
-
38
- # adding this and __init_subclass__ to avoid circular imports
39
- # as well as simplify future addition of new methods
40
- registry: ClassVar[dict[Method, type[BaseRefreshableSession]]] = {}
41
-
42
- def __init_subclass__(cls, method: Method):
43
- super().__init_subclass__()
44
-
45
- # guarantees that methods are unique
46
- if method in BaseRefreshableSession.registry:
47
- BRSWarning(
48
- f"Method {repr(method)} is already registered. Overwriting."
49
- )
50
-
51
- BaseRefreshableSession.registry[method] = cls
52
-
53
- def __init__(self, **kwargs):
54
- super().__init__(**kwargs)
55
-
56
- @abstractmethod
57
- def _get_credentials(self) -> dict[str, str]: ...
58
-
59
- @abstractmethod
60
- def get_identity(self) -> dict[str, Any]: ...
61
-
62
- def _refresh_using(
63
- self,
64
- credentials_method: Callable,
65
- defer_refresh: bool,
66
- refresh_method: RefreshMethod,
67
- ):
68
- # determining how exactly to refresh expired temporary credentials
69
- if not defer_refresh:
70
- self._credentials = RefreshableCredentials.create_from_metadata(
71
- metadata=credentials_method(),
72
- refresh_using=credentials_method,
73
- method=refresh_method,
74
- )
75
- else:
76
- self._credentials = DeferredRefreshableCredentials(
77
- refresh_using=credentials_method, method=refresh_method
78
- )
79
-
80
- def refreshable_credentials(self) -> dict[str, str]:
81
- """The current temporary AWS security credentials.
82
-
83
- Returns
84
- -------
85
- dict[str, str]
86
- Temporary AWS security credentials containing:
87
- AWS_ACCESS_KEY_ID : str
88
- AWS access key identifier.
89
- AWS_SECRET_ACCESS_KEY : str
90
- AWS secret access key.
91
- AWS_SESSION_TOKEN : str
92
- AWS session token.
93
- """
7
+ from __future__ import annotations
94
8
 
95
- creds = self.get_credentials().get_frozen_credentials()
96
- return {
97
- "AWS_ACCESS_KEY_ID": creds.access_key,
98
- "AWS_SECRET_ACCESS_KEY": creds.secret_key,
99
- "AWS_SESSION_TOKEN": creds.token,
100
- }
9
+ __all__ = ["RefreshableSession"]
101
10
 
102
- @property
103
- def credentials(self) -> dict[str, str]:
104
- """The current temporary AWS security credentials."""
11
+ from typing import get_args
105
12
 
106
- return self.refreshable_credentials()
13
+ from .exceptions import BRSValidationError
14
+ from .utils import BaseRefreshableSession, Method
107
15
 
108
16
 
109
17
  class RefreshableSession:
110
- """Factory class for constructing refreshable boto3 sessions using various authentication
111
- methods, e.g. STS.
18
+ """Factory class for constructing refreshable boto3 sessions using various
19
+ authentication methods, e.g. STS.
112
20
 
113
- This class provides a unified interface for creating boto3 sessions whose credentials are
114
- automatically refreshed in the background.
21
+ This class provides a unified interface for creating boto3 sessions whose
22
+ credentials are automatically refreshed in the background.
115
23
 
116
- Use ``RefreshableSession(method="...")`` to construct an instance using the desired method.
24
+ Use ``RefreshableSession(method="...")`` to construct an instance using
25
+ the desired method.
117
26
 
118
- For additional information on required parameters, refer to the See Also section below.
27
+ For additional information on required parameters, refer to the See Also
28
+ section below.
119
29
 
120
30
  Parameters
121
31
  ----------
122
32
  method : Method
123
- The authentication and refresh method to use for the session. Must match a registered method name.
124
- Default is "sts".
33
+ The authentication and refresh method to use for the session. Must
34
+ match a registered method name. Default is "sts".
35
+ defer_refresh : bool, optional
36
+ If ``True`` then temporary credentials are not automatically refreshed
37
+ until they are explicitly needed. If ``False`` then temporary
38
+ credentials refresh immediately upon expiration. It is highly
39
+ recommended that you use ``True``. Default is ``True``.
40
+ advisory_timeout : int, optional
41
+ USE THIS ARGUMENT WITH CAUTION!!!
42
+
43
+ Botocore will attempt to refresh credentials early according to
44
+ this value (in seconds), but will continue using the existing
45
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
46
+ mandatory_timeout : int, optional
47
+ USE THIS ARGUMENT WITH CAUTION!!!
48
+
49
+ Botocore requires a successful refresh before continuing. If
50
+ refresh fails in this window (in seconds), API calls may fail.
51
+ Default is 10 minutes (600 seconds).
52
+ cache_clients : bool, optional
53
+ If ``True`` then clients created by this session will be cached and
54
+ reused for subsequent calls to :meth:`client()` with the same
55
+ parameter signatures. Due to the memory overhead of clients, the
56
+ default is ``True`` in order to protect system resources.
125
57
 
126
58
  Other Parameters
127
59
  ----------------
128
60
  **kwargs : dict
129
- Additional keyword arguments forwarded to the constructor of the selected session class.
61
+ Additional keyword arguments forwarded to the constructor of the
62
+ selected session class.
130
63
 
131
64
  See Also
132
65
  --------
133
- boto3_refresh_session.custom.CustomRefreshableSession
134
- boto3_refresh_session.sts.STSRefreshableSession
135
- boto3_refresh_session.ecs.ECSRefreshableSession
66
+ boto3_refresh_session.methods.custom.CustomRefreshableSession
67
+ boto3_refresh_session.methods.iot.x509.IOTX509RefreshableSession
68
+ boto3_refresh_session.methods.sts.STSRefreshableSession
136
69
  """
137
70
 
138
71
  def __new__(
139
72
  cls, method: Method = "sts", **kwargs
140
73
  ) -> BaseRefreshableSession:
141
74
  if method not in (methods := cls.get_available_methods()):
142
- raise BRSError(
143
- f"{repr(method)} is an invalid method parameter. Available methods are "
144
- f"{', '.join(repr(meth) for meth in methods)}."
75
+ raise BRSValidationError(
76
+ f"{method!r} is an invalid method parameter. "
77
+ "Available methods are "
78
+ f"{', '.join(repr(meth) for meth in methods)}.",
79
+ param="method",
80
+ value=method,
145
81
  )
146
82
 
147
- obj = BaseRefreshableSession.registry[method]
148
- return obj(**kwargs)
83
+ return BaseRefreshableSession.registry[method](**kwargs)
149
84
 
150
85
  @classmethod
151
86
  def get_available_methods(cls) -> list[str]:
@@ -154,7 +89,10 @@ class RefreshableSession:
154
89
  Returns
155
90
  -------
156
91
  list[str]
157
- A list of all currently available credential refresh methods, e.g. 'sts'.
92
+ A list of all currently available credential refresh methods,
93
+ e.g. 'sts', 'custom'.
158
94
  """
159
95
 
160
- return list(get_args(Method))
96
+ args = list(get_args(Method))
97
+ args.remove("__sentinel__")
98
+ return args
@@ -0,0 +1,16 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ __all__ = []
6
+
7
+ from . import cache, constants, internal, typing
8
+ from .cache import *
9
+ from .constants import *
10
+ from .internal import *
11
+ from .typing import *
12
+
13
+ __all__.extend(cache.__all__)
14
+ __all__.extend(constants.__all__)
15
+ __all__.extend(internal.__all__)
16
+ __all__.extend(typing.__all__)
@@ -0,0 +1,98 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """Cache primitives for memoizing boto3 client instances.
6
+
7
+ `ClientCache` provides a thread-safe mapping for cached clients and raises
8
+ `BRSCacheError` when lookups or mutations violate the expected cache contract.
9
+ """
10
+
11
+ __all__ = ["ClientCache"]
12
+
13
+ from threading import Lock
14
+ from typing import Hashable, Optional
15
+
16
+ from botocore.client import BaseClient
17
+
18
+ from ..exceptions import BRSCacheExistsError, BRSCacheNotFoundError
19
+
20
+
21
+ class ClientCache:
22
+ """A thread-safe cache for storing boto3 clients which can be used like a
23
+ dictionary."""
24
+
25
+ def __init__(self):
26
+ self._cache: dict[Hashable, BaseClient] = {}
27
+ self._lock = Lock()
28
+
29
+ def __len__(self) -> int:
30
+ with self._lock:
31
+ return len(self._cache)
32
+
33
+ def __contains__(self, hash: Hashable) -> bool:
34
+ with self._lock:
35
+ return hash in self._cache
36
+
37
+ def __iter__(self):
38
+ with self._lock:
39
+ return iter(self._cache.keys())
40
+
41
+ def __getitem__(self, hash: Hashable) -> BaseClient:
42
+ with self._lock:
43
+ try:
44
+ return self._cache[hash]
45
+ except KeyError as err:
46
+ raise BRSCacheNotFoundError(
47
+ "The client you requested has not been cached."
48
+ ) from err
49
+
50
+ def __setitem__(self, hash: Hashable, client: BaseClient) -> None:
51
+ with self._lock:
52
+ if hash in self._cache:
53
+ raise BRSCacheExistsError("Client already exists in cache.")
54
+
55
+ self._cache[hash] = client
56
+
57
+ def __delitem__(self, hash: Hashable) -> None:
58
+ with self._lock:
59
+ if hash not in self._cache:
60
+ raise BRSCacheNotFoundError("Client not found in cache.")
61
+ del self._cache[hash]
62
+
63
+ def keys(self) -> tuple[Hashable, ...]:
64
+ """Returns the keys in the cache."""
65
+
66
+ with self._lock:
67
+ return tuple(self._cache.keys())
68
+
69
+ def values(self) -> tuple[BaseClient, ...]:
70
+ """Returns the clients from the cache."""
71
+
72
+ with self._lock:
73
+ return tuple(self._cache.values())
74
+
75
+ def items(self) -> tuple[tuple[Hashable, BaseClient], ...]:
76
+ """Returns the items in the cache as (hash, BaseClient) tuples."""
77
+
78
+ with self._lock:
79
+ return tuple(self._cache.items())
80
+
81
+ def get(
82
+ self, hash: Hashable, default: Optional[BaseClient] = None
83
+ ) -> Optional[BaseClient]:
84
+ """Gets the client using the given signature, or returns None if no
85
+ default is provided.
86
+ """
87
+
88
+ with self._lock:
89
+ return self._cache.get(hash, default)
90
+
91
+ def pop(self, hash: Hashable) -> BaseClient:
92
+ """Pops and returns the client using the given signature."""
93
+
94
+ with self._lock:
95
+ if (client := self._cache.get(hash)) is None:
96
+ raise BRSCacheNotFoundError("Client not found in cache.")
97
+ del self._cache[hash]
98
+ return client
@@ -0,0 +1,16 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ __all__ = [
6
+ "ROLE_ARN_PATTERN",
7
+ "MFA_SERIAL_PATTERN",
8
+ "ROLE_SESSION_NAME_PATTERN",
9
+ ]
10
+
11
+ from re import compile
12
+
13
+ # AWS ARN validation patterns
14
+ ROLE_ARN_PATTERN = compile(r"^arn:aws[a-z-]*:iam::\d{12}:role/[\w+=,.@-]+$")
15
+ MFA_SERIAL_PATTERN = compile(r"^arn:aws[a-z-]*:iam::\d{12}:mfa/[\w+=,.@-]+$")
16
+ ROLE_SESSION_NAME_PATTERN = compile(r"^[a-zA-Z0-9+=,.@-]{2,64}$")