boto3-refresh-session 4.0.0__tar.gz → 5.0.0__tar.gz
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.
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/PKG-INFO +65 -4
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/README.md +62 -2
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/__init__.py +2 -1
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/exceptions.py +8 -0
- boto3_refresh_session-5.0.0/boto3_refresh_session/methods/__init__.py +10 -0
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/methods/custom.py +1 -1
- boto3_refresh_session-5.0.0/boto3_refresh_session/methods/iot/__init__.py +7 -0
- boto3_refresh_session-4.0.0/boto3_refresh_session/methods/iot/core.typed → boto3_refresh_session-5.0.0/boto3_refresh_session/methods/iot/core.py +4 -16
- boto3_refresh_session-5.0.0/boto3_refresh_session/methods/iot/x509.py +336 -0
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/methods/sts.py +2 -2
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/session.py +1 -0
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/internal.py +39 -1
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/typing.py +9 -6
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/pyproject.toml +3 -3
- boto3_refresh_session-4.0.0/boto3_refresh_session/methods/__init__.py +0 -10
- boto3_refresh_session-4.0.0/boto3_refresh_session/methods/iot/__init__.typed +0 -4
- boto3_refresh_session-4.0.0/boto3_refresh_session/methods/iot/certificate.typed +0 -57
- boto3_refresh_session-4.0.0/boto3_refresh_session/methods/iot/cognito.typed +0 -17
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/LICENSE +0 -0
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/NOTICE +0 -0
- {boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/__init__.py +0 -0
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: boto3-refresh-session
|
3
|
-
Version:
|
3
|
+
Version: 5.0.0
|
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,8 +121,11 @@ Description-Content-Type: text/markdown
|
|
120
121
|
## 😛 Features
|
121
122
|
|
122
123
|
- Drop-in replacement for `boto3.session.Session`
|
123
|
-
- Supports automatic credential refresh
|
124
|
-
-
|
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)
|
@@ -140,6 +144,10 @@ Please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-sess
|
|
140
144
|
|
141
145
|
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
146
|
|
147
|
+
#### 😛 v5.0.0
|
148
|
+
|
149
|
+
Support for IoT Core via X.509 certificate-based authentication (over HTTPS) is now available!
|
150
|
+
|
143
151
|
#### ☎️ Delayed Responses
|
144
152
|
|
145
153
|
I am currently grappling with a very serious medical condition. Accordingly, expect delayed responses to issues and requests until my health stabilizes.
|
@@ -301,3 +309,56 @@ pip install boto3-refresh-session
|
|
301
309
|
```
|
302
310
|
|
303
311
|
</details>
|
312
|
+
|
313
|
+
<details>
|
314
|
+
<summary><strong>IoT Core X.509 (click to expand)</strong></summary>
|
315
|
+
|
316
|
+
### IoT Core X.509
|
317
|
+
|
318
|
+
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**.
|
319
|
+
|
320
|
+
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).
|
321
|
+
|
322
|
+
### PEM file
|
323
|
+
|
324
|
+
```python
|
325
|
+
import boto3_refresh_session as brs
|
326
|
+
|
327
|
+
# PEM certificate + private key example
|
328
|
+
session = brs.RefreshableSession(
|
329
|
+
method="iot",
|
330
|
+
endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
|
331
|
+
role_alias="<your-role-alias>",
|
332
|
+
certificate="/path/to/certificate.pem",
|
333
|
+
private_key="/path/to/private-key.pem",
|
334
|
+
thing_name="<your-thing-name>", # optional, if used in policies
|
335
|
+
duration_seconds=3600, # optional, capped by role alias
|
336
|
+
region_name="us-east-1",
|
337
|
+
)
|
338
|
+
|
339
|
+
# Now you can use the session like any boto3 session
|
340
|
+
s3 = session.client("s3")
|
341
|
+
print(s3.list_buckets())
|
342
|
+
```
|
343
|
+
|
344
|
+
### PKCS#11
|
345
|
+
|
346
|
+
```python
|
347
|
+
session = brs.RefreshableSession(
|
348
|
+
method="iot",
|
349
|
+
endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
|
350
|
+
role_alias="<your-role-alias>",
|
351
|
+
certificate="/path/to/certificate.pem",
|
352
|
+
pkcs11={
|
353
|
+
"pkcs11_lib": "/usr/local/lib/softhsm/libsofthsm2.so",
|
354
|
+
"user_pin": "1234",
|
355
|
+
"slot_id": 0,
|
356
|
+
"token_label": "MyToken",
|
357
|
+
"private_key_label": "MyKey",
|
358
|
+
},
|
359
|
+
thing_name="<your-thing-name>",
|
360
|
+
region_name="us-east-1",
|
361
|
+
)
|
362
|
+
```
|
363
|
+
|
364
|
+
</details>
|
@@ -95,8 +95,11 @@
|
|
95
95
|
## 😛 Features
|
96
96
|
|
97
97
|
- Drop-in replacement for `boto3.session.Session`
|
98
|
-
- Supports automatic credential refresh
|
99
|
-
-
|
98
|
+
- Supports automatic credential refresh for:
|
99
|
+
- **STS**
|
100
|
+
- **IoT Core**
|
101
|
+
- X.509 certificates w/ role aliases over mTLS (PEM files and PKCS#11)
|
102
|
+
- Custom authentication methods
|
100
103
|
- Natively supports all parameters supported by `boto3.session.Session`
|
101
104
|
- [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/)
|
102
105
|
- Future releases will include support for IoT (coming soon)
|
@@ -115,6 +118,10 @@ Please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-sess
|
|
115
118
|
|
116
119
|
The `ecs` module has been dropped. For additional details and rationale, please review [this PR](https://github.com/michaelthomasletts/boto3-refresh-session/pull/78).
|
117
120
|
|
121
|
+
#### 😛 v5.0.0
|
122
|
+
|
123
|
+
Support for IoT Core via X.509 certificate-based authentication (over HTTPS) is now available!
|
124
|
+
|
118
125
|
#### ☎️ Delayed Responses
|
119
126
|
|
120
127
|
I am currently grappling with a very serious medical condition. Accordingly, expect delayed responses to issues and requests until my health stabilizes.
|
@@ -275,4 +282,57 @@ pip install boto3-refresh-session
|
|
275
282
|
)
|
276
283
|
```
|
277
284
|
|
285
|
+
</details>
|
286
|
+
|
287
|
+
<details>
|
288
|
+
<summary><strong>IoT Core X.509 (click to expand)</strong></summary>
|
289
|
+
|
290
|
+
### IoT Core X.509
|
291
|
+
|
292
|
+
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**.
|
293
|
+
|
294
|
+
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).
|
295
|
+
|
296
|
+
### PEM file
|
297
|
+
|
298
|
+
```python
|
299
|
+
import boto3_refresh_session as brs
|
300
|
+
|
301
|
+
# PEM certificate + private key example
|
302
|
+
session = brs.RefreshableSession(
|
303
|
+
method="iot",
|
304
|
+
endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
|
305
|
+
role_alias="<your-role-alias>",
|
306
|
+
certificate="/path/to/certificate.pem",
|
307
|
+
private_key="/path/to/private-key.pem",
|
308
|
+
thing_name="<your-thing-name>", # optional, if used in policies
|
309
|
+
duration_seconds=3600, # optional, capped by role alias
|
310
|
+
region_name="us-east-1",
|
311
|
+
)
|
312
|
+
|
313
|
+
# Now you can use the session like any boto3 session
|
314
|
+
s3 = session.client("s3")
|
315
|
+
print(s3.list_buckets())
|
316
|
+
```
|
317
|
+
|
318
|
+
### PKCS#11
|
319
|
+
|
320
|
+
```python
|
321
|
+
session = brs.RefreshableSession(
|
322
|
+
method="iot",
|
323
|
+
endpoint="<your-credentials-endpoint>.credentials.iot.<region>.amazonaws.com",
|
324
|
+
role_alias="<your-role-alias>",
|
325
|
+
certificate="/path/to/certificate.pem",
|
326
|
+
pkcs11={
|
327
|
+
"pkcs11_lib": "/usr/local/lib/softhsm/libsofthsm2.so",
|
328
|
+
"user_pin": "1234",
|
329
|
+
"slot_id": 0,
|
330
|
+
"token_label": "MyToken",
|
331
|
+
"private_key_label": "MyKey",
|
332
|
+
},
|
333
|
+
thing_name="<your-thing-name>",
|
334
|
+
region_name="us-east-1",
|
335
|
+
)
|
336
|
+
```
|
337
|
+
|
278
338
|
</details>
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/__init__.py
RENAMED
@@ -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__ = "
|
12
|
+
__version__ = "5.0.0"
|
12
13
|
__title__ = "boto3-refresh-session"
|
13
14
|
__author__ = "Mike Letts"
|
14
15
|
__maintainer__ = "Mike Letts"
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/exceptions.py
RENAMED
@@ -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)
|
@@ -6,28 +6,16 @@ from typing import get_args
|
|
6
6
|
|
7
7
|
from ...exceptions import BRSError
|
8
8
|
from ...utils import (
|
9
|
-
|
10
|
-
|
11
|
-
|
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 = "
|
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
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/methods/sts.py
RENAMED
@@ -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
|
)
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/internal.py
RENAMED
@@ -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(
|
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)
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/typing.py
RENAMED
@@ -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["
|
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
|
-
|
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
|
-
|
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
|
-
|
142
|
+
pkcs11_lib: str
|
140
143
|
user_pin: NotRequired[str]
|
141
144
|
slot_id: NotRequired[int]
|
142
145
|
token_label: NotRequired[str | None]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "boto3-refresh-session"
|
3
|
-
version = "
|
3
|
+
version = "5.0.0"
|
4
4
|
description = "A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically."
|
5
5
|
authors = [
|
6
6
|
{name = "Mike Letts",email = "lettsmt@gmail.com"}
|
@@ -8,8 +8,8 @@ authors = [
|
|
8
8
|
license = {text = "MIT"}
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.10"
|
11
|
-
dependencies = ["boto3", "botocore", "requests", "typing-extensions"]
|
12
|
-
keywords = ["boto3", "botocore", "aws", "sts", "credentials", "token", "refresh"]
|
11
|
+
dependencies = ["boto3", "botocore", "requests", "typing-extensions", "awscrt"]
|
12
|
+
keywords = ["boto3", "botocore", "aws", "sts", "credentials", "token", "refresh", "iot", "x509"]
|
13
13
|
maintainers = [
|
14
14
|
{name="Michael Letts", email="lettsmt@gmail.com"},
|
15
15
|
]
|
@@ -1,10 +0,0 @@
|
|
1
|
-
__all__ = []
|
2
|
-
|
3
|
-
# TODO: import iot submodules when finished
|
4
|
-
from . import custom, sts
|
5
|
-
from .custom import CustomRefreshableSession
|
6
|
-
from .sts import STSRefreshableSession
|
7
|
-
|
8
|
-
# TODO: add iot submodules to __all__ when finished
|
9
|
-
__all__.extend(custom.__all__)
|
10
|
-
__all__.extend(sts.__all__)
|
@@ -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: ...
|
File without changes
|
File without changes
|
{boto3_refresh_session-4.0.0 → boto3_refresh_session-5.0.0}/boto3_refresh_session/utils/__init__.py
RENAMED
File without changes
|