boto3-refresh-session 2.0.5__py3-none-any.whl → 7.1.3__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.

Files changed (29) hide show
  1. boto3_refresh_session/__init__.py +16 -4
  2. boto3_refresh_session/exceptions.py +115 -3
  3. boto3_refresh_session/methods/__init__.py +14 -0
  4. boto3_refresh_session/methods/custom.py +58 -26
  5. boto3_refresh_session/methods/iot/__init__.py +11 -0
  6. boto3_refresh_session/methods/iot/{core.typed → core.py} +14 -18
  7. boto3_refresh_session/methods/iot/x509.py +614 -0
  8. boto3_refresh_session/methods/sts.py +174 -36
  9. boto3_refresh_session/session.py +48 -32
  10. boto3_refresh_session/utils/__init__.py +18 -0
  11. boto3_refresh_session/utils/cache.py +98 -0
  12. boto3_refresh_session/utils/config/__init__.py +10 -0
  13. boto3_refresh_session/utils/config/config.py +274 -0
  14. boto3_refresh_session/utils/constants.py +41 -0
  15. boto3_refresh_session/utils/internal.py +441 -0
  16. boto3_refresh_session/utils/typing.py +138 -0
  17. {boto3_refresh_session-2.0.5.dist-info → boto3_refresh_session-7.1.3.dist-info}/METADATA +99 -114
  18. boto3_refresh_session-7.1.3.dist-info/RECORD +21 -0
  19. {boto3_refresh_session-2.0.5.dist-info → boto3_refresh_session-7.1.3.dist-info}/WHEEL +1 -1
  20. boto3_refresh_session-7.1.3.dist-info/licenses/LICENSE +373 -0
  21. boto3_refresh_session-7.1.3.dist-info/licenses/NOTICE +21 -0
  22. boto3_refresh_session/methods/ecs.py +0 -109
  23. boto3_refresh_session/methods/iot/__init__.typed +0 -4
  24. boto3_refresh_session/methods/iot/certificate.typed +0 -54
  25. boto3_refresh_session/methods/iot/cognito.typed +0 -16
  26. boto3_refresh_session/utils.py +0 -212
  27. boto3_refresh_session-2.0.5.dist-info/LICENSE +0 -21
  28. boto3_refresh_session-2.0.5.dist-info/NOTICE +0 -12
  29. boto3_refresh_session-2.0.5.dist-info/RECORD +0 -17
@@ -1,75 +1,213 @@
1
- from __future__ import annotations
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."""
2
6
 
3
7
  __all__ = ["STSRefreshableSession"]
4
8
 
5
- from typing import Any
9
+ from typing import Callable
6
10
 
7
- from ..exceptions import BRSWarning
8
- from ..session import BaseRefreshableSession
9
- from ..utils import AssumeRoleParams, STSClientParams, TemporaryCredentials
11
+ from ..exceptions import BRSConfigurationError, BRSValidationError, BRSWarning
12
+ from ..utils import (
13
+ AssumeRoleConfig,
14
+ AssumeRoleParams,
15
+ BaseRefreshableSession,
16
+ Identity,
17
+ STSClientConfig,
18
+ STSClientParams,
19
+ TemporaryCredentials,
20
+ refreshable_session,
21
+ )
10
22
 
11
23
 
24
+ @refreshable_session
12
25
  class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
13
26
  """A :class:`boto3.session.Session` object that automatically refreshes
14
27
  temporary AWS credentials using an IAM role that is assumed via STS.
15
28
 
16
29
  Parameters
17
30
  ----------
18
- assume_role_kwargs : AssumeRoleParams
31
+ assume_role_kwargs : AssumeRoleParams | AssumeRoleConfig
19
32
  Required keyword arguments for :meth:`STS.Client.assume_role` (i.e.
20
- boto3 STS client).
33
+ boto3 STS client). ``RoleArn`` is required. ``RoleSessionName`` will
34
+ default to 'boto3-refresh-session' if not provided.
35
+
36
+ For MFA authentication, two modalities are supported:
37
+
38
+ 1. **Dynamic tokens (recommended)**: Provide ``SerialNumber`` in
39
+ ``assume_role_kwargs`` and pass ``mfa_token_provider`` callable.
40
+ The provider callable will be invoked on each refresh to obtain
41
+ fresh MFA tokens. Do not include ``TokenCode`` in this case.
42
+
43
+ 2. **Static/injectable tokens**: Provide both ``SerialNumber`` and
44
+ ``TokenCode`` in ``assume_role_kwargs``. You are responsible for
45
+ updating ``assume_role_kwargs["TokenCode"]`` before the token
46
+ expires.
47
+ sts_client_kwargs : STSClientParams | STSClientConfig, optional
48
+ Optional keyword arguments for the :class:`STS.Client` object. Do not
49
+ provide values for ``service_name`` as they are unnecessary. Default
50
+ is None.
51
+ mfa_token_provider : Callable[[], str], optional
52
+ An optional callable that returns a string representing a fresh MFA
53
+ token code. If provided, this will be called during each credential
54
+ refresh to obtain a new token, which overrides any ``TokenCode`` in
55
+ ``assume_role_kwargs``. When using this parameter, ``SerialNumber``
56
+ must be provided in ``assume_role_kwargs``. Default is None.
57
+ mfa_token_provider_kwargs : dict, optional
58
+ Optional keyword arguments to pass to the ``mfa_token_provider``
59
+ callable. Default is None.
21
60
  defer_refresh : bool, optional
22
61
  If ``True`` then temporary credentials are not automatically refreshed
23
62
  until they are explicitly needed. If ``False`` then temporary
24
63
  credentials refresh immediately upon expiration. It is highly
25
64
  recommended that you use ``True``. Default is ``True``.
26
- sts_client_kwargs : STSClientParams, optional
27
- Optional keyword arguments for the :class:`STS.Client` object. Do not
28
- provide values for ``service_name`` as they are unnecessary. Default
29
- is None.
65
+ advisory_timeout : int, optional
66
+ USE THIS ARGUMENT WITH CAUTION!!!
67
+
68
+ Botocore will attempt to refresh credentials early according to
69
+ this value (in seconds), but will continue using the existing
70
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
71
+ mandatory_timeout : int, optional
72
+ USE THIS ARGUMENT WITH CAUTION!!!
73
+
74
+ Botocore requires a successful refresh before continuing. If
75
+ refresh fails in this window (in seconds), API calls may fail.
76
+ Default is 10 minutes (600 seconds).
77
+ cache_clients : bool, optional
78
+ If ``True`` then clients created by this session will be cached and
79
+ reused for subsequent calls to :meth:`client()` with the same
80
+ parameter signatures. Due to the memory overhead of clients, the
81
+ default is ``True`` in order to protect system resources.
30
82
 
31
83
  Other Parameters
32
84
  ----------------
33
85
  kwargs : dict
34
86
  Optional keyword arguments for the :class:`boto3.session.Session`
35
87
  object.
88
+
89
+ See Also
90
+ --------
91
+ boto3_refresh_session.utils.config.config.AssumeRoleConfig
92
+ boto3_refresh_session.utils.config.config.STSClientConfig
36
93
  """
37
94
 
38
95
  def __init__(
39
96
  self,
40
- assume_role_kwargs: AssumeRoleParams,
41
- defer_refresh: bool | None = None,
42
- sts_client_kwargs: STSClientParams | None = None,
97
+ assume_role_kwargs: AssumeRoleParams | AssumeRoleConfig,
98
+ sts_client_kwargs: STSClientParams | STSClientConfig | None = None,
99
+ mfa_token_provider: Callable[[], str] | None = None,
100
+ mfa_token_provider_kwargs: dict | None = None,
43
101
  **kwargs,
44
102
  ):
45
- super().__init__(**kwargs)
46
- self.assume_role_kwargs = assume_role_kwargs
47
-
48
- if sts_client_kwargs is not None:
49
- # overwriting 'service_name' if if appears in sts_client_kwargs
50
- if "service_name" in sts_client_kwargs:
51
- BRSWarning(
52
- "'sts_client_kwargs' cannot contain values for "
53
- "'service_name'. Reverting to service_name = 'sts'."
103
+ # initializing asssume_role_kwargs attribute
104
+ match assume_role_kwargs:
105
+ case AssumeRoleConfig():
106
+ self.assume_role_kwargs = assume_role_kwargs
107
+ case _:
108
+ self.assume_role_kwargs = AssumeRoleConfig(
109
+ **assume_role_kwargs
54
110
  )
55
- del sts_client_kwargs["service_name"]
56
- self._sts_client = self.client(
57
- service_name="sts", **sts_client_kwargs
111
+
112
+ # initializing sts_client_kwargs attribute
113
+ match sts_client_kwargs:
114
+ case STSClientConfig():
115
+ self.sts_client_kwargs = sts_client_kwargs
116
+ case None:
117
+ self.sts_client_kwargs = STSClientConfig()
118
+ case _:
119
+ self.sts_client_kwargs = STSClientConfig(**sts_client_kwargs)
120
+
121
+ # ensuring 'refresh_method' is not set manually
122
+ if "refresh_method" in kwargs:
123
+ BRSWarning.warn(
124
+ "'refresh_method' cannot be set manually. "
125
+ "Reverting to 'sts-assume-role'."
58
126
  )
59
- else:
60
- self._sts_client = self.client(service_name="sts")
61
-
62
- # mounting refreshable credentials
63
- self.initialize(
64
- credentials_method=self._get_credentials,
65
- defer_refresh=defer_refresh is not False,
66
- refresh_method="sts-assume-role",
127
+ del kwargs["refresh_method"]
128
+
129
+ # setting 'RoleSessionName' if not provided
130
+ self.assume_role_kwargs.RoleSessionName = self.assume_role_kwargs.get(
131
+ "RoleSessionName", "boto3-refresh-session"
67
132
  )
68
133
 
134
+ # store MFA token provider
135
+ try:
136
+ # verifying type of mfa_token_provider
137
+ assert (
138
+ isinstance(mfa_token_provider, Callable)
139
+ or mfa_token_provider is None
140
+ )
141
+ self.mfa_token_provider = mfa_token_provider
142
+ except AssertionError as err:
143
+ raise BRSValidationError(
144
+ "'mfa_token_provider' must be a callable that returns a "
145
+ "string representing an MFA token code!",
146
+ param="mfa_token_provider",
147
+ ) from err
148
+
149
+ # storing mfa_token_provider_kwargs
150
+ self.mfa_token_provider_kwargs = mfa_token_provider_kwargs or {}
151
+
152
+ # ensure SerialNumber is set appropriately with mfa_token_provider
153
+ if (
154
+ self.mfa_token_provider
155
+ and self.assume_role_kwargs.SerialNumber is None
156
+ ):
157
+ raise BRSConfigurationError(
158
+ "'SerialNumber' must be provided in 'assume_role_kwargs' "
159
+ "when using 'mfa_token_provider'!",
160
+ param="SerialNumber",
161
+ )
162
+
163
+ # ensure SerialNumber and TokenCode are set in the absence of
164
+ # mfa_token_provider
165
+ if (
166
+ self.mfa_token_provider is None
167
+ and (
168
+ self.assume_role_kwargs.SerialNumber is not None
169
+ and self.assume_role_kwargs.TokenCode is None
170
+ )
171
+ or (
172
+ self.assume_role_kwargs.SerialNumber is None
173
+ and self.assume_role_kwargs.TokenCode is not None
174
+ )
175
+ ):
176
+ raise BRSConfigurationError(
177
+ "'SerialNumber' and 'TokenCode' must be provided in "
178
+ "'assume_role_kwargs' when 'mfa_token_provider' is not set "
179
+ "and 'SerialNumber' or 'TokenCode' is missing!",
180
+ param="SerialNumber/TokenCode",
181
+ )
182
+
183
+ # warn if TokenCode provided with mfa_token_provider
184
+ if (
185
+ self.mfa_token_provider
186
+ and self.assume_role_kwargs.TokenCode is not None
187
+ ):
188
+ BRSWarning.warn(
189
+ "'TokenCode' provided in 'assume_role_kwargs' will be "
190
+ "ignored and overridden by 'mfa_token_provider' on each "
191
+ "refresh."
192
+ )
193
+
194
+ # initializing BRSSession
195
+ super().__init__(refresh_method="sts-assume-role", **kwargs)
196
+
197
+ # initializing STS client attribute
198
+ self._sts_client = self.client(**self.sts_client_kwargs)
199
+
69
200
  def _get_credentials(self) -> TemporaryCredentials:
201
+ # override TokenCode with fresh token from provider if configured
202
+ if self.mfa_token_provider is not None:
203
+ self.assume_role_kwargs.TokenCode = self.mfa_token_provider(
204
+ **self.mfa_token_provider_kwargs
205
+ )
206
+
70
207
  temporary_credentials = self._sts_client.assume_role(
71
208
  **self.assume_role_kwargs
72
209
  )["Credentials"]
210
+
73
211
  return {
74
212
  "access_key": temporary_credentials.get("AccessKeyId"),
75
213
  "secret_key": temporary_credentials.get("SecretAccessKey"),
@@ -77,12 +215,12 @@ class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
77
215
  "expiry_time": temporary_credentials.get("Expiration").isoformat(),
78
216
  }
79
217
 
80
- def get_identity(self) -> dict[str, Any]:
218
+ def get_identity(self) -> Identity:
81
219
  """Returns metadata about the identity assumed.
82
220
 
83
221
  Returns
84
222
  -------
85
- dict[str, Any]
223
+ Identity
86
224
  Dict containing caller identity according to AWS STS.
87
225
  """
88
226
 
@@ -1,37 +1,17 @@
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
+ """Public factory for constructing refreshable boto3 sessions."""
6
+
1
7
  from __future__ import annotations
2
8
 
3
9
  __all__ = ["RefreshableSession"]
4
10
 
5
11
  from typing import get_args
6
12
 
7
- from .exceptions import BRSError
8
- from .utils import BRSSession, CredentialProvider, Method, Registry
9
-
10
-
11
- class BaseRefreshableSession(
12
- Registry[Method],
13
- CredentialProvider,
14
- BRSSession,
15
- registry_key="__sentinel__",
16
- ):
17
- """Abstract base class for implementing refreshable AWS sessions.
18
-
19
- Provides a common interface and factory registration mechanism
20
- for subclasses that generate temporary credentials using various
21
- AWS authentication methods (e.g., STS).
22
-
23
- Subclasses must implement ``_get_credentials()`` and ``get_identity()``.
24
- They should also register themselves using the ``method=...`` argument
25
- to ``__init_subclass__``.
26
-
27
- Parameters
28
- ----------
29
- registry : dict[str, type[BaseRefreshableSession]]
30
- Class-level registry mapping method names to registered session types.
31
- """
32
-
33
- def __init__(self, **kwargs):
34
- super().__init__(**kwargs)
13
+ from .exceptions import BRSValidationError
14
+ from .utils import BaseRefreshableSession, Method
35
15
 
36
16
 
37
17
  class RefreshableSession:
@@ -52,6 +32,28 @@ class RefreshableSession:
52
32
  method : Method
53
33
  The authentication and refresh method to use for the session. Must
54
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.
55
57
 
56
58
  Other Parameters
57
59
  ----------------
@@ -62,18 +64,32 @@ class RefreshableSession:
62
64
  See Also
63
65
  --------
64
66
  boto3_refresh_session.methods.custom.CustomRefreshableSession
67
+ boto3_refresh_session.methods.iot.x509.IOTX509RefreshableSession
65
68
  boto3_refresh_session.methods.sts.STSRefreshableSession
66
- boto3_refresh_session.methods.ecs.ECSRefreshableSession
69
+
70
+ Examples
71
+ --------
72
+
73
+ Basic initialization using STS AssumeRole (i.e. ``method="sts"``):
74
+
75
+ >>> from boto3_refresh_session import AssumeRoleConfig, RefreshableSession
76
+ >>> session = RefreshableSession(
77
+ ... AssumeRoleConfig(RoleArn="<your-role-arn>"),
78
+ ... region_name="us-east-1"
79
+ ... )
80
+ >>> s3 = session.client("s3")
67
81
  """
68
82
 
69
83
  def __new__(
70
84
  cls, method: Method = "sts", **kwargs
71
85
  ) -> BaseRefreshableSession:
72
86
  if method not in (methods := cls.get_available_methods()):
73
- raise BRSError(
87
+ raise BRSValidationError(
74
88
  f"{method!r} is an invalid method parameter. "
75
89
  "Available methods are "
76
- f"{', '.join(repr(meth) for meth in methods)}."
90
+ f"{', '.join(repr(meth) for meth in methods)}.",
91
+ param="method",
92
+ value=method,
77
93
  )
78
94
 
79
95
  return BaseRefreshableSession.registry[method](**kwargs)
@@ -86,7 +102,7 @@ class RefreshableSession:
86
102
  -------
87
103
  list[str]
88
104
  A list of all currently available credential refresh methods,
89
- e.g. 'sts', 'ecs', 'custom'.
105
+ e.g. 'sts', 'custom'.
90
106
  """
91
107
 
92
108
  args = list(get_args(Method))
@@ -0,0 +1,18 @@
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, config, constants, internal, typing
8
+ from .cache import *
9
+ from .config import *
10
+ from .constants import *
11
+ from .internal import *
12
+ from .typing import *
13
+
14
+ __all__.extend(cache.__all__)
15
+ __all__.extend(config.__all__)
16
+ __all__.extend(constants.__all__)
17
+ __all__.extend(internal.__all__)
18
+ __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,10 @@
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 config
8
+ from .config import *
9
+
10
+ __all__.extend(config.__all__)