diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index c2b37fd..fa9712e 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -121,26 +121,23 @@ Description: >- # correctly left untouched. # # SIZE BUDGET: an attached managed policy document is capped at 6,144 characters -# (whitespace excluded). LambdaExecutionBoundary measures 691 characters across -# 4 statements as of 2026-07-31 — the fleet-wide floor only. Measure before -# widening — len(json.dumps(doc,separators=(',',':'))) on the synthesized -# PolicyDocument with ${AWS::AccountId} resolved, and UPDATE THESE TWO NUMBERS in -# the same edit (they went stale twice inside this branch alone). +# (whitespace excluded). LambdaExecutionBoundary measures 5903 characters across +# 16 statements as of 2026-08-07 (PLAT-93 consolidation after the meal-order +# PolicySize rollback). Measure before widening — len(json.dumps(doc, +# separators=(',',':'))) on the synthesized PolicyDocument with +# ${AWS::AccountId} resolved, and UPDATE THESE TWO NUMBERS in the same edit. # -# Headroom is 5,453 characters, roughly TWELVE workloads at ~450 each. That is a -# deliberate outcome, not luck: an earlier revision of this branch pre-loaded -# per-workload prefixes for all five mgmt SAM stacks and reached 5,457 characters -# with 687 left — about one workload of room — before any stack had actually -# migrated. Deferring per-workload scope to each migration PR (see the note on -# the boundary itself) removed that pressure entirely. If the budget tightens -# again as workloads land, the end-state fix is per-workload boundaries +# Headroom is 241 characters. Statement consolidation (shared WorkloadSecrets / +# WorkloadDynamoDB / extended WorkloadS3 Resource lists; no new action wildcards) +# is the only remaining lever short of per-workload boundaries. If the budget +# tightens again, the end-state fix is per-workload boundaries # (seahaven-lambda-execution-boundary-), which also resolves the -# shared-ceiling residual — tracked as INFRA-187, do not improvise it. +# shared-ceiling residual — tracked as PLAT-52 / INFRA-187, do not improvise it. # CRITICAL: unlike the 2026-07-27 inline-limit incident, # there is NO restructure available when this cap is reached — a role has exactly # ONE permissions boundary, so statements cannot be spilled into a second attached # managed policy. At the cap the only levers are prefix consolidation and dropping -# unused actions. +# unused actions. Do not introduce dynamodb:* / s3:* / ses:Send* to reclaim space. # # This template is deployed via lib/deploy-substrate-stack.ts # (cloudformation-include) as stack seahaven-deploy-substrate, once per member @@ -492,71 +489,38 @@ Resources: - ec2:DescribeVpcs Resource: "*" - # ── afi-backup-monitor (PLAT-56) — prod-only exact secret ARNs ─────── - # Derived from live stack parameters + secrets created in seahaven-prod - # 2026-08-05. No secret:afi-* patterns. Omitted in seahaven-dev via - # IsProdAccount (same template deploys to both accounts). + # ── Shared workload data-plane (PLAT-93 PolicySize consolidation) ─── + # Per-workload secret/DDB/S3 SIDs were merged so meal-order-manager + # (PLAT-70) can fit under the 6,144-character managed-policy cap + # without introducing new action wildcards. Exact secret ARNs only. + # End-state isolation remains PLAT-52 / INFRA-187. + # + # WorkloadSecrets covers: afi-backup-monitor (PLAT-56), + # front-integrations (PLAT-72), procurement-ingest (PLAT-86), + # meal-order-manager (PLAT-70). - !If - IsProdAccount - - Sid: AfiBackupMonitorSecrets + - Sid: WorkloadSecrets Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-api-key-w0E02a - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-slack-webhook-T4oR3G - - !Ref AWS::NoValue - - # ── front-integrations (PLAT-72) — prod-only exact secret ARNs + DDB ─ - # Derived from live SAM template + secrets created in seahaven-prod - # 2026-08-05. No secret:front-integrations/* patterns. - - !If - - IsProdAccount - - Sid: FrontIntegrationsSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/front-api-token-UXKv0U - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/slack-bot-token-giGfA7 - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/google-service-account-dqv3Bo - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: FrontIntegrationsDynamoDB - Effect: Allow - Action: - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - - dynamodb:Query - - dynamodb:Scan - - dynamodb:BatchGetItem - - dynamodb:BatchWriteItem - - dynamodb:DescribeTable - - dynamodb:ConditionCheckItem - Resource: - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts/index/*" - - !Ref AWS::NoValue - - # ── procurement-ingest (PLAT-86) — prod-only exact secrets + data plane ─ - # Derived from live CDK stacks in seahaven-prod 2026-08-06. Exact secret - # ARNs only (no secret:procurement-* / workorder-ingest/* wildcards). - - !If - - IsProdAccount - - Sid: ProcurementIngestSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:procurement-ingest/web-ui-auth-token-ApAMmr - arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB + - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw - !Ref AWS::NoValue + + # WorkloadDynamoDB: enumerated union of front-integrations CRUD + + # procurement-ingest CRUD/stream actions. Meal-order table ARNs appended. + # Stream actions on non-stream tables are inert at the ceiling. - !If - IsProdAccount - - Sid: ProcurementIngestDynamoDB + - Sid: WorkloadDynamoDB Effect: Allow Action: - dynamodb:GetItem @@ -576,6 +540,8 @@ Resources: # it is a silent no-op. Runtime stream consumers use the # stream ARN via DescribeStream/GetRecords above. Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts/index/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites" @@ -590,10 +556,16 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" - !Ref AWS::NoValue + + # ── procurement-ingest remaining data plane + meal-order S3 (PLAT-86/70) ─ + # WorkloadS3 keeps the prior ProcurementIngestS3 action list and appends + # meal-order form/reports bucket ARNs (same object CRUD shape). - !If - IsProdAccount - - Sid: ProcurementIngestS3 + - Sid: WorkloadS3 Effect: Allow Action: - s3:GetObject* @@ -607,6 +579,10 @@ Resources: - !Sub "arn:aws:s3:::po-ingest-emails-${AWS::AccountId}/*" - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}" - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" - !Ref AWS::NoValue - !If - IsProdAccount @@ -651,6 +627,8 @@ Resources: - arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 - arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 - !Ref AWS::NoValue + # site-alerts publish shared by procurement-ingest alarms and + # meal-order-manager (PLAT-70); no separate MealOrder SNS statement. - !If - IsProdAccount - Sid: ProcurementIngestSns @@ -692,61 +670,15 @@ Resources: - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*" - !Ref AWS::NoValue - # ── meal-order-manager (PLAT-70) — prod-only exact secret ARN + data plane ─ - # Derived from meal-order-manager template.yaml + secrets created in - # seahaven-prod 2026-08-07. Exact ARN only (no secret:meal-order-* patterns). - - !If - - IsProdAccount - - Sid: MealOrderManagerSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerDynamoDB - Effect: Allow - Action: - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - - dynamodb:Query - - dynamodb:Scan - - dynamodb:BatchGetItem - - dynamodb:BatchWriteItem - - dynamodb:DescribeTable - - dynamodb:ConditionCheckItem - Resource: - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerS3 - Effect: Allow - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:PutObject* - - s3:DeleteObject* - - s3:AbortMultipartUpload - Resource: - - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" - - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" - - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" - - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" - - !Ref AWS::NoValue + # ── meal-order-manager (PLAT-70) — statements not covered above ───── + # Secrets → WorkloadSecrets; DynamoDB → WorkloadDynamoDB; S3 → WorkloadS3; + # SNS → ProcurementIngestSns. Trimmed to identity-policy needs. - !If - IsProdAccount - Sid: MealOrderManagerSsm Effect: Allow Action: - ssm:GetParameter - - ssm:GetParameters Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" - !Ref AWS::NoValue @@ -765,26 +697,17 @@ Resources: Effect: Allow Action: - ses:SendRawEmail - - ses:SendEmail Resource: "*" - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerSns - Effect: Allow - Action: - - sns:Publish - Resource: - - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" - - !Ref AWS::NoValue # ── FURTHER PER-WORKLOAD DATA-PLANE STATEMENTS ────────────────────── - # Floor above + afi-backup-monitor (PLAT-56) + front-integrations - # (PLAT-72) + procurement-ingest (PLAT-86) + seahaven-site (PLAT-91) - # + meal-order-manager (PLAT-70). - # Additional stacks add their own statements here, derived from THEIR - # OWN template, in their own PR, deployed to UPDATE_COMPLETE before - # first workload deploy. WIDENING PATH in the header still governs. + # Floor above + consolidated WorkloadSecrets/DynamoDB/S3 (PLAT-93) + + # procurement remainder + seahaven-site (PLAT-91) + meal-order extras + # (PLAT-70). Prefer extending existing Resource lists over new SIDs. + # Additional stacks add statements here, derived from THEIR OWN + # template, in their own PR, deployed to UPDATE_COMPLETE before first + # workload deploy. Measure SIZE BUDGET before merging. WIDENING PATH + # in the header still governs. # # WHY the floor stayed empty of data-plane (decided 2026-07-30, Adam): # The security win of INFRA-186 comes from DELETION, not enumeration. @@ -795,10 +718,10 @@ Resources: # # The end-state fix for the shared-ceiling residual (one boundary = # every SAM/Terraform workload reaches every other's data plane once - # they land) is per-workload boundaries — tracked as INFRA-187. Do not - # improvise it: both guardrail policies pin ONE literal boundary ARN - # inside StringEquals conditions, and loosening that to a wildcard - # weakens the gate. + # they land) is per-workload boundaries — tracked as PLAT-52 / + # INFRA-187. Do not improvise it: both guardrail policies pin ONE + # literal boundary ARN inside StringEquals conditions, and loosening + # that to a wildcard weakens the gate. # --------------------------------------------------------------------------- # Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped #