salesforce-data-customcode 6.1.0.dev6__py3-none-any.whl → 6.1.0.dev7__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.
@@ -19,11 +19,15 @@ Inject external credential auth into an outgoing request.
19
19
  from __future__ import annotations
20
20
 
21
21
  import base64
22
+ import datetime
23
+ import hashlib
24
+ import hmac
22
25
  from typing import (
23
26
  TYPE_CHECKING,
24
27
  Any,
25
28
  Dict,
26
29
  )
30
+ import urllib.parse
27
31
 
28
32
  from requests.auth import AuthBase
29
33
 
@@ -32,6 +36,8 @@ from datacustomcode.named_credential.direct.credentials import AuthType
32
36
  if TYPE_CHECKING:
33
37
  from requests.models import PreparedRequest
34
38
 
39
+ _SIGV4_ALGORITHM = "AWS4-HMAC-SHA256"
40
+
35
41
 
36
42
  class DynamicAuthHandler(AuthBase):
37
43
  def __init__(self, cred_config: Dict[str, Any]) -> None:
@@ -57,7 +63,129 @@ class DynamicAuthHandler(AuthBase):
57
63
  )
58
64
  request.headers["Authorization"] = f"Bearer {bearer}"
59
65
 
66
+ elif self.auth_type == AuthType.AWS_SIG_V4.value:
67
+ self._sign_aws_sigv4(request)
68
+
60
69
  else:
61
70
  raise ValueError(f"Unsupported auth_type '{self.auth_type}'.")
62
71
 
63
72
  return request
73
+
74
+ def _sign_aws_sigv4(self, request: PreparedRequest) -> None:
75
+ """Sign ``request`` with AWS Signature Version 4.
76
+
77
+ Requires ``aws_access_key_id``, ``aws_secret_access_key``, ``aws_region``,
78
+ and ``aws_service`` in the credential config; ``aws_session_token`` is
79
+ optional (for temporary credentials). The signed date, payload hash, and
80
+ (when present) session token are added as ``x-amz-*`` headers so the sent
81
+ request matches what was signed.
82
+ """
83
+ access_key = self.config.get("aws_access_key_id")
84
+ secret_key = self.config.get("aws_secret_access_key")
85
+ region = self.config.get("aws_region")
86
+ service = self.config.get("aws_service")
87
+ session_token = self.config.get("aws_session_token")
88
+ missing = [
89
+ name
90
+ for name, value in (
91
+ ("aws_access_key_id", access_key),
92
+ ("aws_secret_access_key", secret_key),
93
+ ("aws_region", region),
94
+ ("aws_service", service),
95
+ )
96
+ if not value
97
+ ]
98
+ if missing:
99
+ raise ValueError(f"'{self.auth_type}' auth requires {', '.join(missing)}.")
100
+ access_key = str(access_key)
101
+ secret_key = str(secret_key)
102
+ region = str(region)
103
+ service = str(service)
104
+
105
+ parsed = urllib.parse.urlsplit(str(request.url or ""))
106
+ host = parsed.netloc
107
+ canonical_uri = urllib.parse.quote(parsed.path or "/", safe="/-_.~")
108
+ canonical_query = _canonical_query_string(parsed.query)
109
+
110
+ body = request.body or b""
111
+ if isinstance(body, str):
112
+ body = body.encode("utf-8")
113
+ payload_hash = hashlib.sha256(body).hexdigest()
114
+
115
+ now = datetime.datetime.now(datetime.timezone.utc)
116
+ amz_date = now.strftime("%Y%m%dT%H%M%SZ")
117
+ datestamp = now.strftime("%Y%m%d")
118
+
119
+ request.headers["x-amz-date"] = amz_date
120
+ request.headers["x-amz-content-sha256"] = payload_hash
121
+ if session_token:
122
+ request.headers["x-amz-security-token"] = session_token
123
+
124
+ signed = {
125
+ "host": host,
126
+ "x-amz-content-sha256": payload_hash,
127
+ "x-amz-date": amz_date,
128
+ }
129
+ if session_token:
130
+ signed["x-amz-security-token"] = session_token
131
+ signed_headers = ";".join(sorted(signed))
132
+ canonical_headers = "".join(
133
+ f"{name}:{signed[name]}\n" for name in sorted(signed)
134
+ )
135
+
136
+ canonical_request = "\n".join(
137
+ [
138
+ request.method or "GET",
139
+ canonical_uri,
140
+ canonical_query,
141
+ canonical_headers,
142
+ signed_headers,
143
+ payload_hash,
144
+ ]
145
+ )
146
+ credential_scope = f"{datestamp}/{region}/{service}/aws4_request"
147
+ string_to_sign = "\n".join(
148
+ [
149
+ _SIGV4_ALGORITHM,
150
+ amz_date,
151
+ credential_scope,
152
+ hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
153
+ ]
154
+ )
155
+ signing_key = _derive_signing_key(secret_key, datestamp, region, service)
156
+ signature = hmac.new(
157
+ signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
158
+ ).hexdigest()
159
+
160
+ request.headers["Authorization"] = (
161
+ f"{_SIGV4_ALGORITHM} Credential={access_key}/{credential_scope}, "
162
+ f"SignedHeaders={signed_headers}, Signature={signature}"
163
+ )
164
+
165
+
166
+ def _canonical_query_string(query: str) -> str:
167
+ """Build the AWS Sig V4 canonical query string from a raw query string."""
168
+ pairs = urllib.parse.parse_qsl(query, keep_blank_values=True)
169
+ encoded = [
170
+ (
171
+ urllib.parse.quote(key, safe="-_.~"),
172
+ urllib.parse.quote(value, safe="-_.~"),
173
+ )
174
+ for key, value in pairs
175
+ ]
176
+ encoded.sort()
177
+ return "&".join(f"{key}={value}" for key, value in encoded)
178
+
179
+
180
+ def _derive_signing_key(
181
+ secret_key: str, datestamp: str, region: str, service: str
182
+ ) -> bytes:
183
+ """Derive the AWS Sig V4 signing key via the chained HMAC-SHA256 sequence."""
184
+
185
+ def _hmac(key: bytes, msg: str) -> bytes:
186
+ return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
187
+
188
+ k_date = _hmac(f"AWS4{secret_key}".encode(), datestamp)
189
+ k_region = _hmac(k_date, region)
190
+ k_service = _hmac(k_region, service)
191
+ return _hmac(k_service, "aws4_request")
@@ -41,6 +41,7 @@ CREDENTIAL_FILE_ENV_VAR = "DATACUSTOMCODE_EXTERNAL_CALLOUT_CONFIG"
41
41
  class AuthType(str, Enum):
42
42
  """External Credential auth types supported by External Services."""
43
43
 
44
+ AWS_SIG_V4 = "AwsSv4"
44
45
  BASIC = "Basic"
45
46
  CUSTOM = "Custom"
46
47
  JWT = "Jwt"
@@ -122,7 +122,7 @@ matching entries in `config.json`) to point at your own DLOs.
122
122
  `auth_type` selects how auth is injected for local testing. It should mirror the
123
123
  External Credential your Named Credential uses in the org, so local and deployed
124
124
  runs behave the same. This example uses `Custom` (Gemini's `X-goog-api-key`);
125
- all four supported types:
125
+ all supported types:
126
126
 
127
127
  | `auth_type` | Fields read | Header sent |
128
128
  | ----------- | ------------------------------- | --------------------------------------- |
@@ -130,7 +130,11 @@ all four supported types:
130
130
  | `Custom` | `custom_headers` (sent verbatim)| the headers you list |
131
131
  | `OAuth` | `access_token` or `token` | `Authorization: Bearer <token>` |
132
132
  | `Jwt` | `access_token` or `token` | `Authorization: Bearer <token>` |
133
+ | `AwsSv4` | `aws_access_key_id`, `aws_secret_access_key`, `aws_region`, `aws_service`, optional `aws_session_token` | `Authorization: AWS4-HMAC-SHA256 ...` plus `x-amz-date` / `x-amz-content-sha256` (and `x-amz-security-token` when a session token is set) |
133
134
 
134
135
  `OAuth`/`Jwt` take a token you supply for the local run — the SDK does not fetch
135
136
  or refresh it. In the Data Cloud runtime the Named Credential handles token
136
137
  acquisition; this local config only stands in for that during testing.
138
+
139
+ `AwsSv4` signs the request with AWS Signature Version 4 using the keys you
140
+ supply, mirroring an AWS Signature Version 4 External Credential in the org.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 6.1.0.dev6
3
+ Version: 6.1.0.dev7
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -57,8 +57,8 @@ datacustomcode/named_credential/__init__.py,sha256=XD_UI8ggOkFvkqCOZI_3dP1AkviqP
57
57
  datacustomcode/named_credential/base.py,sha256=-LMskZkKNoaFg-Zc80M_4IPLF7ru5pfnbkfLI7YXrFc,1816
58
58
  datacustomcode/named_credential/default.py,sha256=z03B0daZvTPiTmJWGAtDCICB1srelESg7QmPH2iRK5o,3339
59
59
  datacustomcode/named_credential/direct/__init__.py,sha256=zqErMI1GqEhseLnYkrWtq3WipRi_dP153c8wwLvgI-o,799
60
- datacustomcode/named_credential/direct/auth.py,sha256=U09jO0Pn3OEOEudmvcBmtkwoXqE0Ae_Mv6TAMqb3eG4,2201
61
- datacustomcode/named_credential/direct/credentials.py,sha256=fibIZTyBT1IbAGFjXwAXqYcQt9o_sB8zFmuixGmibE4,4534
60
+ datacustomcode/named_credential/direct/auth.py,sha256=I1vdB8zVxdREwABDnjY7qwpTZFhT9OHJTsgpxPIuT84,6837
61
+ datacustomcode/named_credential/direct/credentials.py,sha256=B1UpqwRXnzkjycUi50KfxJA5ZeAhR33gElRMzNOit4M,4560
62
62
  datacustomcode/named_credential/direct/transport.py,sha256=UL5eJJsyoAjr8ovJDQKQY3O20DivvbWFPGRyM8Y2or4,4078
63
63
  datacustomcode/named_credential/direct/url_resolver.py,sha256=KItv12Az0qoxnVacCTcM9t9AY7qB9_tqPWMgiP-7Y6s,3823
64
64
  datacustomcode/named_credential/errors.py,sha256=zyMEgljTf85digXrLrlR9HBh5Un4WtG0WsZx0K4EWt0,1212
@@ -111,7 +111,7 @@ datacustomcode/templates/script/account.ipynb,sha256=LIbxgiVxflNASdspF2lfpMKkKAT
111
111
  datacustomcode/templates/script/build_native_dependencies.sh,sha256=ICRrp4f1ATBwPaUiuVGjY-MwiubFswNJLf8gMGU6YNg,197
112
112
  datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv,sha256=C7ggLBfoyi3M2BdMLNyOeKqF-5OO-Da76lkwgWg2cVQ,302
113
113
  datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py,sha256=Mfm3iQtEHTQRW6cmZTwRKdTh3IeGWR-teXrd6x-YLSY,2223
114
- datacustomcode/templates/script/examples/external_callout/README.md,sha256=9pcepHizQxsTduNUjDF4GkkB8nM4dke9353mJxCZzjM,5646
114
+ datacustomcode/templates/script/examples/external_callout/README.md,sha256=CNLPSS6anLxRQyZ6hxAXUuvUPH09HMv2GPoQQMsuT94,6056
115
115
  datacustomcode/templates/script/examples/external_callout/entrypoint.py,sha256=KgSAmkpJ1On7R3Lr4_Slf9rTHgLr3TjC43QSFbaIzUY,5005
116
116
  datacustomcode/templates/script/examples/external_callout/external_callout_config.json,sha256=PUCmw_humbVoCyswFneuGYx0INbqSTPOumDqtGOztiM,320
117
117
  datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py,sha256=UQr1EhJURFFGI5626efvv-MxGHltzcpJcEJVmz5ZQdo,2333
@@ -122,8 +122,8 @@ datacustomcode/templates/script/requirements-dev.txt,sha256=OWwuy1awesqOZOS3Zizm
122
122
  datacustomcode/templates/script/requirements.txt,sha256=qJbqzs5z0Hrx590U3T7dHq_m8UQEx2TdhgrJSz-QHIQ,40
123
123
  datacustomcode/token_provider.py,sha256=qA_e4vqSXnxn0MccFFPULWO8ZeUOEAu5QpBnoYCg-fc,6839
124
124
  datacustomcode/version.py,sha256=9LlbVrzwBvut1L308QbwNBgxdQk5CrghH39JARVSxms,989
125
- salesforce_data_customcode-6.1.0.dev6.dist-info/METADATA,sha256=-7Xy6AdRhwyLbyCMCZqHQ8gbnuvdXUz744Ql0ZCE2Tw,27417
126
- salesforce_data_customcode-6.1.0.dev6.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
127
- salesforce_data_customcode-6.1.0.dev6.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
128
- salesforce_data_customcode-6.1.0.dev6.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
129
- salesforce_data_customcode-6.1.0.dev6.dist-info/RECORD,,
125
+ salesforce_data_customcode-6.1.0.dev7.dist-info/METADATA,sha256=jzc55cJlMHH-nGsNSzIrLu-GhN4nlmIOfK9fj8nZ1u4,27417
126
+ salesforce_data_customcode-6.1.0.dev7.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
127
+ salesforce_data_customcode-6.1.0.dev7.dist-info/entry_points.txt,sha256=WpQ94UB7UuRCYGOLtJV3vAgm1tl7iVf43VYRWIuNu5Y,74
128
+ salesforce_data_customcode-6.1.0.dev7.dist-info/licenses/LICENSE.txt,sha256=iOi8EmQpfkFhMENi7VYtkl2EqS14pEOccoXHiW2dyPU,11443
129
+ salesforce_data_customcode-6.1.0.dev7.dist-info/RECORD,,