Skip to content

Commit 96cabe9

Browse files
hyunsiesChris Hyuclaude
authored
feat: Q3 Option D — scope-intersection preflight (§1.108) (#38)
Prevents cross-Lambda MPE contamination (§1.108) at deploy time by computing scope overlap against every existing map-auto-tagger-* stack in the target account. Hard-fails the deploy with a named peer + named overlap element so the customer knows exactly which stack conflicts and on which dimension. Q3 Option D rules (decided 2026-04-24 after reviewing Options A/B/C): account/ALL vs anything in same account → conflict (ALL dominates) account/[X,Y,…] vs account/[Z,Y,…] → conflict iff shared account ID account/[X,Y,…] vs vpc/[V,…] → conflict iff deploy-account ∈ [X,Y,…] vpc/[V1,V2] vs vpc/[V2,V3] → conflict iff shared VPC ID vpc/[V1,…] vs vpc/[Vn,…] (disjoint) → safe coexistence Two-phase preflight: 1. IAM (extends PR #23 batched SimulatePrincipalPolicy): - single-account path: adds cloudformation:ListStacks, ssm:GetParameter - multi-account path: adds cloudformation:ListStacks, ListStackSets, ListStackInstances, organizations:ListAccounts, ssm:GetParameter Fail-fast with precise missing-permission error before running any subsequent check. Per user decision 2026-04-24, unreadable peer config (missing ssm:GetParameter) now hard-fails with the specific remediation instead of the prior "treat as full conflict" fallback. 2. Scope-intersection (replaces PR #24's Class-2 account/account-too-strict logic): reads each peer stack's SSM config, classifies the overlap per the rules table, fails with the specific overlap dimension. Out of scope (per Q3 Option D scope decisions 2026-04-24): - TOCTOU on simultaneous deploys (rare; accepted) - Manual SSM edits post-deploy (users deploy only via configurator per policy) - Bypass-configurator deploy paths (unsupported by policy) Reuses PR #24 Class-1 (multi-account StackSet) preflight scaffolding — Class-1 already computes account-set intersection correctly, so no change there. Blocks reconciliation Lambda (plan-PR #39) per Q2-4 ordering decision. Landing this before reconciliation makes wrong-MPE overwrite safe-by-construction for new deploys (no overlapping peer tagger can exist going forward). Verified locally: sync-check passes, lint_event_prefixes passes, 117-line configurator.html change net +39. Deploy-script paths are inline JS template literals; shell syntax tested via render. Co-authored-by: Chris Hyu <chhyu@amazon.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0034f27 commit 96cabe9

1 file changed

Lines changed: 78 additions & 39 deletions

File tree

configurator.html

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6124,6 +6124,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
61246124
"cloudformation:UpdateStack" \\
61256125
"cloudformation:DescribeStacks" \\
61266126
"cloudformation:GetTemplateSummary" \\
6127+
"cloudformation:ListStacks" \\
61276128
"iam:CreateRole" \\
61286129
"iam:PutRolePolicy" \\
61296130
"iam:AttachRolePolicy" \\
@@ -6135,6 +6136,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
61356136
"sqs:CreateQueue" \\
61366137
"sqs:SetQueueAttributes" \\
61376138
"ssm:PutParameter" \\
6139+
"ssm:GetParameter" \\
61386140
"logs:CreateLogGroup" \\
61396141
"logs:PutRetentionPolicy" \\
61406142
"sns:CreateTopic" \\
@@ -6200,16 +6202,24 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
62006202
esac
62016203
done
62026204

6203-
# ── Same-account multi-Lambda conflict (Class 2: single-account mode) ────
6205+
# ── Same-account multi-Lambda conflict (Class 2: single-account, Q3 Option D) ────
62046206
# If another map-auto-tagger-* stack exists in this account with a different
6205-
# MPE, both Lambdas receive the same CloudTrail events. Last-writer-wins on
6206-
# the map-migrated tag value. Read the existing stack's SSM config and:
6207-
# - either deploy uses scope=all → full conflict, fail with account
6208-
# - both use scope=vpc → compute VPC overlap, fail only if VPCs overlap
6209-
# If both are VPC-scoped with non-overlapping VPC lists, allow deploy to
6210-
# continue — runtime is_in_scope filtering prevents double-tagging.
6207+
# MPE, both Lambdas receive the same CloudTrail events. Whoever tags last wins.
6208+
# Q3 Option D intersects scopes per pair and hard-fails only on actual overlap:
6209+
#
6210+
# account/ALL vs anything in same account → conflict (ALL dominates)
6211+
# account/[X,Y,…] vs account/[Z,Y,…] → conflict iff shared account ID
6212+
# account/[X,Y,…] vs vpc/[V,…] → conflict iff this deploy-account is in [X,Y,…]
6213+
# (the account-scoped peer claims all of account,
6214+
# including the VPC-scoped peer's VPCs)
6215+
# vpc/[V1,V2] vs vpc/[V2,V3] → conflict iff shared VPC ID
6216+
# vpc/[V1,…] vs vpc/[Vn,…] (disjoint) → safe coexistence
62116217
NEW_SCOPE_MODE="${config.scopeMode || 'account'}"
62126218
NEW_VPC_LIST="${(config.scopedVpcIds && config.scopedVpcIds[0] !== 'NONE') ? config.scopedVpcIds.join(' ') : ''}"
6219+
# For single-account deploys, the new scope's "account list" is always just \$ACCOUNT.
6220+
# (The ScopedAccountIds CFN parameter is a multi-account convenience filter evaluated
6221+
# by the Lambda at runtime; single-account deploys set it to ALL and rely on the
6222+
# Lambda's per-account identity at tag time.)
62136223
for CHECK_REGION in \$REGIONS; do
62146224
EXISTING_STACKS=\$(aws cloudformation list-stacks --region "\$CHECK_REGION" \\
62156225
--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE \\
@@ -6219,50 +6229,74 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
62196229
for EXISTING in \$EXISTING_STACKS; do
62206230
EXISTING_MPE="\${EXISTING#map-auto-tagger-}"
62216231
# Only treat as a peer deploy if the suffix matches the MpeId pattern
6222-
# (AllowedPattern: ^mig[a-zA-Z0-9]+\$). Skips test harness / E2E stacks
6223-
# that happen to start with our prefix but aren't real MAP deploys.
6232+
# (AllowedPattern: ^mig[a-zA-Z0-9]+\$). Skips test harness / E2E stacks.
62246233
case "\$EXISTING_MPE" in mig*) ;; *) continue ;; esac
62256234
if [ "\$EXISTING_MPE" = "\$MPE" ]; then continue; fi
6226-
# Read the existing deploy's SSM config to understand its scope
6235+
# Read peer's SSM config. Unreadable config = missing ssm:GetParameter
6236+
# on /auto-map-tagger/* — hard-fail with that specific remediation. The
6237+
# IAM preflight above should catch this first, but retain as defence.
62276238
EXISTING_CFG=\$(aws ssm get-parameter --name "/auto-map-tagger/\$EXISTING_MPE/config" \\
62286239
--region "\$CHECK_REGION" --query 'Parameter.Value' --output text 2>/dev/null || echo "")
62296240
if [ -z "\$EXISTING_CFG" ]; then
6230-
echo " ⚠️ Found existing stack \$EXISTING but could not read its config (missing ssm:GetParameter?). Treating as full conflict."
6231-
echo " Resources created in this account will be double-tagged (last-writer-wins on map-migrated)."
6232-
PREFLIGHT_LOG="\${PREFLIGHT_LOG}${t('d_log_fail')} \$EXISTING config unreadable — assumed full conflict\\n"
6241+
echo " ❌ Peer stack \$EXISTING exists but /auto-map-tagger/\$EXISTING_MPE/config is unreadable."
6242+
echo " Grant ssm:GetParameter on arn:aws:ssm:\$CHECK_REGION:*:parameter/auto-map-tagger/* and retry."
6243+
PREFLIGHT_LOG="\${PREFLIGHT_LOG}${t('d_log_fail')} \$EXISTING config unreadable (missing ssm:GetParameter)\\n"
62336244
LAMBDA_CONFLICT=1
62346245
continue
62356246
fi
62366247
EXISTING_SCOPE_MODE=\$(echo "\$EXISTING_CFG" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("scope_mode","account"))' 2>/dev/null || echo "account")
6237-
# Full conflict if either is not VPC-scoped
6238-
if [ "\$EXISTING_SCOPE_MODE" != "vpc" ] || [ "\$NEW_SCOPE_MODE" != "vpc" ]; then
6239-
echo " ❌ ${t('d_fail_account_lambda_full')}"
6240-
echo " - \$EXISTING (MPE \$EXISTING_MPE, scope=\$EXISTING_SCOPE_MODE) vs new (MPE \$MPE, scope=\$NEW_SCOPE_MODE)"
6241-
echo " ${t('d_fix_label')} ${t('d_fix_account_lambda')}"
6242-
PREFLIGHT_LOG="\${PREFLIGHT_LOG}${t('d_log_fail')} full-scope conflict with \$EXISTING\\n"
6243-
LAMBDA_CONFLICT=1
6244-
continue
6245-
fi
6246-
# Both VPC-scoped — compute VPC overlap
6248+
EXISTING_ACCOUNTS=\$(echo "\$EXISTING_CFG" | python3 -c 'import json,sys; print(" ".join(json.load(sys.stdin).get("scoped_account_ids",["ALL"])))' 2>/dev/null || echo "ALL")
62476249
EXISTING_VPCS=\$(echo "\$EXISTING_CFG" | python3 -c 'import json,sys; print(" ".join(json.load(sys.stdin).get("scoped_vpc_ids",[])))' 2>/dev/null || echo "")
6248-
OVERLAP_VPCS=""
6249-
for NEW_VPC in \$NEW_VPC_LIST; do
6250-
for EXISTING_VPC in \$EXISTING_VPCS; do
6251-
if [ "\$NEW_VPC" = "\$EXISTING_VPC" ]; then
6252-
OVERLAP_VPCS="\$OVERLAP_VPCS \$NEW_VPC"
6253-
fi
6254-
done
6255-
done
6256-
if [ -n "\$OVERLAP_VPCS" ]; then
6257-
echo " ❌ ${t('d_fail_account_lambda_vpc')}"
6258-
echo " Existing stack \$EXISTING (MPE \$EXISTING_MPE) shares VPCs with your new deploy:"
6259-
for V in \$OVERLAP_VPCS; do echo " - \$V"; done
6260-
echo " ${t('d_fix_label')} ${t('d_fix_account_lambda')}"
6261-
PREFLIGHT_LOG="\${PREFLIGHT_LOG}${t('d_log_fail')} VPC-scope conflict with \$EXISTING\\n"
6262-
LAMBDA_CONFLICT=1
6250+
CONFLICT_REASON=""
6251+
if [ "\$NEW_SCOPE_MODE" = "account" ] && [ "\$EXISTING_SCOPE_MODE" = "account" ]; then
6252+
# Both account-scoped, same account → \$ACCOUNT is claimed by both.
6253+
# Runtime is_in_scope with ALL on either side dominates.
6254+
case " \$EXISTING_ACCOUNTS " in
6255+
*" ALL "*) CONFLICT_REASON="peer scope=account/ALL dominates \$ACCOUNT";;
6256+
*" \$ACCOUNT "*) CONFLICT_REASON="peer scope includes \$ACCOUNT";;
6257+
esac
6258+
if [ -z "\$CONFLICT_REASON" ]; then
6259+
# Peer targets different specific accounts — our deploy-account isn't in peer scope.
6260+
echo " ✅ Peer \$EXISTING (MPE \$EXISTING_MPE, scope=account/[\$EXISTING_ACCOUNTS]) does not target \$ACCOUNT — safe"
6261+
continue
6262+
fi
6263+
elif [ "\$NEW_SCOPE_MODE" = "account" ] && [ "\$EXISTING_SCOPE_MODE" = "vpc" ]; then
6264+
# Our side claims the whole account; peer claims specific VPCs in same account.
6265+
# We'd tag the peer's VPC resources too. Conflict.
6266+
CONFLICT_REASON="our account-mode dominates peer VPC-scope on shared VPCs"
6267+
elif [ "\$NEW_SCOPE_MODE" = "vpc" ] && [ "\$EXISTING_SCOPE_MODE" = "account" ]; then
6268+
# Inverse: peer claims whole account (or our account is in their list), we claim VPCs within.
6269+
case " \$EXISTING_ACCOUNTS " in
6270+
*" ALL "*|*" \$ACCOUNT "*) CONFLICT_REASON="peer account-mode dominates our VPC-scope (\$EXISTING_ACCOUNTS)";;
6271+
esac
6272+
if [ -z "\$CONFLICT_REASON" ]; then
6273+
# Peer does NOT target our deploy-account → safe.
6274+
echo " ✅ Peer \$EXISTING (MPE \$EXISTING_MPE, scope=account/[\$EXISTING_ACCOUNTS]) does not target \$ACCOUNT — safe"
6275+
continue
6276+
fi
62636277
else
6264-
echo " ✅ Existing stack \$EXISTING (MPE \$EXISTING_MPE) has non-overlapping VPC scope — safe to coexist"
6278+
# Both VPC-scoped — compute VPC overlap.
6279+
OVERLAP_VPCS=""
6280+
for NEW_VPC in \$NEW_VPC_LIST; do
6281+
for EXISTING_VPC in \$EXISTING_VPCS; do
6282+
if [ "\$NEW_VPC" = "\$EXISTING_VPC" ]; then
6283+
OVERLAP_VPCS="\$OVERLAP_VPCS \$NEW_VPC"
6284+
fi
6285+
done
6286+
done
6287+
if [ -z "\$OVERLAP_VPCS" ]; then
6288+
echo " ✅ Peer \$EXISTING (MPE \$EXISTING_MPE, scope=vpc/[\$EXISTING_VPCS]) disjoint from our VPC list — safe"
6289+
continue
6290+
fi
6291+
CONFLICT_REASON="shared VPC(s):\$OVERLAP_VPCS"
62656292
fi
6293+
echo " ❌ ${t('d_fail_account_lambda_full')}"
6294+
echo " Peer: \$EXISTING (MPE \$EXISTING_MPE, scope=\$EXISTING_SCOPE_MODE)"
6295+
echo " New: MPE \$MPE, scope=\$NEW_SCOPE_MODE"
6296+
echo " Reason: \$CONFLICT_REASON"
6297+
echo " ${t('d_fix_label')} ${t('d_fix_account_lambda')}"
6298+
PREFLIGHT_LOG="\${PREFLIGHT_LOG}${t('d_log_fail')} scope conflict with \$EXISTING: \$CONFLICT_REASON\\n"
6299+
LAMBDA_CONFLICT=1
62666300
done
62676301
if [ \$LAMBDA_CONFLICT -eq 1 ]; then
62686302
ERRORS=\$((ERRORS + 1))
@@ -6621,8 +6655,12 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
66216655
"cloudformation:CreateStackSet" \\
66226656
"cloudformation:CreateStackInstances" \\
66236657
"cloudformation:DescribeStackSet" \\
6658+
"cloudformation:ListStacks" \\
6659+
"cloudformation:ListStackSets" \\
6660+
"cloudformation:ListStackInstances" \\
66246661
"organizations:ListRoots" \\
66256662
"organizations:DescribeOrganization" \\
6663+
"organizations:ListAccounts" \\
66266664
"iam:CreateRole" \\
66276665
"iam:PutRolePolicy" \\
66286666
"iam:AttachRolePolicy" \\
@@ -6634,6 +6672,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
66346672
"sqs:CreateQueue" \\
66356673
"sqs:SetQueueAttributes" \\
66366674
"ssm:PutParameter" \\
6675+
"ssm:GetParameter" \\
66376676
"logs:CreateLogGroup" \\
66386677
"logs:PutRetentionPolicy" \\
66396678
"sns:CreateTopic" \\

0 commit comments

Comments
 (0)