Setting the file. One moment. Register · AWS Marketplace Metering · aws/agent-toolkit-for-aws · Skills DocsQuery Patterns
70
Creating Amazon Aurora Db Cluster With Instances
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
scripts/register.py
Python·269 lines·12 KB
,
"us-east-1"
)
13)
14subscribers_table = dynamodb.Table(os.environ["SUBSCRIBERS_TABLE"])
15mp_client = boto3.client("meteringmarketplace", region_name="us-east-1")
16
17# In-region customer-profile table (buyer PII). Uses the Lambda's OWN region (no region_name
18# override) so PII stays in-region — never written to the us-east-1 subscribers table.
19_dynamodb_local = boto3.resource("dynamodb")
20_CUSTOMER_PROFILE_TABLE = os.environ.get("CUSTOMER_PROFILE_TABLE")
21customer_profile_table = (
22 _dynamodb_local.Table(_CUSTOMER_PROFILE_TABLE) if _CUSTOMER_PROFILE_TABLE else None
23)
24
25PRODUCT_CODE = os.environ["PRODUCT_CODE"]
26MAX_TOKEN_LENGTH = 16384
27
28# Subset of ALLOWED_REGISTRATION_FIELDS the seller PROMOTED to top-level attributes on the
29# customer-profile table (each has a matching GSI). Promoted fields are written BOTH into the
30# registrationData map AND as a top-level attribute so a GSI can look profiles up by them.
31PROMOTED_PROFILE_FIELDS = [
32 f.strip() for f in os.environ.get("PROMOTED_PROFILE_FIELDS", "").split(",") if f.strip()
33]
34
35# The AWS Region this register Lambda runs in — added to the subscriber row's
36# registeredRegions String Set (BatchMeterUsage is regional, so a buyer may be resolved in
37# multiple regions). Lambda sets AWS_REGION automatically.
38INVOCATION_REGION = os.environ.get("AWS_REGION", "")
39
40# Registration input allowlist + bounds. The endpoint is public and unauthenticated, so
41# only these named fields are persisted, each length-bounded, up to a max field count.
42# TODO(seller): set ALLOWED_REGISTRATION_FIELDS (comma-separated) to YOUR form fields.
43ALLOWED_REGISTRATION_FIELDS = [
44 f.strip() for f in os.environ.get("ALLOWED_REGISTRATION_FIELDS", "").split(",") if f.strip()
45]
46MAX_REGISTRATION_FIELDS = int(os.environ.get("MAX_REGISTRATION_FIELDS", "20"))
47MAX_FIELD_VALUE_LENGTH = int(os.environ.get("MAX_FIELD_VALUE_LENGTH", "512"))
48
49
50def _mask(value):
51 """Mask a sensitive identifier for logging (ILER-R1a): keep only the last 4 chars.
52 Buyer account IDs / license ARNs are sensitive and MUST NOT be logged in full."""
53 if not value:
54 return "<none>"
55 s = str(value)
56 return "****" + s[-4:] if len(s) > 4 else "****"
57
58
59def _stamp(update_expr, values):
60 """Append createdAt(once)/updatedAt audit stamps to a SET UpdateExpression (createdAt once, updatedAt every write)."""
61 from datetime import datetime, timezone
62
63 values = dict(values)
64 values[":now"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
65 return (
66 f"{update_expr}, createdAt = if_not_exists(createdAt, :now), updatedAt = :now",
67 values,
68 )
69
70
71def handler(event, context):
72 """Handle buyer registration: resolve the fulfillment token, upsert the subscriber.
73
74 Flow:
75 1. Buyer clicks "Set up your account" in AWS Marketplace after subscribing.
76 2. AWS Marketplace POSTs to our registration URL with x-amzn-marketplace-token.
77 3. We call ResolveCustomer to get CustomerAWSAccountId + LicenseArn (+ ProductCode).
78 The registration token is reusable until it expires (~4 hours), not one-time-use.
79 4. We upsert the buyer's profile row in the IN-REGION customer-profile table, keyed by
80 licenseArn + customerAWSAccountId — ALWAYS (a registered buyer has an in-region profile
81 even with no extra form fields), setting productCode plus, when the seller collected
82 allowlisted fields, the registrationData map + any promoted top-level fields. We upsert
83 the PII-FREE subscriber row (us-east-1) with only productCode + this Lambda's AWS Region
84 appended to registeredRegions (idempotent). NO registration PII is written to the
85 subscribers table.
86
87 registeredRegions is reference-only and NEVER gates metering (metering is driven by
88 the usage table's metering_pending GSI). CustomerIdentifier is a deprecated legacy
89 field and is NOT persisted for a new integration.
90
91 Note: this script is for NEW skill-generated integrations. For an EXISTING seller
92 integration, do NOT use this script — read the seller's existing registration code
93 and adapt guidance to it.
94 """
95 body = event.get("body", "")
96 if event.get("isBase64Encoded"):
97 import base64
98
99 body = base64.b64decode(body).decode("utf-8")
100
101 if not body or len(body) > MAX_TOKEN_LENGTH:
102 return _response(400, {"error": "Invalid request body"})
103
104 form = urllib.parse.parse_qs(body)
105 token = form.get("x-amzn-marketplace-token", [None])[0]
106
107 if not token or not token.strip():
108 return _response(400, {"error": "Missing x-amzn-marketplace-token"})
109
110 try:
111 result = mp_client.resolve_customer(RegistrationToken=token)
112 except mp_client.exceptions.ExpiredTokenException:
113 return _response(
114 400,
115 {
116 "error": "Registration token expired. Please return to AWS Marketplace and "
117 "click Set up your account again."
118 },
119 )
120 except mp_client.exceptions.InvalidTokenException:
121 return _response(400, {"error": "Invalid registration token"})
122 except Exception as e:
123 logger.error(f"ResolveCustomer failed: {type(e).__name__}")
124 return _response(500, {"error": "Failed to resolve customer. Please try again."})
125
126 customer_aws_account_id = result.get("CustomerAWSAccountId")
127 license_arn = result.get("LicenseArn")
128 product_code = result.get("ProductCode")
129
130 if not customer_aws_account_id or not product_code:
131 logger.error("ResolveCustomer response missing required fields")
132 return _response(500, {"error": "Failed to resolve customer. Please try again."})
133
134 if product_code != PRODUCT_CODE:
135 logger.error(f"Product code mismatch: received={product_code} expected={PRODUCT_CODE}")
136 return _response(400, {"error": "Product code mismatch"})
137
138 if not license_arn:
139 # A Concurrent Agreements ResolveCustomer returns a LicenseArn. Without one we
140 # cannot key the subscriber row by the real licenseArn; surface the error rather
141 # than inventing a placeholder. (A `License Updated` event will create the row.)
142 logger.error(
143 f"ResolveCustomer returned no LicenseArn for account={_mask(customer_aws_account_id)}; "
144 "cannot persist a keyed subscriber row (a License Updated event will create it)"
145 )
146 return _response(
147 200,
148 {"message": "Registration received; your account is being activated."},
149 )
150
151 # 1) Buyer profile → the IN-REGION customer-profile table, keyed by the resolved
152 # identity (licenseArn + customerAWSAccountId). We ALWAYS upsert this row — a buyer that
153 # registered has an in-region profile record even if the seller collected no extra form
154 # fields — and additionally set the registrationData map + any promoted top-level fields
155 # WHEN there are allowlisted fields to store. Never written to the us-east-1 subscribers table.
156 registration_data = _extract_registration_fields(form)
157 if customer_profile_table is None:
158 # No profile store configured. In this reference (new-stack) handler the template always
159 # sets CUSTOMER_PROFILE_TABLE, so this indicates a misconfiguration — fail loud (500)
160 # rather than silently skip persisting the buyer's in-region profile. (An EXISTING
161 # seller's own handler may store registration data differently — the skill adapts to
162 # that; this reference script is not used verbatim there.)
163 logger.error("CUSTOMER_PROFILE_TABLE not configured; cannot persist buyer profile")
164 return _response(500, {"error": "Registration store not configured. Please try again."})
165
166 # Always stamp productCode + the invocation Region on the profile row (identity/reference
167 # metadata, not PII). Set registrationData + promoted fields only when the seller has
168 # allowlisted fields to store.
169 profile_expr = "SET productCode = :pc"
170 profile_values = {":pc": product_code}
171 profile_names = {}
172 if registration_data:
173 # Use INDEX-based expression tokens (not the raw field name) so a promoted field whose
174 # name contains characters invalid in an expression token (e.g. a hyphen or space) does
175 # not break the UpdateItem. Each promoted field is written both into the registrationData
176 # map (:rd) AND as its own top-level attribute (via #n{i}) so its GSI can index it.
177 profile_expr += ", registrationData = :rd"
178 profile_values[":rd"] = registration_data
179 for i, field in enumerate(PROMOTED_PROFILE_FIELDS):
180 if field in registration_data:
181 name_tok = f"#n{i}"
182 val_tok = f":v{i}"
183 profile_expr += f", {name_tok} = {val_tok}"
184 profile_names[name_tok] = field
185 profile_values[val_tok] = registration_data[field]
186 profile_expr, profile_values = _stamp(profile_expr, profile_values)
187 kwargs = {
188 "Key": {"licenseArn": license_arn, "customerAWSAccountId": customer_aws_account_id},
189 "UpdateExpression": profile_expr,
190 "ExpressionAttributeValues": profile_values,
191 }
192 if profile_names:
193 kwargs["ExpressionAttributeNames"] = profile_names
194 customer_profile_table.update_item(**kwargs)
195
196 # 2) Subscribers row (us-east-1) — PII-FREE: only the non-PII productCode + the
197 # idempotent registeredRegions append. NO registration form data is written here.
198 # registeredRegions is a DynamoDB String Set (SS). `ADD` on a set is atomic and idempotent
199 # (a no-op if the region is already present), so concurrent/retried registrations for the
200 # same buyer + region can never duplicate an entry — no read-then-write, no get_item.
201 update_expr = "SET productCode = :pc"
202 values = {":pc": product_code}
203 if INVOCATION_REGION:
204 update_expr += " ADD registeredRegions :region"
205 values[":region"] = {INVOCATION_REGION} # Python set -> DynamoDB String Set
206
207 update_expr, values = _stamp(update_expr, values)
208 subscribers_table.update_item(
209 Key={"licenseArn": license_arn, "customerAWSAccountId": customer_aws_account_id},
210 UpdateExpression=update_expr,
211 ExpressionAttributeValues=values,
212 )
213
214 logger.info(
215 f"Registered customer account={_mask(customer_aws_account_id)} region={INVOCATION_REGION} "
216 f"(PII → in-region customer-profile table; subscribers row PII-free)"
217 )
218
219 return _response(
220 200,
221 {"message": "Registration successful. Your account is being activated."},
222 )
223
224
225def _extract_registration_fields(form):
226 """Extract ALLOWLISTED custom registration form fields.
227
228 The registration endpoint is public and unauthenticated, so we do NOT persist
229 arbitrary caller-supplied fields. We persist only the fields named in the
230 ALLOWED_REGISTRATION_FIELDS env var (comma-separated), and enforce per-field length
231 and total field-count bounds. Unknown/oversized fields are dropped, not written
232 verbatim to DynamoDB. The token is never persisted.
233
234 TODO(seller): configure ALLOWED_REGISTRATION_FIELDS to match your registration form,
235 e.g. "company_name,email,team_size". If unset, no custom fields are persisted.
236 """
237 registration_data: dict[str, str] = {}
238 if not ALLOWED_REGISTRATION_FIELDS:
239 return registration_data
240 count = 0
241 for key in ALLOWED_REGISTRATION_FIELDS:
242 if count >= MAX_REGISTRATION_FIELDS:
243 break
244 values = form.get(key)
245 if not values:
246 continue
247 value = values[0] # single-valued only; ignore repeated params
248 if not isinstance(value, str):
249 continue
250 if len(value) > MAX_FIELD_VALUE_LENGTH:
251 logger.warning(f"Registration field '{key}' exceeds max length; truncating")
252 value = value[:MAX_FIELD_VALUE_LENGTH]
253 registration_data[key] = value
254 count += 1
255 return registration_data
256
257
258def _response(status, body):
259 return {
260 "statusCode": status,
261 "headers": {
262 "Content-Type": "application/json",
263 "Cache-Control": "no-store",
264 "X-Content-Type-Options": "nosniff",
265 "X-Frame-Options": "DENY",
266 "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
267 },
268 "body": json.dumps(body),
269 }