boto3-refresh-session 4.0.0__py3-none-any.whl → 5.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.
@@ -3,12 +3,13 @@ __all__ = []
3
3
  from . import exceptions, session
4
4
  from .exceptions import *
5
5
  from .methods.custom import *
6
+ from .methods.iot import *
6
7
  from .methods.sts import *
7
8
  from .session import *
8
9
 
9
10
  __all__.extend(session.__all__)
10
11
  __all__.extend(exceptions.__all__)
11
- __version__ = "4.0.0"
12
+ __version__ = "5.0.1"
12
13
  __title__ = "boto3-refresh-session"
13
14
  __author__ = "Mike Letts"
14
15
  __maintainer__ = "Mike Letts"
@@ -1,5 +1,7 @@
1
1
  __all__ = ["BRSError", "BRSWarning"]
2
2
 
3
+ import warnings
4
+
3
5
 
4
6
  class BRSError(Exception):
5
7
  """The base exception for boto3-refresh-session.
@@ -39,3 +41,9 @@ class BRSWarning(UserWarning):
39
41
 
40
42
  def __repr__(self) -> str:
41
43
  return f"{self.__class__.__name__}({self.message!r})"
44
+
45
+ @classmethod
46
+ def warn(cls, message: str, *, stacklevel: int = 2):
47
+ """Emits a BRSWarning with a consistent stacklevel."""
48
+
49
+ warnings.warn(cls(message), stacklevel=stacklevel)
@@ -1,10 +1,10 @@
1
1
  __all__ = []
2
2
 
3
- # TODO: import iot submodules when finished
4
- from . import custom, sts
5
- from .custom import CustomRefreshableSession
6
- from .sts import STSRefreshableSession
3
+ from . import custom, iot, sts
4
+ from .custom import *
5
+ from .iot import *
6
+ from .sts import *
7
7
 
8
- # TODO: add iot submodules to __all__ when finished
9
8
  __all__.extend(custom.__all__)
9
+ __all__.extend(iot.__all__)
10
10
  __all__.extend(sts.__all__)
@@ -71,7 +71,7 @@ class CustomRefreshableSession(BaseRefreshableSession, registry_key="custom"):
71
71
  **kwargs,
72
72
  ):
73
73
  if "refresh_method" in kwargs:
74
- BRSWarning(
74
+ BRSWarning.warn(
75
75
  "'refresh_method' cannot be set manually. "
76
76
  "Reverting to 'custom'."
77
77
  )
@@ -0,0 +1,7 @@
1
+ __all__ = []
2
+
3
+ from . import core
4
+ from .core import IoTRefreshableSession
5
+ from .x509 import IOTX509RefreshableSession
6
+
7
+ __all__.extend(core.__all__)
@@ -6,28 +6,16 @@ from typing import get_args
6
6
 
7
7
  from ...exceptions import BRSError
8
8
  from ...utils import (
9
- BaseRefreshableSession,
10
- BRSSession,
11
- CredentialProvider,
12
- IoTAuthenticationMethod,
13
- Registry,
9
+ BaseIoTRefreshableSession,
10
+ BaseRefreshableSession,
11
+ IoTAuthenticationMethod,
14
12
  )
15
13
 
16
14
 
17
- class BaseIoTRefreshableSession(
18
- Registry[IoTAuthenticationMethod],
19
- CredentialProvider,
20
- BRSSession,
21
- registry_key="__iot_sentinel__",
22
- ):
23
- def __init__(self, **kwargs):
24
- super().__init__(**kwargs)
25
-
26
-
27
15
  class IoTRefreshableSession(BaseRefreshableSession, registry_key="iot"):
28
16
  def __new__(
29
17
  cls,
30
- authentication_method: IoTAuthenticationMethod = "certificate",
18
+ authentication_method: IoTAuthenticationMethod = "x509",
31
19
  **kwargs,
32
20
  ) -> BaseIoTRefreshableSession:
33
21
  if authentication_method not in (
@@ -0,0 +1,336 @@
1
+ __all__ = ["IOTX509RefreshableSession"]
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import cast
7
+ from urllib.parse import ParseResult, urlparse
8
+
9
+ from awscrt.exceptions import AwsCrtError
10
+ from awscrt.http import HttpClientConnection, HttpRequest
11
+ from awscrt.io import (
12
+ ClientBootstrap,
13
+ ClientTlsContext,
14
+ DefaultHostResolver,
15
+ EventLoopGroup,
16
+ Pkcs11Lib,
17
+ TlsConnectionOptions,
18
+ TlsContextOptions,
19
+ )
20
+
21
+ from ...exceptions import BRSError, BRSWarning
22
+ from ...utils import (
23
+ PKCS11,
24
+ AWSCRTResponse,
25
+ Identity,
26
+ TemporaryCredentials,
27
+ refreshable_session,
28
+ )
29
+ from .core import BaseIoTRefreshableSession
30
+
31
+
32
+ @refreshable_session
33
+ class IOTX509RefreshableSession(
34
+ BaseIoTRefreshableSession, registry_key="x509"
35
+ ):
36
+ """A :class:`boto3.session.Session` object that automatically refreshes
37
+ temporary credentials returned by the IoT Core credential provider.
38
+
39
+ Parameters
40
+ ----------
41
+ endpoint : str
42
+ The endpoint URL for the IoT Core credential provider. Must contain
43
+ '.credentials.iot.'.
44
+ role_alias : str
45
+ The IAM role alias to use when requesting temporary credentials.
46
+ certificate : str | bytes
47
+ The X.509 certificate to use when requesting temporary credentials.
48
+ ``str`` represents the file path to the certificate, while ``bytes``
49
+ represents the actual certificate data.
50
+ thing_name : str, optional
51
+ The name of the IoT thing to use when requesting temporary
52
+ credentials. Default is None.
53
+ private_key : str | bytes | None, optional
54
+ The private key to use when requesting temporary credentials. ``str``
55
+ represents the file path to the private key, while ``bytes``
56
+ represents the actual private key data. Optional only if ``pkcs11``
57
+ is provided. Default is None.
58
+ pkcs11 : PKCS11, optional
59
+ The PKCS#11 library to use when requesting temporary credentials. If
60
+ provided, ``private_key`` must be None.
61
+ ca : str | bytes | None, optional
62
+ The CA certificate to use when verifying the IoT Core endpoint. ``str``
63
+ represents the file path to the CA certificate, while ``bytes``
64
+ represents the actual CA certificate data. Default is None.
65
+ verify_peer : bool, optional
66
+ Whether to verify the CA certificate when establishing the TLS
67
+ connection. Default is True.
68
+ timeout : float | int | None, optional
69
+ The timeout for the TLS connection in seconds. Default is 10.0.
70
+ duration_seconds : int | None, optional
71
+ The duration for which the temporary credentials are valid, in
72
+ seconds. Cannot exceed the value declared in the IAM policy.
73
+ Default is None.
74
+
75
+ Notes
76
+ -----
77
+ Gavin Adams at AWS was a major influence on this implementation.
78
+ Thank you, Gavin!
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ endpoint: str,
84
+ role_alias: str,
85
+ certificate: str | bytes,
86
+ thing_name: str | None = None,
87
+ private_key: str | bytes | None = None,
88
+ pkcs11: PKCS11 | None = None,
89
+ ca: str | bytes | None = None,
90
+ verify_peer: bool = True,
91
+ timeout: float | int | None = None,
92
+ duration_seconds: int | None = None,
93
+ **kwargs,
94
+ ):
95
+ # initializing BRSSession
96
+ super().__init__(refresh_method="iot-x509", **kwargs)
97
+
98
+ # initializing public attributes
99
+ self.endpoint = self._normalize_iot_credential_endpoint(
100
+ endpoint=endpoint
101
+ )
102
+ self.role_alias = role_alias
103
+ self.certificate = certificate
104
+ self.thing_name = thing_name
105
+ self.private_key = private_key
106
+ self.pkcs11 = pkcs11
107
+ self.ca = ca
108
+ self.verify_peer = verify_peer
109
+ self.timeout = 10.0 if timeout is None else timeout
110
+ self.duration_seconds = duration_seconds
111
+
112
+ # loading X.509 certificate if presented as a string, which
113
+ # is presumed to be the file path.
114
+ # if presented as bytes then self.certificate is presumed to be
115
+ # the actual certificate itself
116
+ if self.certificate and isinstance(self.certificate, str):
117
+ self.certificate = (
118
+ Path(self.certificate).expanduser().resolve().read_bytes()
119
+ )
120
+
121
+ # either private_key or pkcs11 must be provided
122
+ if self.private_key is None and self.pkcs11 is None:
123
+ raise BRSError(
124
+ "Either 'private_key' or 'pkcs11' must be provided."
125
+ )
126
+
127
+ # . . . but both cannot be provided!
128
+ if self.private_key is not None and self.pkcs11 is not None:
129
+ raise BRSError(
130
+ "Only one of 'private_key' or 'pkcs11' can be provided."
131
+ )
132
+
133
+ # if the provided private_key is bytes then it's presumed to be
134
+ # the actual private key. but if it's string then it's presumed
135
+ # to be the file path
136
+ if self.private_key and isinstance(self.private_key, str):
137
+ self.private_key = (
138
+ Path(self.private_key).expanduser().resolve().read_bytes()
139
+ )
140
+
141
+ # verifying PKCS#11 dict
142
+ if self.pkcs11:
143
+ self.pkcs11 = self._validate_pkcs11(pkcs11=self.pkcs11)
144
+
145
+ # ca is like many other attributes in that str implies file location
146
+ if self.ca and isinstance(self.ca, str):
147
+ self.ca = Path(self.ca).expanduser().resolve().read_bytes()
148
+
149
+ def _get_credentials(self) -> TemporaryCredentials:
150
+ url = urlparse(
151
+ f"https://{self.endpoint}/role-aliases/{self.role_alias}"
152
+ "/credentials"
153
+ )
154
+ request = HttpRequest("GET", url.path)
155
+ request.headers.add("host", str(url.hostname))
156
+ if self.thing_name:
157
+ request.headers.add("x-amzn-iot-thingname", self.thing_name)
158
+ if self.duration_seconds:
159
+ request.headers.add(
160
+ "x-amzn-iot-credential-duration-seconds",
161
+ str(self.duration_seconds),
162
+ )
163
+ response = AWSCRTResponse()
164
+ port = 443 if not url.port else url.port
165
+ connection = (
166
+ self._mtls_client_connection(url=url, port=port)
167
+ if not self.pkcs11
168
+ else self._mtls_pkcs11_client_connection(url=url, port=port)
169
+ )
170
+
171
+ try:
172
+ stream = connection.request(
173
+ request, response.on_response, response.on_body
174
+ )
175
+ stream.activate()
176
+ stream.completion_future.result(float(self.timeout))
177
+ finally:
178
+ try:
179
+ connection.close()
180
+ except Exception:
181
+ ...
182
+
183
+ if response.status_code == 200:
184
+ credentials = json.loads(response.body.decode("utf-8"))[
185
+ "credentials"
186
+ ]
187
+ return {
188
+ "access_key": credentials["accessKeyId"],
189
+ "secret_key": credentials["secretAccessKey"],
190
+ "token": credentials["sessionToken"],
191
+ "expiry_time": credentials["expiration"],
192
+ }
193
+ else:
194
+ raise BRSError(
195
+ "Error getting credentials: "
196
+ f"{json.loads(response.body.decode())}"
197
+ )
198
+
199
+ def _mtls_client_connection(
200
+ self, url: ParseResult, port: int
201
+ ) -> HttpClientConnection:
202
+ event_loop_group: EventLoopGroup = EventLoopGroup()
203
+ host_resolver: DefaultHostResolver = DefaultHostResolver(
204
+ event_loop_group
205
+ )
206
+ bootstrap: ClientBootstrap = ClientBootstrap(
207
+ event_loop_group, host_resolver
208
+ )
209
+ tls_ctx_opt = TlsContextOptions.create_client_with_mtls(
210
+ cert_buffer=self.certificate, key_buffer=self.private_key
211
+ )
212
+
213
+ if self.ca:
214
+ tls_ctx_opt.override_default_trust_store(self.ca)
215
+
216
+ tls_ctx_opt.verify_peer = self.verify_peer
217
+ tls_ctx = ClientTlsContext(tls_ctx_opt)
218
+ tls_conn_opt: TlsConnectionOptions = cast(
219
+ TlsConnectionOptions, tls_ctx.new_connection_options()
220
+ )
221
+ tls_conn_opt.set_server_name(str(url.hostname))
222
+
223
+ try:
224
+ connection_future = HttpClientConnection.new(
225
+ host_name=str(url.hostname),
226
+ port=port,
227
+ bootstrap=bootstrap,
228
+ tls_connection_options=tls_conn_opt,
229
+ )
230
+ return connection_future.result(self.timeout)
231
+ except AwsCrtError as err:
232
+ raise BRSError(
233
+ "Error completing mTLS connection to endpoint "
234
+ f"'{url.hostname}'"
235
+ ) from err
236
+
237
+ def _mtls_pkcs11_client_connection(
238
+ self, url: ParseResult, port: int
239
+ ) -> HttpClientConnection:
240
+ event_loop_group: EventLoopGroup = EventLoopGroup()
241
+ host_resolver: DefaultHostResolver = DefaultHostResolver(
242
+ event_loop_group
243
+ )
244
+ bootstrap: ClientBootstrap = ClientBootstrap(
245
+ event_loop_group, host_resolver
246
+ )
247
+
248
+ if not self.pkcs11:
249
+ raise BRSError(
250
+ "Attempting to establish mTLS connection using PKCS#11"
251
+ "but 'pkcs11' parameter is 'None'!"
252
+ )
253
+
254
+ tls_ctx_opt = TlsContextOptions.create_client_with_mtls_pkcs11(
255
+ pkcs11_lib=Pkcs11Lib(file=self.pkcs11["pkcs11_lib"]),
256
+ user_pin=self.pkcs11["user_pin"],
257
+ slot_id=self.pkcs11["slot_id"],
258
+ token_label=self.pkcs11["token_label"],
259
+ private_key_label=self.pkcs11["private_key_label"],
260
+ cert_file_contents=self.certificate,
261
+ )
262
+
263
+ if self.ca:
264
+ tls_ctx_opt.override_default_trust_store(self.ca)
265
+
266
+ tls_ctx_opt.verify_peer = self.verify_peer
267
+ tls_ctx = ClientTlsContext(tls_ctx_opt)
268
+ tls_conn_opt: TlsConnectionOptions = cast(
269
+ TlsConnectionOptions, tls_ctx.new_connection_options()
270
+ )
271
+ tls_conn_opt.set_server_name(str(url.hostname))
272
+
273
+ try:
274
+ connection_future = HttpClientConnection.new(
275
+ host_name=str(url.hostname),
276
+ port=port,
277
+ bootstrap=bootstrap,
278
+ tls_connection_options=tls_conn_opt,
279
+ )
280
+ return connection_future.result(self.timeout)
281
+ except AwsCrtError as err:
282
+ raise BRSError("Error completing mTLS connection.") from err
283
+
284
+ def get_identity(self) -> Identity:
285
+ """Returns metadata about the current caller identity.
286
+
287
+ Returns
288
+ -------
289
+ Identity
290
+ Dict containing information about the current calleridentity.
291
+ """
292
+
293
+ return self.client("sts").get_caller_identity()
294
+
295
+ @staticmethod
296
+ def _normalize_iot_credential_endpoint(endpoint: str) -> str:
297
+ if ".credentials.iot." in endpoint:
298
+ return endpoint
299
+
300
+ if ".iot." in endpoint and "-ats." in endpoint:
301
+ logged_data_endpoint = re.sub(r"^[^. -]+", "***", endpoint)
302
+ logged_credential_endpoint = re.sub(
303
+ r"^[^. -]+",
304
+ "***",
305
+ (endpoint := endpoint.replace("-ats.iot", ".credentials.iot")),
306
+ )
307
+ BRSWarning.warn(
308
+ "The 'endpoint' parameter you provided represents the data "
309
+ "endpoint for IoT not the credentials endpoint! The endpoint "
310
+ "you provided was therefore modified from "
311
+ f"'{logged_data_endpoint}' -> '{logged_credential_endpoint}'"
312
+ )
313
+ return endpoint
314
+
315
+ raise BRSError(
316
+ "Invalid IoT endpoint provided for credentials provider. "
317
+ "Expected '<id>.credentials.iot.<region>.amazonaws.com'"
318
+ )
319
+
320
+ @staticmethod
321
+ def _validate_pkcs11(pkcs11: PKCS11) -> PKCS11:
322
+ if "pkcs11_lib" not in pkcs11:
323
+ raise BRSError(
324
+ "PKCS#11 library path must be provided as 'pkcs11_lib'"
325
+ " in 'pkcs11'."
326
+ )
327
+ elif not Path(pkcs11["pkcs11_lib"]).expanduser().resolve().is_file():
328
+ raise BRSError(
329
+ f"'{pkcs11['pkcs11_lib']}' is not a valid file path for "
330
+ "'pkcs11_lib' in 'pkcs11'."
331
+ )
332
+ pkcs11.setdefault("user_pin", None)
333
+ pkcs11.setdefault("slot_id", None)
334
+ pkcs11.setdefault("token_label", None)
335
+ pkcs11.setdefault("private_key_label", None)
336
+ return pkcs11
@@ -45,7 +45,7 @@ class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
45
45
  **kwargs,
46
46
  ):
47
47
  if "refresh_method" in kwargs:
48
- BRSWarning(
48
+ BRSWarning.warn(
49
49
  "'refresh_method' cannot be set manually. "
50
50
  "Reverting to 'sts-assume-role'."
51
51
  )
@@ -60,7 +60,7 @@ class STSRefreshableSession(BaseRefreshableSession, registry_key="sts"):
60
60
  if sts_client_kwargs is not None:
61
61
  # overwriting 'service_name' if if appears in sts_client_kwargs
62
62
  if "service_name" in sts_client_kwargs:
63
- BRSWarning(
63
+ BRSWarning.warn(
64
64
  "'sts_client_kwargs' cannot contain values for "
65
65
  "'service_name'. Reverting to service_name = 'sts'."
66
66
  )
@@ -36,6 +36,7 @@ class RefreshableSession:
36
36
  See Also
37
37
  --------
38
38
  boto3_refresh_session.methods.custom.CustomRefreshableSession
39
+ boto3_refresh_session.methods.iot.IOTX509RefreshableSession
39
40
  boto3_refresh_session.methods.sts.STSRefreshableSession
40
41
  """
41
42
 
@@ -1,4 +1,6 @@
1
1
  __all__ = [
2
+ "AWSCRTResponse",
3
+ "BaseIoTRefreshableSession",
2
4
  "BaseRefreshableSession",
3
5
  "BRSSession",
4
6
  "CredentialProvider",
@@ -10,6 +12,7 @@ from abc import ABC, abstractmethod
10
12
  from functools import wraps
11
13
  from typing import Any, Callable, ClassVar, Generic, TypeVar, cast
12
14
 
15
+ from awscrt.http import HttpHeaders
13
16
  from boto3.session import Session
14
17
  from botocore.credentials import (
15
18
  DeferredRefreshableCredentials,
@@ -19,6 +22,7 @@ from botocore.credentials import (
19
22
  from ..exceptions import BRSWarning
20
23
  from .typing import (
21
24
  Identity,
25
+ IoTAuthenticationMethod,
22
26
  Method,
23
27
  RefreshableTemporaryCredentials,
24
28
  RefreshMethod,
@@ -46,7 +50,9 @@ class Registry(Generic[RegistryKey]):
46
50
  super().__init_subclass__(**kwargs)
47
51
 
48
52
  if registry_key in cls.registry:
49
- BRSWarning(f"{registry_key!r} already registered. Overwriting.")
53
+ BRSWarning.warn(
54
+ f"{registry_key!r} already registered. Overwriting."
55
+ )
50
56
 
51
57
  if "sentinel" not in registry_key:
52
58
  cls.registry[registry_key] = cls
@@ -202,3 +208,35 @@ class BaseRefreshableSession(
202
208
 
203
209
  def __init__(self, **kwargs):
204
210
  super().__init__(**kwargs)
211
+
212
+
213
+ class BaseIoTRefreshableSession(
214
+ Registry[IoTAuthenticationMethod],
215
+ CredentialProvider,
216
+ BRSSession,
217
+ registry_key="__iot_sentinel__",
218
+ ):
219
+ def __init__(self, **kwargs):
220
+ super().__init__(**kwargs)
221
+
222
+
223
+ class AWSCRTResponse:
224
+ """Lightweight response collector for awscrt HTTP."""
225
+
226
+ def __init__(self):
227
+ """Initialize to default for when callbacks are called."""
228
+
229
+ self.status_code = None
230
+ self.headers = None
231
+ self.body = bytearray()
232
+
233
+ def on_response(self, http_stream, status_code, headers, **kwargs):
234
+ """Process awscrt.io response."""
235
+
236
+ self.status_code = status_code
237
+ self.headers = HttpHeaders(headers)
238
+
239
+ def on_body(self, http_stream, chunk, **kwargs):
240
+ """Process awscrt.io body."""
241
+
242
+ self.body.extend(chunk)
@@ -33,20 +33,23 @@ except ImportError:
33
33
  from typing_extensions import NotRequired
34
34
 
35
35
  #: Type alias for all currently available IoT authentication methods.
36
- IoTAuthenticationMethod = Literal["certificate", "cognito", "__iot_sentinel__"]
36
+ IoTAuthenticationMethod = Literal["x509", "__iot_sentinel__"]
37
37
 
38
38
  #: Type alias for all currently available credential refresh methods.
39
39
  Method = Literal[
40
- "sts",
41
40
  "custom",
41
+ "iot",
42
+ "sts",
42
43
  "__sentinel__",
43
- ] # TODO: Add iot when implemented
44
+ "__iot_sentinel__",
45
+ ]
44
46
 
45
47
  #: Type alias for all refresh method names.
46
48
  RefreshMethod = Literal[
47
- "sts-assume-role",
48
49
  "custom",
49
- ] # TODO: Add iot-certificate and iot-cognito when iot implemented
50
+ "iot-x509",
51
+ "sts-assume-role",
52
+ ]
50
53
 
51
54
  #: Type alias for all currently registered credential refresh methods.
52
55
  RegistryKey = TypeVar("RegistryKey", bound=str)
@@ -136,7 +139,7 @@ class STSClientParams(TypedDict):
136
139
 
137
140
 
138
141
  class PKCS11(TypedDict):
139
- pkcs11_loc: str
142
+ pkcs11_lib: str
140
143
  user_pin: NotRequired[str]
141
144
  slot_id: NotRequired[int]
142
145
  token_label: NotRequired[str | None]
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boto3-refresh-session
3
- Version: 4.0.0
3
+ Version: 5.0.1
4
4
  Summary: A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically.
5
5
  License: MIT
6
- Keywords: boto3,botocore,aws,sts,credentials,token,refresh
6
+ Keywords: boto3,botocore,aws,sts,credentials,token,refresh,iot,x509
7
7
  Author: Mike Letts
8
8
  Author-email: lettsmt@gmail.com
9
9
  Maintainer: Michael Letts
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Dist: awscrt
18
19
  Requires-Dist: boto3
19
20
  Requires-Dist: botocore
20
21
  Requires-Dist: requests
@@ -120,30 +121,15 @@ Description-Content-Type: text/markdown
120
121
  ## 😛 Features
121
122
 
122
123
  - Drop-in replacement for `boto3.session.Session`
123
- - Supports automatic credential refresh methods for STS
124
- - Supports custom authentication methods with automatic refresh for complicated authentication flows
124
+ - Supports automatic credential refresh for:
125
+ - **STS**
126
+ - **IoT Core**
127
+ - X.509 certificates w/ role aliases over mTLS (PEM files and PKCS#11)
128
+ - Custom authentication methods
125
129
  - Natively supports all parameters supported by `boto3.session.Session`
126
130
  - [Tested](https://github.com/michaelthomasletts/boto3-refresh-session/tree/main/tests), [documented](https://michaelthomasletts.github.io/boto3-refresh-session/index.html), and [published to PyPI](https://pypi.org/project/boto3-refresh-session/)
127
131
  - Future releases will include support for IoT (coming soon)
128
132
 
129
- ## ⚠️ Important Updates
130
-
131
- #### 😥 v3.0.0
132
-
133
- **The changes introduced by v3.0.0 will not impact ~99% of users** who generally interact with `boto3-refresh-session` by only `RefreshableSession`, *which is the intended usage for this package after all.*
134
-
135
- Advanced users, however, particularly those using low-level objects such as `BaseRefreshableSession | refreshable_session | BRSSession | utils.py`, may experience breaking changes.
136
-
137
- Please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-session/pull/75) for additional details.
138
-
139
- #### ✂️ v4.0.0
140
-
141
- The `ecs` module has been dropped. For additional details and rationale, please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-session/pull/78).
142
-
143
- #### ☎️ Delayed Responses
144
-
145
- I am currently grappling with a very serious medical condition. Accordingly, expect delayed responses to issues and requests until my health stabilizes.
146
-
147
133
  ## 😌 Recognition and Testimonials
148
134
 
149
135
  [Featured in TL;DR Sec.](https://tldrsec.com/p/tldr-sec-282)
@@ -301,3 +287,74 @@ pip install boto3-refresh-session
301
287
  ```
302
288
 
303
289
  </details>
290
+
291
+ <details>
292
+ <summary><strong>IoT Core X.509 (click to expand)</strong></summary>
293
+
294
+ ### IoT Core X.509
295
+
296
+ AWS IoT Core can vend temporary AWS credentials through the **credentials provider** when you connect with an X.509 certificate and a **role alias**. `boto3-refresh-session` makes this flow seamless by automatically refreshing credentials over **mTLS**.
297
+
298
+ For additional information on the exact parameters that `IOTX509RefreshableSession` takes, [check this documentation](https://github.com/michaelthomasletts/boto3-refresh-session/blob/main/boto3_refresh_session/methods/iot/x509.py).
299
+
300
+ ### PEM file
301
+
302
+ ```python
303
+ import boto3_refresh_session as brs
304
+
305
+ # PEM certificate + private key example
306
+ session = brs.RefreshableSession(
307
+ method="iot",
308
+ endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
309
+ role_alias="<your-role-alias>",
310
+ certificate="/path/to/certificate.pem",
311
+ private_key="/path/to/private-key.pem",
312
+ thing_name="<your-thing-name>", # optional, if used in policies
313
+ duration_seconds=3600, # optional, capped by role alias
314
+ region_name="us-east-1",
315
+ )
316
+
317
+ # Now you can use the session like any boto3 session
318
+ s3 = session.client("s3")
319
+ print(s3.list_buckets())
320
+ ```
321
+
322
+ ### PKCS#11
323
+
324
+ ```python
325
+ session = brs.RefreshableSession(
326
+ method="iot",
327
+ endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
328
+ role_alias="<your-role-alias>",
329
+ certificate="/path/to/certificate.pem",
330
+ pkcs11={
331
+ "pkcs11_lib": "/usr/local/lib/softhsm/libsofthsm2.so",
332
+ "user_pin": "1234",
333
+ "slot_id": 0,
334
+ "token_label": "MyToken",
335
+ "private_key_label": "MyKey",
336
+ },
337
+ thing_name="<your-thing-name>",
338
+ region_name="us-east-1",
339
+ )
340
+ ```
341
+
342
+ </details>
343
+
344
+ ## ⚠️ Changes
345
+
346
+ #### 😥 v3.0.0
347
+
348
+ **The changes introduced by v3.0.0 will not impact ~99% of users** who generally interact with `boto3-refresh-session` by only `RefreshableSession`, *which is the intended usage for this package after all.*
349
+
350
+ Advanced users, however, particularly those using low-level objects such as `BaseRefreshableSession | refreshable_session | BRSSession | utils.py`, may experience breaking changes.
351
+
352
+ Please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-session/pull/75) for additional details.
353
+
354
+ #### ✂️ v4.0.0
355
+
356
+ The `ecs` module has been dropped. For additional details and rationale, please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-session/pull/78).
357
+
358
+ #### 😛 v5.0.0
359
+
360
+ Support for IoT Core via X.509 certificate-based authentication (over HTTPS) is now available!
@@ -0,0 +1,17 @@
1
+ boto3_refresh_session/__init__.py,sha256=ytuR2e2ELOdRrxiayLpWWfy75lcN5WQu5YMO_2QbHao,415
2
+ boto3_refresh_session/exceptions.py,sha256=QS5_xy3hNrfkdT_wKPZWH8WqSbFYCKPcK8DomGYIvcU,1218
3
+ boto3_refresh_session/methods/__init__.py,sha256=FpwWixSVpy_6pUe1u4fXmjO-_fDH--qTk_xrMnBCHxU,193
4
+ boto3_refresh_session/methods/custom.py,sha256=MLdUMU9s6NQoJWBKQ5Fsxeyxb_Xrm9V59pVX22M8fyI,4178
5
+ boto3_refresh_session/methods/iot/__init__.py,sha256=wIYp7HFZ_Q8XEHwWmpKjDNXxBm29C0RisP_9GSVwzZI,147
6
+ boto3_refresh_session/methods/iot/core.py,sha256=xtvbC23h6fw06lRZWN4r7TlnUEf3t9T7-zSPGCSlSLI,1151
7
+ boto3_refresh_session/methods/iot/x509.py,sha256=QvHXJKRkRuF5TOUOEH4rTN8lfm_rRnp-flLoolMaxbw,12368
8
+ boto3_refresh_session/methods/sts.py,sha256=NGqJFJNLjG9Mve7o19tb_i6lvgQW1HoALIqF6lJNV9A,3336
9
+ boto3_refresh_session/session.py,sha256=UM_dWHSo0Wn8gLN99zg36SRVb-Yy_to1wk8UgZEuQZA,2086
10
+ boto3_refresh_session/utils/__init__.py,sha256=6F2ErbgBT2ZmZwFF3OzvQEd1Vh4XM3kaL6YGMTrcrkQ,156
11
+ boto3_refresh_session/utils/internal.py,sha256=HbuIzT0pC8QS4pgNj3M7POGaW-OEz2l3ESfYI1Qouuo,7072
12
+ boto3_refresh_session/utils/typing.py,sha256=AqPey1N8nNUU2BwQYIIz-xGrfgjyNUzDt8MK0eB5DSQ,3429
13
+ boto3_refresh_session-5.0.1.dist-info/LICENSE,sha256=I3ZYTXAjbIly6bm6J-TvFTuuHwTKws4h89QaY5c5HiY,1067
14
+ boto3_refresh_session-5.0.1.dist-info/METADATA,sha256=MU9rSSwpRswocpZg2oLHfYR_ip-BJvZMljZQFDY_f_0,13956
15
+ boto3_refresh_session-5.0.1.dist-info/NOTICE,sha256=1s8r33qbl1z0YvPB942iWgvbkP94P_e8AnROr1qXXuw,939
16
+ boto3_refresh_session-5.0.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ boto3_refresh_session-5.0.1.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- from .certificate import IoTCertificateRefreshableSession
2
- from .core import IoTRefreshableSession
3
-
4
- __all__ = ["IoTRefreshableSession"]
@@ -1,57 +0,0 @@
1
- __all__ = ["IoTCertificateRefreshableSession"]
2
-
3
- from pathlib import Path
4
- from typing import Any
5
-
6
- from ...exceptions import BRSError
7
- from ...utils import (
8
- Identity, PKCS11, TemporaryCredentials, refreshable_session
9
- )
10
- from .core import BaseIoTRefreshableSession
11
-
12
-
13
- @refreshable_session
14
- class IoTCertificateRefreshableSession(
15
- BaseIoTRefreshableSession, registry_key="certificate"
16
- ):
17
- def __init__(
18
- self,
19
- endpoint: str,
20
- role_alias: str,
21
- thing_name: str,
22
- certificate: str | bytes,
23
- private_key: str | bytes | None = None,
24
- pkcs11: PKCS11 | None = None,
25
- ca: bytes | None = None,
26
- verify_peer: bool = True,
27
- ):
28
- self.endpoint = endpoint
29
- self.role_alias = role_alias
30
- self.thing_name = thing_name
31
- self.certificate = certificate
32
- self.private_key = private_key
33
- self.pkcs11 = pkcs11
34
- self.ca = ca
35
- self.verify_peer = verify_peer
36
-
37
- if self.certificate and isinstance(self.certificate, str):
38
- with open(Path(self.certificate), "rb") as cert_pem_file:
39
- self.certificate = cert_pem_file.read()
40
-
41
- if self.private_key is None and self.pkcs11 is None:
42
- raise BRSError(
43
- "Either 'private_key' or 'pkcs11' must be provided."
44
- )
45
-
46
- if self.private_key is not None and self.pkcs11 is not None:
47
- raise BRSError(
48
- "Only one of 'private_key' or 'pkcs11' can be provided."
49
- )
50
-
51
- if self.private_key and isinstance(self.private_key, str):
52
- with open(Path(self.private_key), "rb") as private_key_pem_file:
53
- self.private_key = private_key_pem_file.read()
54
-
55
- def _get_credentials(self) -> TemporaryCredentials: ...
56
-
57
- def get_identity(self) -> Identity: ...
@@ -1,17 +0,0 @@
1
- __all__ = ["IoTCognitoRefreshableSession"]
2
-
3
- from typing import Any
4
-
5
- from ...utils import Identity, TemporaryCredentials, refreshable_session
6
- from .core import BaseIoTRefreshableSession
7
-
8
-
9
- @refreshable_session
10
- class IoTCognitoRefreshableSession(
11
- BaseIoTRefreshableSession, registry_key="cognito"
12
- ):
13
- def __init__(self): ...
14
-
15
- def _get_credentials(self) -> TemporaryCredentials: ...
16
-
17
- def get_identity(self) -> Identity: ...
@@ -1,18 +0,0 @@
1
- boto3_refresh_session/__init__.py,sha256=8Mws6gI3cPLZ1ODvQA2WhU-Clq9v5k7MvYMdM3mr5l8,388
2
- boto3_refresh_session/exceptions.py,sha256=DumBh6cDVU46eelSNt1CsG2uMSBekSbmhqWEaAWw130,1003
3
- boto3_refresh_session/methods/__init__.py,sha256=Npth3cOf8waY0XOS0xIvbG28pOIU8rs7_hi5NhJ9HwU,280
4
- boto3_refresh_session/methods/custom.py,sha256=j90Iv1DKdGgP1JNwQfpEhaJDBrB2AtDe8kqI2Mktwlg,4173
5
- boto3_refresh_session/methods/iot/__init__.typed,sha256=Z33nIB6oCsz9TZwikHfNHgY1SKxkSCdB5rwdPSUl3C4,135
6
- boto3_refresh_session/methods/iot/certificate.typed,sha256=sFTa1rF7tebr48Bjw_YtVeOdVvazAHBJGGiM33tsFXI,1828
7
- boto3_refresh_session/methods/iot/cognito.typed,sha256=wyBMWUkuhLt27JsKZIwtfylDdCavNexcEy16ZaDFjUY,435
8
- boto3_refresh_session/methods/iot/core.typed,sha256=Q5WshxgIIOgAaqoU7n8wBKMe9eSzZ6H8db-q1gThHzk,1407
9
- boto3_refresh_session/methods/sts.py,sha256=dzf68BE0f1nFsITOKOnygh-mTvBqThKkrW2eEc-wFKA,3326
10
- boto3_refresh_session/session.py,sha256=aJaOJK1yBFWkSc2qZzrQuDcMYto73S_iyabCF5vJZls,2022
11
- boto3_refresh_session/utils/__init__.py,sha256=6F2ErbgBT2ZmZwFF3OzvQEd1Vh4XM3kaL6YGMTrcrkQ,156
12
- boto3_refresh_session/utils/internal.py,sha256=bpKTAF_xdBw1wJPHIG8aGRMiXkSkp7CI9et0U5o3qEI,6103
13
- boto3_refresh_session/utils/typing.py,sha256=KXE-BFb303pVdIj5MUp66aJSRRDjDV3mfuPcSA_QE4g,3496
14
- boto3_refresh_session-4.0.0.dist-info/LICENSE,sha256=I3ZYTXAjbIly6bm6J-TvFTuuHwTKws4h89QaY5c5HiY,1067
15
- boto3_refresh_session-4.0.0.dist-info/METADATA,sha256=cH0FKUnHTxXNxzhuDpZFGXBxujnoYY0-FPuYsEqJqJM,12133
16
- boto3_refresh_session-4.0.0.dist-info/NOTICE,sha256=1s8r33qbl1z0YvPB942iWgvbkP94P_e8AnROr1qXXuw,939
17
- boto3_refresh_session-4.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
18
- boto3_refresh_session-4.0.0.dist-info/RECORD,,