capo-cloudtrail-data 0.2.0__tar.gz → 0.4.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.
- {capo_cloudtrail_data-0.2.0/src/capo_cloudtrail_data.egg-info → capo_cloudtrail_data-0.4.0}/PKG-INFO +2 -2
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/pyproject.toml +2 -2
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_signers.py +54 -17
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_sigv4.py +164 -19
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_operations/cloud_trail_data_service/put_audit_events.py +3 -2
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/eventstream.py +15 -13
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_services/_pipeline.py +17 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0/src/capo_cloudtrail_data.egg-info}/PKG-INFO +2 -2
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/tests/test_sigv4.py +217 -16
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/LICENSE +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/README.md +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/setup.cfg +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/__init__.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_async.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_identity.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_providers.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_zapros_handler.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_body.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_iter.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_pagination.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/__init__.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/errors.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/serialize.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/xml.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_rule_engine/__init__.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_rule_engine/_aws_partition.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_rule_engine/_endpoint_rule_set.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_rule_engine/_endpoint_runtime.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_services/_aws_config.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_services/async_cloud_trail_data.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_services/cloud_trail_data.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/__init__.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/_base.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/channel_insufficient_permission.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/channel_not_found.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/channel_unsupported_schema.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/duplicated_audit_event_id.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/invalid_channel_arn.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/unsupported_operation_exception.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/py.typed +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/_prelude/blob.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/_prelude/timestamp.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/audit_event.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/audit_event_result_entries.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/audit_event_result_entry.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/audit_events.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/channel_arn.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/error_code.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/error_message.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/external_id.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/put_audit_events_request.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/put_audit_events_response.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/result_error_entries.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/result_error_entry.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/uuid.py +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data.egg-info/SOURCES.txt +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data.egg-info/dependency_links.txt +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data.egg-info/requires.txt +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data.egg-info/top_level.txt +0 -0
- {capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/tests/test_body.py +0 -0
{capo_cloudtrail_data-0.2.0/src/capo_cloudtrail_data.egg-info → capo_cloudtrail_data-0.4.0}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capo-cloudtrail-data
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for Cloudtrail Data.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python SDK for AWS Cloudtrail Data.
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_signers.py
RENAMED
|
@@ -7,7 +7,11 @@ from zapros import Request
|
|
|
7
7
|
|
|
8
8
|
from capo_cloudtrail_data._auth._identity import Credentials, Identity
|
|
9
9
|
from capo_cloudtrail_data._auth._providers import IdentityProvider
|
|
10
|
-
from capo_cloudtrail_data._auth._sigv4 import
|
|
10
|
+
from capo_cloudtrail_data._auth._sigv4 import (
|
|
11
|
+
S3_SIGNING_NAMES,
|
|
12
|
+
SigV4AuthContext,
|
|
13
|
+
sign_sigv4,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
IdentityT = TypeVar("IdentityT", bound="Identity")
|
|
13
17
|
|
|
@@ -30,14 +34,25 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
30
34
|
The full auth scheme (``name`` variant, ``signingName``, ``signingRegion``,
|
|
31
35
|
encoding/normalization flags) is provided by the caller — either from the
|
|
32
36
|
endpoint rule-set's ``authSchemes`` property or built by the generated
|
|
33
|
-
``get_signer`` from operation defaults.
|
|
37
|
+
``get_signer`` from operation defaults. ``unsigned_payload`` mirrors the
|
|
38
|
+
operation's ``aws.auth#unsignedPayload`` trait: the body is sent but left
|
|
39
|
+
out of the signature. ``event_stream`` marks a request event stream: the
|
|
40
|
+
request is signed with the ``STREAMING-AWS4-HMAC-SHA256-EVENTS`` payload
|
|
41
|
+
marker and every event with a signature chained from it.
|
|
34
42
|
"""
|
|
35
43
|
|
|
36
44
|
def __init__(
|
|
37
|
-
self,
|
|
45
|
+
self,
|
|
46
|
+
provider: IdentityProvider[Credentials],
|
|
47
|
+
*,
|
|
48
|
+
auth_scheme: dict[str, Any],
|
|
49
|
+
unsigned_payload: bool = False,
|
|
50
|
+
event_stream: bool = False,
|
|
38
51
|
) -> None:
|
|
39
52
|
super().__init__(provider)
|
|
40
53
|
self._auth_scheme = auth_scheme
|
|
54
|
+
self._unsigned_payload = unsigned_payload
|
|
55
|
+
self._event_stream = event_stream
|
|
41
56
|
|
|
42
57
|
async def asign(self, req: Request) -> Request:
|
|
43
58
|
creds = await self.provider.aresolve_identity()
|
|
@@ -55,13 +70,24 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
55
70
|
"disableNormalizePath", False
|
|
56
71
|
),
|
|
57
72
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
body
|
|
64
|
-
|
|
73
|
+
unsigned_payload = self._unsigned_payload
|
|
74
|
+
if (
|
|
75
|
+
not unsigned_payload
|
|
76
|
+
and not self._event_stream
|
|
77
|
+
and req.body is not None
|
|
78
|
+
and not isinstance(req.body, bytes)
|
|
79
|
+
):
|
|
80
|
+
if self._auth_scheme["signingName"] in S3_SIGNING_NAMES:
|
|
81
|
+
# S3 accepts UNSIGNED-PAYLOAD for any operation; streamed bodies rely on it.
|
|
82
|
+
unsigned_payload = True
|
|
83
|
+
else:
|
|
84
|
+
raise NotImplementedError(
|
|
85
|
+
"Currently we don't support signed chunked payloads, so buffer the body and "
|
|
86
|
+
"pass bytes as a workaround; chunked signed implementation coming soon"
|
|
87
|
+
)
|
|
88
|
+
return sign_sigv4(
|
|
89
|
+
req, ctx, unsigned_payload=unsigned_payload, event_stream=self._event_stream
|
|
90
|
+
)
|
|
65
91
|
|
|
66
92
|
def sign(self, req: Request) -> Request:
|
|
67
93
|
creds = self.provider.resolve_identity()
|
|
@@ -79,10 +105,21 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
79
105
|
"disableNormalizePath", False
|
|
80
106
|
),
|
|
81
107
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
body
|
|
88
|
-
|
|
108
|
+
unsigned_payload = self._unsigned_payload
|
|
109
|
+
if (
|
|
110
|
+
not unsigned_payload
|
|
111
|
+
and not self._event_stream
|
|
112
|
+
and req.body is not None
|
|
113
|
+
and not isinstance(req.body, bytes)
|
|
114
|
+
):
|
|
115
|
+
if self._auth_scheme["signingName"] in S3_SIGNING_NAMES:
|
|
116
|
+
# S3 accepts UNSIGNED-PAYLOAD for any operation; streamed bodies rely on it.
|
|
117
|
+
unsigned_payload = True
|
|
118
|
+
else:
|
|
119
|
+
raise NotImplementedError(
|
|
120
|
+
"Currently we don't support signed chunked payloads, so buffer the body and "
|
|
121
|
+
"pass bytes as a workaround; chunked signed implementation coming soon"
|
|
122
|
+
)
|
|
123
|
+
return sign_sigv4(
|
|
124
|
+
req, ctx, unsigned_payload=unsigned_payload, event_stream=self._event_stream
|
|
125
|
+
)
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_auth/_sigv4.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""AWS Signature Version 4 — single-chunk signing.
|
|
1
|
+
"""AWS Signature Version 4 — single-chunk and event-stream signing.
|
|
2
2
|
|
|
3
3
|
Reference:
|
|
4
4
|
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
|
|
@@ -15,7 +15,8 @@ import functools
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import hmac
|
|
17
17
|
import re
|
|
18
|
-
from
|
|
18
|
+
from collections.abc import AsyncIterator, Iterator
|
|
19
|
+
from typing import Any, Literal, TypedDict, cast
|
|
19
20
|
from urllib.parse import quote, unquote
|
|
20
21
|
|
|
21
22
|
import zapros
|
|
@@ -23,6 +24,9 @@ from pywhatwgurl import URLSearchParams
|
|
|
23
24
|
from zapros import Headers, Request
|
|
24
25
|
from zapros._utils import get_host_header_value
|
|
25
26
|
|
|
27
|
+
from .._iter import AnyIterator
|
|
28
|
+
from .._protocol.eventstream import HeaderValue, Message, encode_headers
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
def build_sigv4_auth_scheme(
|
|
28
32
|
signing_name: str, region: str | None, endpoint_scheme: dict[str, Any] | None = None
|
|
@@ -65,6 +69,10 @@ class SigV4AuthContext(TypedDict):
|
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
_SIGV4_ALGORITHM = "AWS4-HMAC-SHA256"
|
|
72
|
+
# Per-event algorithm and request payload marker of a signed event stream.
|
|
73
|
+
_EVENT_ALGORITHM = "AWS4-HMAC-SHA256-PAYLOAD"
|
|
74
|
+
_EVENT_STREAM_PAYLOAD = "STREAMING-AWS4-HMAC-SHA256-EVENTS"
|
|
75
|
+
_AMZ_DATE_FORMAT = "%Y%m%dT%H%M%SZ"
|
|
68
76
|
_EMPTY_PAYLOAD_SHA256 = hashlib.sha256(b"").hexdigest()
|
|
69
77
|
|
|
70
78
|
# Headers excluded from the signed-headers set. Mirrors botocore's denylist:
|
|
@@ -95,9 +103,11 @@ _UNSIGNED_HEADERS = frozenset(
|
|
|
95
103
|
|
|
96
104
|
_MULTI_SPACE = re.compile(r" +")
|
|
97
105
|
|
|
98
|
-
# Services that require the payload hash to travel in ``x-amz-content-sha256
|
|
99
|
-
#
|
|
100
|
-
|
|
106
|
+
# Services that require the payload hash to travel in ``x-amz-content-sha256``
|
|
107
|
+
# on every request, and that accept ``UNSIGNED-PAYLOAD`` for any operation.
|
|
108
|
+
# Other services sign the hash into the canonical request without sending it,
|
|
109
|
+
# and only see the header when the payload is left unsigned.
|
|
110
|
+
S3_SIGNING_NAMES = frozenset({"s3", "s3express", "s3-outposts", "s3-object-lambda"})
|
|
101
111
|
|
|
102
112
|
|
|
103
113
|
def _uri_encode(value: str) -> str:
|
|
@@ -231,12 +241,20 @@ def _canonical_query_from_pairs(pairs: list[tuple[str, str]]) -> str:
|
|
|
231
241
|
def sign_sigv4(
|
|
232
242
|
request: Request,
|
|
233
243
|
ctx: SigV4AuthContext,
|
|
234
|
-
|
|
244
|
+
*,
|
|
245
|
+
unsigned_payload: bool = False,
|
|
246
|
+
event_stream: bool = False,
|
|
235
247
|
) -> Request:
|
|
236
248
|
"""Return a new ``Request`` carrying SigV4 single-chunk auth headers.
|
|
237
249
|
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
The payload hash covers ``request.body``, which must be ``bytes`` or
|
|
251
|
+
``None``. ``unsigned_payload`` signs ``UNSIGNED-PAYLOAD`` instead and
|
|
252
|
+
forwards any body unchanged (streaming S3 requests and operations carrying
|
|
253
|
+
``aws.auth#unsignedPayload``). ``event_stream`` signs a request event
|
|
254
|
+
stream: the ``STREAMING-AWS4-HMAC-SHA256-EVENTS`` marker stands in for the
|
|
255
|
+
payload hash and the body — an iterator of encoded event messages — is
|
|
256
|
+
wrapped in an :class:`EventStreamIterator` that signs every event with a
|
|
257
|
+
signature chained from this request's.
|
|
240
258
|
"""
|
|
241
259
|
service = ctx["signing_name"]
|
|
242
260
|
region = ctx["signing_region"]
|
|
@@ -250,21 +268,31 @@ def sign_sigv4(
|
|
|
250
268
|
date_stamp = amz_date[:8]
|
|
251
269
|
else:
|
|
252
270
|
now = _amz_now()
|
|
253
|
-
amz_date = now.strftime(
|
|
271
|
+
amz_date = now.strftime(_AMZ_DATE_FORMAT)
|
|
254
272
|
date_stamp = now.strftime("%Y%m%d")
|
|
255
273
|
headers["X-Amz-Date"] = amz_date
|
|
256
274
|
|
|
257
|
-
# Payload hash. For S3-family services, x-amz-content-sha256 is mandatory
|
|
258
|
-
#
|
|
275
|
+
# Payload hash. For S3-family services, x-amz-content-sha256 is mandatory;
|
|
276
|
+
# for every service it is the only way to announce an unsigned payload or
|
|
277
|
+
# an event stream.
|
|
278
|
+
# Either way it must be set BEFORE computing the canonical request (it
|
|
279
|
+
# gets signed).
|
|
259
280
|
payload_hash = headers.get("X-Amz-Content-SHA256")
|
|
260
281
|
if payload_hash is None:
|
|
261
|
-
if
|
|
282
|
+
if event_stream:
|
|
283
|
+
payload_hash = _EVENT_STREAM_PAYLOAD
|
|
284
|
+
elif unsigned_payload:
|
|
262
285
|
payload_hash = "UNSIGNED-PAYLOAD"
|
|
286
|
+
elif request.body is None:
|
|
287
|
+
payload_hash = _EMPTY_PAYLOAD_SHA256
|
|
288
|
+
elif isinstance(request.body, bytes):
|
|
289
|
+
payload_hash = hashlib.sha256(request.body).hexdigest()
|
|
263
290
|
else:
|
|
264
|
-
|
|
265
|
-
|
|
291
|
+
raise TypeError(
|
|
292
|
+
"sign_sigv4 hashes a bytes body only; pass unsigned_payload=True "
|
|
293
|
+
"to send a streamed body without a payload hash"
|
|
266
294
|
)
|
|
267
|
-
if service in
|
|
295
|
+
if service in S3_SIGNING_NAMES or unsigned_payload or event_stream:
|
|
268
296
|
headers["X-Amz-Content-SHA256"] = payload_hash
|
|
269
297
|
|
|
270
298
|
# Session token (STS / assumed-role credentials).
|
|
@@ -308,18 +336,135 @@ def sign_sigv4(
|
|
|
308
336
|
f"Signature={signature}"
|
|
309
337
|
)
|
|
310
338
|
|
|
311
|
-
|
|
312
|
-
if
|
|
339
|
+
body = request.body
|
|
340
|
+
if event_stream:
|
|
341
|
+
if not isinstance(body, (Iterator, AsyncIterator)):
|
|
342
|
+
raise TypeError(
|
|
343
|
+
"event_stream requires an iterator of encoded events as the body"
|
|
344
|
+
)
|
|
345
|
+
body = EventStreamIterator(
|
|
346
|
+
cast("Iterator[bytes] | AsyncIterator[bytes]", body), ctx, signature
|
|
347
|
+
)
|
|
348
|
+
if body is not None:
|
|
313
349
|
return Request(
|
|
314
350
|
request.url,
|
|
315
351
|
request.method,
|
|
316
352
|
headers,
|
|
317
|
-
body=
|
|
353
|
+
body=body,
|
|
318
354
|
context=request.context,
|
|
319
355
|
)
|
|
320
356
|
return Request(request.url, request.method, headers, context=request.context)
|
|
321
357
|
|
|
322
358
|
|
|
359
|
+
def _sign_event(
|
|
360
|
+
ctx: SigV4AuthContext, prior_signature: str, payload: bytes
|
|
361
|
+
) -> tuple[bytes, str]:
|
|
362
|
+
"""Wrap one encoded event in a signed outer message.
|
|
363
|
+
|
|
364
|
+
Returns ``(frame, signature)``; the hex signature seeds the next event.
|
|
365
|
+
The string to sign chains every frame to its predecessor — the first one
|
|
366
|
+
to the request's ``Authorization`` signature — so the service can verify
|
|
367
|
+
the stream incrementally::
|
|
368
|
+
|
|
369
|
+
AWS4-HMAC-SHA256-PAYLOAD
|
|
370
|
+
<date, YYYYMMDDTHHMMSSZ>
|
|
371
|
+
<credential scope>
|
|
372
|
+
<prior signature, hex>
|
|
373
|
+
<hex sha256 of the encoded ``:date`` header>
|
|
374
|
+
<hex sha256 of the payload>
|
|
375
|
+
|
|
376
|
+
The AWS SDKs (and the service) use the ``-PAYLOAD`` algorithm name; the
|
|
377
|
+
Transcribe developer guide's pseudocode omits the suffix.
|
|
378
|
+
"""
|
|
379
|
+
service = ctx["signing_name"]
|
|
380
|
+
region = ctx["signing_region"]
|
|
381
|
+
now = _amz_now()
|
|
382
|
+
amz_date = now.strftime(_AMZ_DATE_FORMAT)
|
|
383
|
+
date_stamp = amz_date[:8]
|
|
384
|
+
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
|
385
|
+
date_header: dict[str, HeaderValue] = {":date": now}
|
|
386
|
+
string_to_sign = "\n".join(
|
|
387
|
+
(
|
|
388
|
+
_EVENT_ALGORITHM,
|
|
389
|
+
amz_date,
|
|
390
|
+
credential_scope,
|
|
391
|
+
prior_signature,
|
|
392
|
+
hashlib.sha256(encode_headers(date_header)).hexdigest(),
|
|
393
|
+
hashlib.sha256(payload).hexdigest(),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
signing_key = _derive_signing_key(
|
|
397
|
+
ctx["secret_access_key"], date_stamp, region, service
|
|
398
|
+
)
|
|
399
|
+
signature = hmac.new(
|
|
400
|
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
|
401
|
+
).hexdigest()
|
|
402
|
+
frame = Message(
|
|
403
|
+
{**date_header, ":chunk-signature": bytes.fromhex(signature)}, payload
|
|
404
|
+
).encode()
|
|
405
|
+
return frame, signature
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class EventStreamIterator(AnyIterator[bytes]):
|
|
409
|
+
"""Sign a request event stream, one frame per event.
|
|
410
|
+
|
|
411
|
+
Wraps an iterator of encoded event messages (as produced by the generated
|
|
412
|
+
``serialize_event_*`` functions). Each event becomes the payload of an
|
|
413
|
+
outer message carrying ``:date`` and ``:chunk-signature`` headers, the
|
|
414
|
+
signature chained from the previous frame's and seeded by the request's
|
|
415
|
+
``Authorization`` signature. Once the events are exhausted, one empty
|
|
416
|
+
signed frame is yielded to mark the end of the stream.
|
|
417
|
+
|
|
418
|
+
Like :class:`~capo._body.Body`, it is both a sync and an async iterator:
|
|
419
|
+
iterate it the way the wrapped source is iterable (``next`` over a sync
|
|
420
|
+
source, ``async for`` over an async one).
|
|
421
|
+
|
|
422
|
+
Reference:
|
|
423
|
+
https://docs.aws.amazon.com/transcribe/latest/dg/streaming-setting-up.html
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def __init__(
|
|
427
|
+
self,
|
|
428
|
+
events: Iterator[bytes] | AsyncIterator[bytes],
|
|
429
|
+
ctx: SigV4AuthContext,
|
|
430
|
+
seed_signature: str,
|
|
431
|
+
) -> None:
|
|
432
|
+
self._events = events
|
|
433
|
+
self._ctx = ctx
|
|
434
|
+
self._prior_signature = seed_signature
|
|
435
|
+
self._ended = False
|
|
436
|
+
|
|
437
|
+
def _frame(self, payload: bytes) -> bytes:
|
|
438
|
+
frame, self._prior_signature = _sign_event(
|
|
439
|
+
self._ctx, self._prior_signature, payload
|
|
440
|
+
)
|
|
441
|
+
return frame
|
|
442
|
+
|
|
443
|
+
def __next__(self) -> bytes:
|
|
444
|
+
if self._ended:
|
|
445
|
+
raise StopIteration
|
|
446
|
+
if not isinstance(self._events, Iterator):
|
|
447
|
+
raise TypeError("this event stream wraps an async source; use `async for`")
|
|
448
|
+
try:
|
|
449
|
+
payload = next(cast(Iterator[bytes], self._events))
|
|
450
|
+
except StopIteration:
|
|
451
|
+
self._ended = True
|
|
452
|
+
payload = b""
|
|
453
|
+
return self._frame(payload)
|
|
454
|
+
|
|
455
|
+
async def __anext__(self) -> bytes:
|
|
456
|
+
if self._ended:
|
|
457
|
+
raise StopAsyncIteration
|
|
458
|
+
if not isinstance(self._events, AsyncIterator):
|
|
459
|
+
raise TypeError("this event stream wraps a sync source; use `for`")
|
|
460
|
+
try:
|
|
461
|
+
payload = await cast(AsyncIterator[bytes], self._events).__anext__()
|
|
462
|
+
except StopAsyncIteration:
|
|
463
|
+
self._ended = True
|
|
464
|
+
payload = b""
|
|
465
|
+
return self._frame(payload)
|
|
466
|
+
|
|
467
|
+
|
|
323
468
|
def presign_sigv4(
|
|
324
469
|
request: Request,
|
|
325
470
|
ctx: SigV4AuthContext,
|
|
@@ -346,7 +491,7 @@ def presign_sigv4(
|
|
|
346
491
|
service = ctx["signing_name"]
|
|
347
492
|
region = ctx["signing_region"]
|
|
348
493
|
|
|
349
|
-
amz_date = sign_time.strftime(
|
|
494
|
+
amz_date = sign_time.strftime(_AMZ_DATE_FORMAT)
|
|
350
495
|
date_stamp = sign_time.strftime("%Y%m%d")
|
|
351
496
|
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
|
352
497
|
|
|
@@ -27,6 +27,7 @@ from capo_cloudtrail_data._rule_engine._endpoint_rule_set import EndpointParams,
|
|
|
27
27
|
from capo_cloudtrail_data._services._pipeline import (
|
|
28
28
|
AsyncOperationOptions,
|
|
29
29
|
OperationOptions,
|
|
30
|
+
raise_error,
|
|
30
31
|
)
|
|
31
32
|
from capo_cloudtrail_data.errors import UnknownServiceError
|
|
32
33
|
|
|
@@ -157,7 +158,7 @@ def put_audit_events(
|
|
|
157
158
|
try:
|
|
158
159
|
if response.status >= 300:
|
|
159
160
|
response.read()
|
|
160
|
-
|
|
161
|
+
raise_error(response, handle_error)
|
|
161
162
|
return handle_response(response), response
|
|
162
163
|
except BaseException:
|
|
163
164
|
response.close()
|
|
@@ -175,7 +176,7 @@ async def async_put_audit_events(
|
|
|
175
176
|
try:
|
|
176
177
|
if response.status >= 300:
|
|
177
178
|
await response.aread()
|
|
178
|
-
|
|
179
|
+
raise_error(response, handle_error)
|
|
179
180
|
return await async_handle_response(response), response
|
|
180
181
|
except BaseException:
|
|
181
182
|
await response.aclose()
|
|
@@ -89,7 +89,7 @@ def _decode_header_value(
|
|
|
89
89
|
raise ValueError(f"unsupported eventstream header type: {type_byte:#x}")
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def encode_headers(headers: dict[str, HeaderValue]) -> bytes:
|
|
93
93
|
parts: list[bytes] = []
|
|
94
94
|
for name, value in headers.items():
|
|
95
95
|
name_bytes = name.encode("utf-8")
|
|
@@ -124,10 +124,11 @@ class Message:
|
|
|
124
124
|
|
|
125
125
|
def encode(self) -> bytes:
|
|
126
126
|
"""Serialize this message into eventstream framing bytes."""
|
|
127
|
-
headers_bytes =
|
|
127
|
+
headers_bytes = encode_headers(self.headers)
|
|
128
128
|
headers_length = len(headers_bytes)
|
|
129
129
|
payload = self.payload
|
|
130
|
-
total_length
|
|
130
|
+
# total_length covers the whole frame, trailing message CRC included.
|
|
131
|
+
total_length = 12 + headers_length + len(payload) + 4
|
|
131
132
|
|
|
132
133
|
prelude = total_length.to_bytes(4, "big") + headers_length.to_bytes(4, "big")
|
|
133
134
|
prelude_crc = zlib.crc32(prelude).to_bytes(4, "big")
|
|
@@ -158,31 +159,31 @@ class MessageDecoder:
|
|
|
158
159
|
prelude = bytes(self._buffer[:8])
|
|
159
160
|
prelude_crc = int.from_bytes(self._buffer[8:12], "big")
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
# total_length covers the whole frame: 12-byte prelude, headers,
|
|
163
|
+
# payload, and the trailing 4-byte message CRC.
|
|
164
|
+
if total_length < 16 + headers_length:
|
|
162
165
|
raise ValueError(
|
|
163
166
|
f"invalid eventstream message: total_length={total_length} "
|
|
164
|
-
f"is smaller than minimum {
|
|
167
|
+
f"is smaller than minimum {16 + headers_length}"
|
|
165
168
|
)
|
|
166
169
|
|
|
167
|
-
|
|
168
|
-
if len(self._buffer) < message_end:
|
|
170
|
+
if len(self._buffer) < total_length:
|
|
169
171
|
return
|
|
170
172
|
|
|
171
173
|
computed_prelude_crc = zlib.crc32(prelude) & 0xFFFFFFFF
|
|
172
174
|
if computed_prelude_crc != prelude_crc:
|
|
173
175
|
raise ValueError("eventstream prelude checksum mismatch")
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
177
|
+
payload_end = total_length - 4
|
|
178
|
+
message_bytes = bytes(self._buffer[:payload_end])
|
|
179
|
+
message_crc = int.from_bytes(self._buffer[payload_end:total_length], "big")
|
|
179
180
|
computed_message_crc = zlib.crc32(message_bytes) & 0xFFFFFFFF
|
|
180
181
|
if computed_message_crc != message_crc:
|
|
181
182
|
raise ValueError("eventstream message checksum mismatch")
|
|
182
183
|
|
|
183
184
|
headers = _decode_headers(self._buffer[12 : 12 + headers_length])
|
|
184
|
-
payload = bytes(self._buffer[12 + headers_length :
|
|
185
|
-
del self._buffer[:
|
|
185
|
+
payload = bytes(self._buffer[12 + headers_length : payload_end])
|
|
186
|
+
del self._buffer[:total_length]
|
|
186
187
|
yield Message(headers=headers, payload=payload)
|
|
187
188
|
|
|
188
189
|
|
|
@@ -233,6 +234,7 @@ __all__ = [
|
|
|
233
234
|
"MessageDecoder",
|
|
234
235
|
"async_raw_stream_to_events",
|
|
235
236
|
"async_read_messages",
|
|
237
|
+
"encode_headers",
|
|
236
238
|
"raw_stream_to_events",
|
|
237
239
|
"read_messages",
|
|
238
240
|
]
|
|
@@ -6,6 +6,7 @@ from collections.abc import AsyncIterator, Iterator, Sequence
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING, Awaitable, Callable, Generic, TypeVar, cast
|
|
8
8
|
|
|
9
|
+
from typing_extensions import Never
|
|
9
10
|
from zapros import (
|
|
10
11
|
AsyncClient,
|
|
11
12
|
Client,
|
|
@@ -135,6 +136,22 @@ async def aexecute_pipeline(
|
|
|
135
136
|
return await make_chain(0)(request)
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def raise_error(response: Response, handle_error: Callable[[Response], Never]) -> Never:
|
|
140
|
+
try:
|
|
141
|
+
handle_error(response)
|
|
142
|
+
except ServiceError as exc:
|
|
143
|
+
# The response itself can say a retry is safe, even when the error is
|
|
144
|
+
# not marked @retryable: a 429, a transient 5xx or a Retry-After header.
|
|
145
|
+
if response.status == 429:
|
|
146
|
+
exc.is_throttling_error = True
|
|
147
|
+
if (
|
|
148
|
+
response.status in (429, 500, 502, 503, 504)
|
|
149
|
+
or "retry-after" in response.headers
|
|
150
|
+
):
|
|
151
|
+
exc.is_retryable = True
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
|
|
138
155
|
def _is_retryable(exc: Exception) -> bool:
|
|
139
156
|
if isinstance(exc, ServiceError):
|
|
140
157
|
return exc.is_retryable
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0/src/capo_cloudtrail_data.egg-info}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capo-cloudtrail-data
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for Cloudtrail Data.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python SDK for AWS Cloudtrail Data.
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -13,12 +13,20 @@ Vectors come from two sources:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import asyncio
|
|
17
|
+
import datetime as dt
|
|
18
|
+
import hashlib
|
|
19
|
+
import hmac
|
|
20
|
+
from collections.abc import AsyncIterator, Iterator
|
|
21
|
+
|
|
16
22
|
import pytest
|
|
17
23
|
|
|
18
24
|
from pywhatwgurl import URL
|
|
19
25
|
from zapros import Headers, Request
|
|
20
26
|
|
|
27
|
+
from capo_cloudtrail_data._auth import _sigv4
|
|
21
28
|
from capo_cloudtrail_data._auth._sigv4 import (
|
|
29
|
+
EventStreamIterator,
|
|
22
30
|
SigV4AuthContext,
|
|
23
31
|
build_sigv4_auth_scheme,
|
|
24
32
|
_build_canonical_request,
|
|
@@ -30,6 +38,7 @@ from capo_cloudtrail_data._auth._sigv4 import (
|
|
|
30
38
|
_uri_encode,
|
|
31
39
|
sign_sigv4,
|
|
32
40
|
)
|
|
41
|
+
from capo_cloudtrail_data._protocol.eventstream import Message, MessageDecoder, encode_headers
|
|
33
42
|
|
|
34
43
|
|
|
35
44
|
# ---------------------------------------------------------------------------
|
|
@@ -201,11 +210,16 @@ _TEST_SUITE_CTX: SigV4AuthContext = {
|
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
|
|
204
|
-
def _make_request(
|
|
213
|
+
def _make_request(
|
|
214
|
+
method: str,
|
|
215
|
+
url: str,
|
|
216
|
+
headers: dict[str, str],
|
|
217
|
+
body: bytes | Iterator[bytes] | AsyncIterator[bytes] | None = None,
|
|
218
|
+
) -> Request:
|
|
205
219
|
"""Build a Request and strip headers Request adds by default that the
|
|
206
220
|
AWS test suite does not include (Accept, User-Agent, Accept-Encoding).
|
|
207
221
|
"""
|
|
208
|
-
req = Request(URL(url), method, headers=headers)
|
|
222
|
+
req = Request(URL(url), method, headers=headers, body=body)
|
|
209
223
|
for h in ("accept", "user-agent", "accept-encoding"):
|
|
210
224
|
if h in req.headers:
|
|
211
225
|
del req.headers[h]
|
|
@@ -219,7 +233,7 @@ def test_get_vanilla():
|
|
|
219
233
|
"https://example.amazonaws.com/",
|
|
220
234
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
221
235
|
)
|
|
222
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
236
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
223
237
|
assert signed.headers["Authorization"] == (
|
|
224
238
|
"AWS4-HMAC-SHA256 "
|
|
225
239
|
"Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request,"
|
|
@@ -235,7 +249,7 @@ def test_get_vanilla_query():
|
|
|
235
249
|
"https://example.amazonaws.com/?Param1=value1",
|
|
236
250
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
237
251
|
)
|
|
238
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
252
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
239
253
|
assert signed.headers["Authorization"] == (
|
|
240
254
|
"AWS4-HMAC-SHA256 "
|
|
241
255
|
"Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request,"
|
|
@@ -255,7 +269,7 @@ def test_get_vanilla_query_order_key():
|
|
|
255
269
|
"https://example.amazonaws.com/?Param2=value2&Param1=value1",
|
|
256
270
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
257
271
|
)
|
|
258
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
272
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
259
273
|
assert signed.headers["Authorization"] == (
|
|
260
274
|
"AWS4-HMAC-SHA256 "
|
|
261
275
|
"Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request,"
|
|
@@ -271,7 +285,7 @@ def test_post_vanilla():
|
|
|
271
285
|
"https://example.amazonaws.com/",
|
|
272
286
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
273
287
|
)
|
|
274
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
288
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
275
289
|
assert signed.headers["Authorization"] == (
|
|
276
290
|
"AWS4-HMAC-SHA256 "
|
|
277
291
|
"Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request,"
|
|
@@ -308,7 +322,7 @@ def test_s3_get_object_example():
|
|
|
308
322
|
"X-Amz-Date": "20130524T000000Z",
|
|
309
323
|
},
|
|
310
324
|
)
|
|
311
|
-
signed = sign_sigv4(req, _S3_DOCS_CTX
|
|
325
|
+
signed = sign_sigv4(req, _S3_DOCS_CTX)
|
|
312
326
|
|
|
313
327
|
# Auto-injected payload hash for an empty body.
|
|
314
328
|
assert signed.headers["X-Amz-Content-SHA256"] == (
|
|
@@ -334,7 +348,7 @@ def test_session_token_added_and_signed():
|
|
|
334
348
|
"https://example.amazonaws.com/",
|
|
335
349
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
336
350
|
)
|
|
337
|
-
signed = sign_sigv4(req, ctx
|
|
351
|
+
signed = sign_sigv4(req, ctx)
|
|
338
352
|
assert signed.headers["X-Amz-Security-Token"] == "TOKEN123"
|
|
339
353
|
# x-amz-security-token must appear in the signed-headers list.
|
|
340
354
|
assert "x-amz-security-token" in signed.headers["Authorization"]
|
|
@@ -345,9 +359,9 @@ def test_s3_sets_payload_hash_for_body():
|
|
|
345
359
|
"PUT",
|
|
346
360
|
"https://examplebucket.s3.amazonaws.com/key",
|
|
347
361
|
{"Host": "examplebucket.s3.amazonaws.com", "X-Amz-Date": "20130524T000000Z"},
|
|
362
|
+
body=b"hello",
|
|
348
363
|
)
|
|
349
|
-
|
|
350
|
-
signed = sign_sigv4(req, _S3_DOCS_CTX, body)
|
|
364
|
+
signed = sign_sigv4(req, _S3_DOCS_CTX)
|
|
351
365
|
# sha256("hello")
|
|
352
366
|
assert signed.headers["X-Amz-Content-SHA256"] == (
|
|
353
367
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
|
@@ -380,8 +394,9 @@ def test_s3_outposts_put_with_encoded_key():
|
|
|
380
394
|
"Host": "s3-outposts.us-west-2.amazonaws.com",
|
|
381
395
|
"X-Amz-Date": "20130524T000000Z",
|
|
382
396
|
},
|
|
397
|
+
body=b"hello",
|
|
383
398
|
)
|
|
384
|
-
signed = sign_sigv4(req, _s3_family_ctx("s3-outposts", "us-west-2")
|
|
399
|
+
signed = sign_sigv4(req, _s3_family_ctx("s3-outposts", "us-west-2"))
|
|
385
400
|
assert signed.headers["X-Amz-Content-SHA256"] == (
|
|
386
401
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
|
387
402
|
)
|
|
@@ -400,7 +415,7 @@ def test_s3express_get_with_encoded_key():
|
|
|
400
415
|
f"https://{host}/a%20b?list-type=2",
|
|
401
416
|
{"Host": host, "X-Amz-Date": "20130524T000000Z"},
|
|
402
417
|
)
|
|
403
|
-
signed = sign_sigv4(req, _s3_family_ctx("s3express", "us-west-2")
|
|
418
|
+
signed = sign_sigv4(req, _s3_family_ctx("s3express", "us-west-2"))
|
|
404
419
|
assert signed.headers["X-Amz-Content-SHA256"] == (
|
|
405
420
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
406
421
|
)
|
|
@@ -419,7 +434,7 @@ def test_s3_object_lambda_get_with_encoded_key():
|
|
|
419
434
|
f"https://{host}/dir/a%20b",
|
|
420
435
|
{"Host": host, "X-Amz-Date": "20130524T000000Z"},
|
|
421
436
|
)
|
|
422
|
-
signed = sign_sigv4(req, _s3_family_ctx("s3-object-lambda", "us-east-1")
|
|
437
|
+
signed = sign_sigv4(req, _s3_family_ctx("s3-object-lambda", "us-east-1"))
|
|
423
438
|
assert signed.headers["X-Amz-Content-SHA256"] == (
|
|
424
439
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
425
440
|
)
|
|
@@ -438,19 +453,205 @@ def test_non_s3_does_not_set_payload_hash_header():
|
|
|
438
453
|
"https://iam.amazonaws.com/",
|
|
439
454
|
{"Host": "iam.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
440
455
|
)
|
|
441
|
-
signed = sign_sigv4(req, ctx
|
|
456
|
+
signed = sign_sigv4(req, ctx)
|
|
442
457
|
# IAM/STS/etc. signs the payload hash into the canonical request but does
|
|
443
458
|
# not transmit an X-Amz-Content-SHA256 header.
|
|
444
459
|
assert "X-Amz-Content-SHA256" not in signed.headers
|
|
445
460
|
|
|
446
461
|
|
|
462
|
+
def test_unsigned_payload_sends_header_for_non_s3():
|
|
463
|
+
"""``aws.auth#unsignedPayload`` on a non-S3 operation (Lex RecognizeUtterance):
|
|
464
|
+
the header is the only way the service learns not to hash the body."""
|
|
465
|
+
ctx: SigV4AuthContext = {**_TEST_SUITE_CTX, "signing_name": "lex"}
|
|
466
|
+
req = _make_request(
|
|
467
|
+
"POST",
|
|
468
|
+
"https://runtime-v2-lex.us-east-1.amazonaws.com/bots/b/utterance",
|
|
469
|
+
{
|
|
470
|
+
"Host": "runtime-v2-lex.us-east-1.amazonaws.com",
|
|
471
|
+
"X-Amz-Date": "20150830T123600Z",
|
|
472
|
+
},
|
|
473
|
+
body=iter([b"audio"]),
|
|
474
|
+
)
|
|
475
|
+
signed = sign_sigv4(req, ctx, unsigned_payload=True)
|
|
476
|
+
assert signed.headers["X-Amz-Content-SHA256"] == "UNSIGNED-PAYLOAD"
|
|
477
|
+
assert "x-amz-content-sha256" in signed.headers["Authorization"].split("SignedHeaders=")[1]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_streamed_body_rejected_without_unsigned_payload():
|
|
481
|
+
req = _make_request(
|
|
482
|
+
"PUT",
|
|
483
|
+
"https://example.amazonaws.com/",
|
|
484
|
+
{"Host": "example.amazonaws.com"},
|
|
485
|
+
body=iter([b"chunk"]),
|
|
486
|
+
)
|
|
487
|
+
with pytest.raises(TypeError):
|
|
488
|
+
sign_sigv4(req, _TEST_SUITE_CTX)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ---------------------------------------------------------------------------
|
|
492
|
+
# Request event streams (Transcribe StartStreamTranscription & co.)
|
|
493
|
+
#
|
|
494
|
+
# Frame vectors computed with the amazon-transcribe-streaming-sdk EventSigner
|
|
495
|
+
# for the same credentials, clock and seed signature.
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
_EVENT_CTX: SigV4AuthContext = {**_TEST_SUITE_CTX, "signing_name": "transcribe"}
|
|
499
|
+
_EVENT_NOW = dt.datetime(2015, 8, 30, 12, 36, 0, 123000, tzinfo=dt.timezone.utc)
|
|
500
|
+
_SEED_SIGNATURE = "5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31"
|
|
501
|
+
_EXPECTED_CHUNK_SIGNATURES = [
|
|
502
|
+
"790059050e9798603e8e42a1ad7d6722d26b05ec6b02d78e6337e5b8c3c41c90",
|
|
503
|
+
"137143c4f956f3fe0ea64f7b27848aa1b9168e770143b06b8a78723c6d0c62c5",
|
|
504
|
+
"cb01529bff77188ec0f881ecdccab5e237925b6a14357c1ef10f873b4fc8793a",
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _audio_events() -> list[bytes]:
|
|
509
|
+
headers = {
|
|
510
|
+
":message-type": "event",
|
|
511
|
+
":event-type": "AudioEvent",
|
|
512
|
+
":content-type": "application/octet-stream",
|
|
513
|
+
}
|
|
514
|
+
return [
|
|
515
|
+
Message(headers, b"\x01\x02\x03").encode(),
|
|
516
|
+
Message(headers, b"").encode(),
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _decode_frames(frames: list[bytes]) -> list[Message]:
|
|
521
|
+
decoder = MessageDecoder()
|
|
522
|
+
return [message for frame in frames for message in decoder.feed(frame)]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _chunk_signature(message: Message) -> str:
|
|
526
|
+
signature = message.headers[":chunk-signature"]
|
|
527
|
+
assert isinstance(signature, bytes) and len(signature) == 32
|
|
528
|
+
return signature.hex()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _expected_event_signature(prior: str, payload: bytes) -> str:
|
|
532
|
+
"""The per-frame string to sign, straight from the Transcribe developer
|
|
533
|
+
guide (with the ``-PAYLOAD`` algorithm name every AWS SDK uses)."""
|
|
534
|
+
string_to_sign = "\n".join(
|
|
535
|
+
[
|
|
536
|
+
"AWS4-HMAC-SHA256-PAYLOAD",
|
|
537
|
+
"20150830T123600Z",
|
|
538
|
+
"20150830/us-east-1/transcribe/aws4_request",
|
|
539
|
+
prior,
|
|
540
|
+
hashlib.sha256(encode_headers({":date": _EVENT_NOW})).hexdigest(),
|
|
541
|
+
hashlib.sha256(payload).hexdigest(),
|
|
542
|
+
]
|
|
543
|
+
)
|
|
544
|
+
key = _derive_signing_key(
|
|
545
|
+
_EVENT_CTX["secret_access_key"], "20150830", "us-east-1", "transcribe"
|
|
546
|
+
)
|
|
547
|
+
return hmac.new(key, string_to_sign.encode(), hashlib.sha256).hexdigest()
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def test_event_stream_frames_chain_from_seed_signature(monkeypatch):
|
|
551
|
+
monkeypatch.setattr(_sigv4, "_amz_now", lambda: _EVENT_NOW)
|
|
552
|
+
events = _audio_events()
|
|
553
|
+
|
|
554
|
+
frames = list(EventStreamIterator(iter(events), _EVENT_CTX, _SEED_SIGNATURE))
|
|
555
|
+
messages = _decode_frames(frames)
|
|
556
|
+
|
|
557
|
+
# One frame per event, plus the empty end frame.
|
|
558
|
+
assert [m.payload for m in messages] == [*events, b""]
|
|
559
|
+
prior = _SEED_SIGNATURE
|
|
560
|
+
for message, payload, expected in zip(
|
|
561
|
+
messages, [*events, b""], _EXPECTED_CHUNK_SIGNATURES
|
|
562
|
+
):
|
|
563
|
+
assert set(message.headers) == {":date", ":chunk-signature"}
|
|
564
|
+
assert message.headers[":date"] == _EVENT_NOW
|
|
565
|
+
signature = _chunk_signature(message)
|
|
566
|
+
assert signature == _expected_event_signature(prior, payload) == expected
|
|
567
|
+
prior = signature
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def test_event_stream_empty_source_still_sends_end_frame(monkeypatch):
|
|
571
|
+
monkeypatch.setattr(_sigv4, "_amz_now", lambda: _EVENT_NOW)
|
|
572
|
+
frames = list(EventStreamIterator(iter([]), _EVENT_CTX, _SEED_SIGNATURE))
|
|
573
|
+
(message,) = _decode_frames(frames)
|
|
574
|
+
assert message.payload == b""
|
|
575
|
+
assert message.headers[":chunk-signature"] == bytes.fromhex(
|
|
576
|
+
_expected_event_signature(_SEED_SIGNATURE, b"")
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_async_event_stream_matches_sync(monkeypatch):
|
|
581
|
+
monkeypatch.setattr(_sigv4, "_amz_now", lambda: _EVENT_NOW)
|
|
582
|
+
events = _audio_events()
|
|
583
|
+
|
|
584
|
+
async def source() -> AsyncIterator[bytes]:
|
|
585
|
+
for event in events:
|
|
586
|
+
yield event
|
|
587
|
+
|
|
588
|
+
async def main() -> list[bytes]:
|
|
589
|
+
return [
|
|
590
|
+
f async for f in EventStreamIterator(source(), _EVENT_CTX, _SEED_SIGNATURE)
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
frames = asyncio.run(main())
|
|
594
|
+
assert frames == list(EventStreamIterator(iter(events), _EVENT_CTX, _SEED_SIGNATURE))
|
|
595
|
+
assert [_chunk_signature(m) for m in _decode_frames(frames)] == _EXPECTED_CHUNK_SIGNATURES
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def test_event_stream_request_signs_marker_and_wraps_body(monkeypatch):
|
|
599
|
+
"""A request event stream signs the ``STREAMING-AWS4-HMAC-SHA256-EVENTS``
|
|
600
|
+
marker, sends it in the header, and wraps the body so every event is
|
|
601
|
+
signed with a chain seeded by the Authorization signature."""
|
|
602
|
+
monkeypatch.setattr(_sigv4, "_amz_now", lambda: _EVENT_NOW)
|
|
603
|
+
events = _audio_events()
|
|
604
|
+
req = _make_request(
|
|
605
|
+
"POST",
|
|
606
|
+
"https://transcribestreaming.us-east-1.amazonaws.com/stream-transcription",
|
|
607
|
+
{
|
|
608
|
+
"Host": "transcribestreaming.us-east-1.amazonaws.com",
|
|
609
|
+
"X-Amz-Date": "20150830T123600Z",
|
|
610
|
+
},
|
|
611
|
+
body=iter(events),
|
|
612
|
+
)
|
|
613
|
+
signed = sign_sigv4(req, _EVENT_CTX, event_stream=True)
|
|
614
|
+
assert signed.headers["X-Amz-Content-SHA256"] == "STREAMING-AWS4-HMAC-SHA256-EVENTS"
|
|
615
|
+
assert "x-amz-content-sha256" in signed.headers["Authorization"].split("SignedHeaders=")[1]
|
|
616
|
+
seed = signed.headers["Authorization"].rsplit("Signature=", 1)[1]
|
|
617
|
+
|
|
618
|
+
assert isinstance(signed.body, EventStreamIterator)
|
|
619
|
+
messages = _decode_frames(list(signed.body))
|
|
620
|
+
assert [m.payload for m in messages] == [*events, b""]
|
|
621
|
+
assert messages[0].headers[":chunk-signature"] == bytes.fromhex(
|
|
622
|
+
_expected_event_signature(seed, events[0])
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def test_event_stream_request_wraps_async_body():
|
|
627
|
+
async def source() -> AsyncIterator[bytes]:
|
|
628
|
+
yield b""
|
|
629
|
+
|
|
630
|
+
req = _make_request(
|
|
631
|
+
"POST",
|
|
632
|
+
"https://transcribestreaming.us-east-1.amazonaws.com/stream-transcription",
|
|
633
|
+
{"Host": "transcribestreaming.us-east-1.amazonaws.com"},
|
|
634
|
+
body=source(),
|
|
635
|
+
)
|
|
636
|
+
signed = sign_sigv4(req, _EVENT_CTX, event_stream=True)
|
|
637
|
+
assert isinstance(signed.body, EventStreamIterator)
|
|
638
|
+
assert isinstance(signed.body, AsyncIterator)
|
|
639
|
+
with pytest.raises(TypeError):
|
|
640
|
+
next(signed.body)
|
|
641
|
+
assert len(asyncio.run(_collect(signed.body))) == 2
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
async def _collect(source: AsyncIterator[bytes]) -> list[bytes]:
|
|
645
|
+
return [chunk async for chunk in source]
|
|
646
|
+
|
|
647
|
+
|
|
447
648
|
def test_amz_date_autopopulated_when_missing():
|
|
448
649
|
req = _make_request(
|
|
449
650
|
"GET",
|
|
450
651
|
"https://example.amazonaws.com/",
|
|
451
652
|
{"Host": "example.amazonaws.com"},
|
|
452
653
|
)
|
|
453
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
654
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
454
655
|
amz_date = signed.headers["X-Amz-Date"]
|
|
455
656
|
# Format: YYYYMMDDTHHMMSSZ
|
|
456
657
|
assert len(amz_date) == 16
|
|
@@ -464,7 +665,7 @@ def test_authorization_header_format():
|
|
|
464
665
|
"https://example.amazonaws.com/",
|
|
465
666
|
{"Host": "example.amazonaws.com", "X-Amz-Date": "20150830T123600Z"},
|
|
466
667
|
)
|
|
467
|
-
signed = sign_sigv4(req, _TEST_SUITE_CTX
|
|
668
|
+
signed = sign_sigv4(req, _TEST_SUITE_CTX)
|
|
468
669
|
auth = signed.headers["Authorization"]
|
|
469
670
|
assert auth.startswith("AWS4-HMAC-SHA256 ")
|
|
470
671
|
# Spec: comma-separated, no required whitespace between parts.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/__init__.py
RENAMED
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_pagination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/_protocol/xml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/errors/_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capo_cloudtrail_data-0.2.0 → capo_cloudtrail_data-0.4.0}/src/capo_cloudtrail_data/types/uuid.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|