boto3-refresh-session 1.0.4__py3-none-any.whl → 6.2.5__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.
@@ -0,0 +1,230 @@
1
+ """STS assume-role refreshable session implementation."""
2
+
3
+ __all__ = ["STSRefreshableSession"]
4
+
5
+ from typing import Callable
6
+
7
+ from ..exceptions import BRSError, BRSWarning
8
+ from ..utils import (
9
+ AssumeRoleParams,
10
+ BaseRefreshableSession,
11
+ Identity,
12
+ STSClientParams,
13
+ TemporaryCredentials,
14
+ refreshable_session,
15
+ )
16
+
17
+
18
+ @refreshable_session
19
+ class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
20
+ """A :class:`boto3.session.Session` object that automatically refreshes
21
+ temporary AWS credentials using an IAM role that is assumed via STS.
22
+
23
+ Parameters
24
+ ----------
25
+ assume_role_kwargs : AssumeRoleParams
26
+ Required keyword arguments for :meth:`STS.Client.assume_role` (i.e.
27
+ boto3 STS client). ``RoleArn`` is required. ``RoleSessionName`` will
28
+ default to 'boto3-refresh-session' if not provided.
29
+
30
+ For MFA authentication, two modalities are supported:
31
+
32
+ 1. **Dynamic tokens (recommended)**: Provide ``SerialNumber`` in
33
+ ``assume_role_kwargs`` and pass ``mfa_token_provider`` callable.
34
+ The provider callable will be invoked on each refresh to obtain
35
+ fresh MFA tokens. Do not include ``TokenCode`` in this case.
36
+
37
+ 2. **Static/injectable tokens**: Provide both ``SerialNumber`` and
38
+ ``TokenCode`` in ``assume_role_kwargs``. You are responsible for
39
+ updating ``assume_role_kwargs["TokenCode"]`` before the token
40
+ expires.
41
+ sts_client_kwargs : STSClientParams, optional
42
+ Optional keyword arguments for the :class:`STS.Client` object. Do not
43
+ provide values for ``service_name`` as they are unnecessary. Default
44
+ is None.
45
+ mfa_token_provider : Callable[[], str], optional
46
+ An optional callable that returns a string representing a fresh MFA
47
+ token code. If provided, this will be called during each credential
48
+ refresh to obtain a new token, which overrides any ``TokenCode`` in
49
+ ``assume_role_kwargs``. When using this parameter, ``SerialNumber``
50
+ must be provided in ``assume_role_kwargs``. Default is None.
51
+ mfa_token_provider_kwargs : dict, optional
52
+ Optional keyword arguments to pass to the ``mfa_token_provider``
53
+ callable. Default is None.
54
+ defer_refresh : bool, optional
55
+ If ``True`` then temporary credentials are not automatically refreshed
56
+ until they are explicitly needed. If ``False`` then temporary
57
+ credentials refresh immediately upon expiration. It is highly
58
+ recommended that you use ``True``. Default is ``True``.
59
+ advisory_timeout : int, optional
60
+ USE THIS ARGUMENT WITH CAUTION!!!
61
+
62
+ Botocore will attempt to refresh credentials early according to
63
+ this value (in seconds), but will continue using the existing
64
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
65
+ mandatory_timeout : int, optional
66
+ USE THIS ARGUMENT WITH CAUTION!!!
67
+
68
+ Botocore requires a successful refresh before continuing. If
69
+ refresh fails in this window (in seconds), API calls may fail.
70
+ Default is 10 minutes (600 seconds).
71
+ cache_clients : bool, optional
72
+ If ``True`` then clients created by this session will be cached and
73
+ reused for subsequent calls to :meth:`client()` with the same
74
+ parameter signatures. Due to the memory overhead of clients, the
75
+ default is ``True`` in order to protect system resources.
76
+
77
+ Other Parameters
78
+ ----------------
79
+ kwargs : dict
80
+ Optional keyword arguments for the :class:`boto3.session.Session`
81
+ object.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ assume_role_kwargs: AssumeRoleParams,
87
+ sts_client_kwargs: STSClientParams | None = None,
88
+ mfa_token_provider: Callable[[], str] | None = None,
89
+ mfa_token_provider_kwargs: dict | None = None,
90
+ **kwargs,
91
+ ):
92
+ # ensuring 'refresh_method' is not set manually
93
+ if "refresh_method" in kwargs:
94
+ BRSWarning.warn(
95
+ "'refresh_method' cannot be set manually. "
96
+ "Reverting to 'sts-assume-role'."
97
+ )
98
+ del kwargs["refresh_method"]
99
+
100
+ # verifying 'RoleArn' is provided in 'assume_role_kwargs'
101
+ if "RoleArn" not in assume_role_kwargs:
102
+ raise BRSError(
103
+ "'RoleArn' must be provided in 'assume_role_kwargs'!"
104
+ )
105
+
106
+ # setting default 'RoleSessionName' if not provided
107
+ if "RoleSessionName" not in assume_role_kwargs:
108
+ BRSWarning.warn(
109
+ "'RoleSessionName' not provided in "
110
+ "'assume_role_kwargs'! Defaulting to "
111
+ "'boto3-refresh-session'."
112
+ )
113
+ assume_role_kwargs["RoleSessionName"] = "boto3-refresh-session"
114
+
115
+ # store MFA token provider
116
+ try:
117
+ # verifying type of mfa_token_provider
118
+ assert (
119
+ isinstance(mfa_token_provider, Callable)
120
+ or mfa_token_provider is None
121
+ )
122
+ self.mfa_token_provider = mfa_token_provider
123
+ except AssertionError as err:
124
+ raise BRSError(
125
+ "'mfa_token_provider' must be a callable that returns a "
126
+ "string representing an MFA token code!"
127
+ ) from err
128
+
129
+ # storing mfa_token_provider_kwargs
130
+ self.mfa_token_provider_kwargs = (
131
+ mfa_token_provider_kwargs if mfa_token_provider_kwargs else {}
132
+ )
133
+
134
+ # ensure SerialNumber is set appropriately with mfa_token_provider
135
+ if (
136
+ self.mfa_token_provider
137
+ and "SerialNumber" not in assume_role_kwargs
138
+ ):
139
+ raise BRSError(
140
+ "'SerialNumber' must be provided in 'assume_role_kwargs' "
141
+ "when using 'mfa_token_provider'!"
142
+ )
143
+
144
+ # ensure SerialNumber and TokenCode are set without mfa_token_provider
145
+ if (
146
+ self.mfa_token_provider is None
147
+ and (
148
+ "SerialNumber" in assume_role_kwargs
149
+ and "TokenCode" not in assume_role_kwargs
150
+ )
151
+ or (
152
+ "SerialNumber" not in assume_role_kwargs
153
+ and "TokenCode" in assume_role_kwargs
154
+ )
155
+ ):
156
+ raise BRSError(
157
+ "'SerialNumber' and 'TokenCode' must be provided in "
158
+ "'assume_role_kwargs' when 'mfa_token_provider' is not set!"
159
+ )
160
+
161
+ # warn if TokenCode provided with mfa_token_provider
162
+ if self.mfa_token_provider and "TokenCode" in assume_role_kwargs:
163
+ BRSWarning.warn(
164
+ "'TokenCode' provided in 'assume_role_kwargs' will be "
165
+ "ignored and overridden by 'mfa_token_provider' on each "
166
+ "refresh."
167
+ )
168
+
169
+ # initializing assume role kwargs attribute
170
+ self.assume_role_kwargs = assume_role_kwargs
171
+
172
+ # initializing BRSSession
173
+ super().__init__(refresh_method="sts-assume-role", **kwargs)
174
+
175
+ if sts_client_kwargs is not None:
176
+ # overwriting 'service_name' if if appears in sts_client_kwargs
177
+ if "service_name" in sts_client_kwargs:
178
+ BRSWarning.warn(
179
+ "'sts_client_kwargs' cannot contain values for "
180
+ "'service_name'. Reverting to service_name = 'sts'."
181
+ )
182
+ del sts_client_kwargs["service_name"]
183
+ self._sts_client = self.client(
184
+ service_name="sts", **sts_client_kwargs
185
+ )
186
+ else:
187
+ self._sts_client = self.client(service_name="sts")
188
+
189
+ def _get_credentials(self) -> TemporaryCredentials:
190
+ params = dict(self.assume_role_kwargs)
191
+
192
+ # override TokenCode with fresh token from provider if configured
193
+ if self.mfa_token_provider:
194
+ params["TokenCode"] = self.mfa_token_provider(
195
+ **self.mfa_token_provider_kwargs
196
+ )
197
+
198
+ # validating TokenCode format
199
+ if (token_code := params.get("TokenCode")) is not None:
200
+ if (
201
+ not isinstance(token_code, str)
202
+ or len(token_code) != 6
203
+ or not token_code.isdigit()
204
+ ):
205
+ raise BRSError(
206
+ "'TokenCode' must be a 6-digit string per AWS MFA "
207
+ "token specifications!"
208
+ )
209
+
210
+ temporary_credentials = self._sts_client.assume_role(**params)[
211
+ "Credentials"
212
+ ]
213
+
214
+ return {
215
+ "access_key": temporary_credentials.get("AccessKeyId"),
216
+ "secret_key": temporary_credentials.get("SecretAccessKey"),
217
+ "token": temporary_credentials.get("SessionToken"),
218
+ "expiry_time": temporary_credentials.get("Expiration").isoformat(),
219
+ }
220
+
221
+ def get_identity(self) -> Identity:
222
+ """Returns metadata about the identity assumed.
223
+
224
+ Returns
225
+ -------
226
+ Identity
227
+ Dict containing caller identity according to AWS STS.
228
+ """
229
+
230
+ return self._sts_client.get_caller_identity()
@@ -1,148 +1,92 @@
1
+ """Public factory for constructing refreshable boto3 sessions."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
- __doc__ = """
4
- A :class:`boto3.session.Session` object that automatically refreshes temporary
5
- credentials.
6
- """
7
5
  __all__ = ["RefreshableSession"]
8
6
 
9
- from typing import Any, Dict
7
+ from typing import get_args
8
+
9
+ from .exceptions import BRSError
10
+ from .utils import BaseRefreshableSession, Method
11
+
12
+
13
+ class RefreshableSession:
14
+ """Factory class for constructing refreshable boto3 sessions using various
15
+ authentication methods, e.g. STS.
10
16
 
11
- from boto3 import client
12
- from boto3.session import Session
13
- from botocore.credentials import (
14
- DeferredRefreshableCredentials,
15
- RefreshableCredentials,
16
- )
17
+ This class provides a unified interface for creating boto3 sessions whose
18
+ credentials are automatically refreshed in the background.
17
19
 
20
+ Use ``RefreshableSession(method="...")`` to construct an instance using
21
+ the desired method.
18
22
 
19
- class RefreshableSession(Session):
20
- """Returns a :class:`boto3.session.Session` object with temporary credentials
21
- that refresh automatically.
23
+ For additional information on required parameters, refer to the See Also
24
+ section below.
22
25
 
23
26
  Parameters
24
27
  ----------
25
- assume_role_kwargs : dict
26
- Required keyword arguments for the :meth:`STS.Client.assume_role` method.
28
+ method : Method
29
+ The authentication and refresh method to use for the session. Must
30
+ match a registered method name. Default is "sts".
27
31
  defer_refresh : bool, optional
28
- If ``True`` then temporary credentials are not automatically refreshed until
29
- they are explicitly needed. If ``False`` then temporary credentials refresh
30
- immediately upon expiration. It is highly recommended that you use ``True``.
31
- Default is ``True``.
32
- sts_client_kwargs : dict, optional
33
- Optional keyword arguments for the :class:`STS.Client` object. Default is
34
- None.
32
+ If ``True`` then temporary credentials are not automatically refreshed
33
+ until they are explicitly needed. If ``False`` then temporary
34
+ credentials refresh immediately upon expiration. It is highly
35
+ recommended that you use ``True``. Default is ``True``.
36
+ advisory_timeout : int, optional
37
+ USE THIS ARGUMENT WITH CAUTION!!!
38
+
39
+ Botocore will attempt to refresh credentials early according to
40
+ this value (in seconds), but will continue using the existing
41
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
42
+ mandatory_timeout : int, optional
43
+ USE THIS ARGUMENT WITH CAUTION!!!
44
+
45
+ Botocore requires a successful refresh before continuing. If
46
+ refresh fails in this window (in seconds), API calls may fail.
47
+ Default is 10 minutes (600 seconds).
48
+ cache_clients : bool, optional
49
+ If ``True`` then clients created by this session will be cached and
50
+ reused for subsequent calls to :meth:`client()` with the same
51
+ parameter signatures. Due to the memory overhead of clients, the
52
+ default is ``True`` in order to protect system resources.
35
53
 
36
54
  Other Parameters
37
55
  ----------------
38
- kwargs : dict
39
- Optional keyword arguments for the :class:`boto3.session.Session` object.
56
+ **kwargs : dict
57
+ Additional keyword arguments forwarded to the constructor of the
58
+ selected session class.
40
59
 
41
- Notes
42
- -----
43
- Check the :ref:`authorization documentation <authorization>` for additional
44
- information concerning how to authorize access to AWS.
45
-
46
- Check the `AWS documentation <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html>`_
47
- for additional information concerning temporary security credentials in IAM.
48
-
49
- Examples
60
+ See Also
50
61
  --------
51
- In order to use this object, you are required to configure parameters for the
52
- :meth:`STS.Client.assume_role` method.
53
-
54
- >>> assume_role_kwargs = {
55
- >>> 'RoleArn': '<your-role-arn>',
56
- >>> 'RoleSessionName': '<your-role-session-name>',
57
- >>> 'DurationSeconds': '<your-selection>',
58
- >>> ...
59
- >>> }
60
-
61
- You may also want to provide optional parameters for the :class:`STS.Client` object.
62
-
63
- >>> sts_client_kwargs = {
64
- >>> ...
65
- >>> }
66
-
67
- You may also provide optional parameters for the :class:`boto3.session.Session` object
68
- when initializing the ``RefreshableSession`` object. Below, we use the ``region_name``
69
- parameter for illustrative purposes.
70
-
71
- >>> session = boto3_refresh_session.RefreshableSession(
72
- >>> assume_role_kwargs=assume_role_kwargs,
73
- >>> sts_client_kwargs=sts_client_kwargs,
74
- >>> region_name='us-east-1',
75
- >>> )
76
-
77
- Using the ``session`` variable that you just created, you can now use all of the methods
78
- available from the :class:`boto3.session.Session` object. In the below example, we
79
- initialize an S3 client and list all available buckets.
80
-
81
- >>> s3 = session.client(service_name='s3')
82
- >>> buckets = s3.list_buckets()
83
-
84
- There are two ways of refreshing temporary credentials automatically with the
85
- ``RefreshableSession`` object: refresh credentials the moment they expire, or wait until
86
- temporary credentials are explicitly needed. The latter is the default. The former must
87
- be configured using the ``defer_refresh`` parameter, as shown below.
88
-
89
- >>> session = boto3_refresh_session.RefreshableSession(
90
- >>> defer_refresh=False,
91
- >>> assume_role_kwargs=assume_role_kwargs,
92
- >>> sts_client_kwargs=sts_client_kwargs,
93
- >>> region_name='us-east-1',
94
- >>> )
62
+ boto3_refresh_session.methods.custom.CustomRefreshableSession
63
+ boto3_refresh_session.methods.iot.x509.IOTX509RefreshableSession
64
+ boto3_refresh_session.methods.sts.STSRefreshableSession
95
65
  """
96
66
 
97
- def __init__(
98
- self,
99
- assume_role_kwargs: Dict[Any],
100
- defer_refresh: bool = True,
101
- sts_client_kwargs: Dict[Any] = None,
102
- **kwargs,
103
- ):
104
- # inheriting from boto3.session.Session
105
- super().__init__(**kwargs)
106
-
107
- # initializing custom parameters that are necessary outside of __init__
108
- self.assume_role_kwargs = assume_role_kwargs
109
-
110
- # initializing the STS client
111
- if sts_client_kwargs is not None:
112
- self._sts_client = client(service_name="sts", **sts_client_kwargs)
113
- else:
114
- self._sts_client = client(service_name="sts")
115
-
116
- # determining how exactly to refresh expired temporary credentials
117
- if not defer_refresh:
118
- self._session._credentials = (
119
- RefreshableCredentials.create_from_metadata(
120
- metadata=self._get_credentials(),
121
- refresh_using=self._get_credentials,
122
- method="sts-assume-role",
123
- )
124
- )
125
- else:
126
- self._session._credentials = DeferredRefreshableCredentials(
127
- refresh_using=self._get_credentials, method="sts-assume-role"
67
+ def __new__(
68
+ cls, method: Method = "sts", **kwargs
69
+ ) -> BaseRefreshableSession:
70
+ if method not in (methods := cls.get_available_methods()):
71
+ raise BRSError(
72
+ f"{method!r} is an invalid method parameter. "
73
+ "Available methods are "
74
+ f"{', '.join(repr(meth) for meth in methods)}."
128
75
  )
129
76
 
130
- def _get_credentials(self) -> Dict[Any]:
131
- """Returns temporary credentials via AWS STS.
77
+ return BaseRefreshableSession.registry[method](**kwargs)
78
+
79
+ @classmethod
80
+ def get_available_methods(cls) -> list[str]:
81
+ """Lists all currently available credential refresh methods.
132
82
 
133
83
  Returns
134
84
  -------
135
- dict
136
- AWS temporary credentials.
85
+ list[str]
86
+ A list of all currently available credential refresh methods,
87
+ e.g. 'sts', 'custom'.
137
88
  """
138
89
 
139
- # fetching temporary credentials
140
- temporary_credentials = self._sts_client.assume_role(
141
- **self.assume_role_kwargs
142
- )["Credentials"]
143
- return {
144
- "access_key": temporary_credentials.get("AccessKeyId"),
145
- "secret_key": temporary_credentials.get("SecretAccessKey"),
146
- "token": temporary_credentials.get("SessionToken"),
147
- "expiry_time": temporary_credentials.get("Expiration").isoformat(),
148
- }
90
+ args = list(get_args(Method))
91
+ args.remove("__sentinel__")
92
+ return args
@@ -0,0 +1,10 @@
1
+ __all__ = []
2
+
3
+ from . import cache, internal, typing
4
+ from .cache import *
5
+ from .internal import *
6
+ from .typing import *
7
+
8
+ __all__.extend(cache.__all__)
9
+ __all__.extend(internal.__all__)
10
+ __all__.extend(typing.__all__)
@@ -0,0 +1,94 @@
1
+ """Cache primitives for memoizing boto3 client instances.
2
+
3
+ `ClientCache` provides a thread-safe mapping for cached clients and raises
4
+ `BRSError` when lookups or mutations violate the expected cache contract.
5
+ """
6
+
7
+ __all__ = ["ClientCache"]
8
+
9
+ from threading import Lock
10
+ from typing import Hashable, Optional
11
+
12
+ from botocore.client import BaseClient
13
+
14
+ from ..exceptions import BRSError
15
+
16
+
17
+ class ClientCache:
18
+ """A thread-safe cache for storing boto3 clients which can be used like a
19
+ dictionary."""
20
+
21
+ def __init__(self):
22
+ self._cache: dict[Hashable, BaseClient] = {}
23
+ self._lock = Lock()
24
+
25
+ def __len__(self) -> int:
26
+ with self._lock:
27
+ return len(self._cache)
28
+
29
+ def __contains__(self, hash: Hashable) -> bool:
30
+ with self._lock:
31
+ return hash in self._cache
32
+
33
+ def __iter__(self):
34
+ with self._lock:
35
+ return iter(self._cache.keys())
36
+
37
+ def __getitem__(self, hash: Hashable) -> BaseClient:
38
+ with self._lock:
39
+ try:
40
+ return self._cache[hash]
41
+ except KeyError as err:
42
+ raise BRSError(
43
+ "The client you requested has not been cached."
44
+ ) from err
45
+
46
+ def __setitem__(self, hash: Hashable, client: BaseClient) -> None:
47
+ with self._lock:
48
+ if hash in self._cache:
49
+ raise BRSError("Client already exists in cache.")
50
+
51
+ self._cache[hash] = client
52
+
53
+ def __delitem__(self, hash: Hashable) -> None:
54
+ with self._lock:
55
+ if hash not in self._cache:
56
+ raise BRSError("Client not found in cache.")
57
+ del self._cache[hash]
58
+
59
+ def keys(self) -> tuple[Hashable, ...]:
60
+ """Returns the keys in the cache."""
61
+
62
+ with self._lock:
63
+ return tuple(self._cache.keys())
64
+
65
+ def values(self) -> tuple[BaseClient, ...]:
66
+ """Returns the clients from the cache."""
67
+
68
+ with self._lock:
69
+ return tuple(self._cache.values())
70
+
71
+ def items(self) -> tuple[tuple[Hashable, BaseClient], ...]:
72
+ """Returns the items in the cache as (hash, BaseClient) tuples."""
73
+
74
+ with self._lock:
75
+ return tuple(self._cache.items())
76
+
77
+ def get(
78
+ self, hash: Hashable, default: Optional[BaseClient] = None
79
+ ) -> Optional[BaseClient]:
80
+ """Gets the client using the given signature, or returns None if no
81
+ default is provided.
82
+ """
83
+
84
+ with self._lock:
85
+ return self._cache.get(hash, default)
86
+
87
+ def pop(self, hash: Hashable) -> BaseClient:
88
+ """Pops and returns the client using the given signature."""
89
+
90
+ with self._lock:
91
+ if (client := self._cache.get(hash)) is None:
92
+ raise BRSError("Client not found in cache.")
93
+ del self._cache[hash]
94
+ return client