From 1f67543f167b5aeffa0deceabb114605faa262eb Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 7 Aug 2026 19:33:49 -0400 Subject: [PATCH] fix(ci): satisfy PR policy and authorizer trust constraints Move weekly-menu step expressions into env blocks, terraform-fmt SES alignment, and pin API Gateway authorizer invoke role assume conditions. --- .github/workflows/weekly-menu.yml | 43 ++++++++++++++++++++----------- terraform/iam.tf | 18 +++++++++++-- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/.github/workflows/weekly-menu.yml b/.github/workflows/weekly-menu.yml index cd9a6ad..169ef0f 100644 --- a/.github/workflows/weekly-menu.yml +++ b/.github/workflows/weekly-menu.yml @@ -28,8 +28,9 @@ jobs: steps: - name: Timezone guard if: github.event_name == 'schedule' + env: + CRON: ${{ github.event.schedule }} run: | - CRON="${{ github.event.schedule }}" OFFSET=$(TZ='America/New_York' date +%z) echo "Cron: $CRON | Eastern offset: $OFFSET" if { [ "$OFFSET" = "-0400" ] && [ "$CRON" = "30 12 * * 1" ]; } || \ @@ -92,9 +93,10 @@ jobs: - name: Get discount settings if: env.SKIP_RUN != 'true' id: discount + env: + API_URL: ${{ steps.stack.outputs.api_url }} run: | - SETTINGS=$(python3 scripts/upload_menu.py settings \ - --api-url "${{ steps.stack.outputs.api_url }}") + SETTINGS=$(python3 scripts/upload_menu.py settings --api-url "$API_URL") BULK=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["bulk_discount_percent"])' "$SETTINGS") SUBSIDY=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["company_subsidy_percent"])' "$SETTINGS") echo "bulk_discount=$BULK" >> "$GITHUB_OUTPUT" @@ -112,42 +114,53 @@ jobs: echo "Google client ID is required for cloud form generation" >&2 exit 1 fi - echo "client_id=$GOOGLE_CLIENT_ID" >> $GITHUB_OUTPUT + echo "client_id=$GOOGLE_CLIENT_ID" >> "$GITHUB_OUTPUT" - name: Generate order form if: env.SKIP_RUN != 'true' + env: + API_URL: ${{ steps.stack.outputs.api_url }} + BULK_DISCOUNT: ${{ steps.discount.outputs.bulk_discount }} + COMPANY_SUBSIDY: ${{ steps.discount.outputs.company_subsidy }} + GOOGLE_CLIENT_ID: ${{ steps.google.outputs.client_id }} run: | python3 src/server/generate_form.py \ - --api-url "${{ steps.stack.outputs.api_url }}" \ - --bulk-discount "${{ steps.discount.outputs.bulk_discount }}" \ - --company-subsidy "${{ steps.discount.outputs.company_subsidy }}" \ - --google-client-id "${{ steps.google.outputs.client_id }}" + --api-url "$API_URL" \ + --bulk-discount "$BULK_DISCOUNT" \ + --company-subsidy "$COMPANY_SUBSIDY" \ + --google-client-id "$GOOGLE_CLIENT_ID" - name: Publish menu through API if: env.SKIP_RUN != 'true' - run: | - python3 scripts/upload_menu.py publish \ - --api-url "${{ steps.stack.outputs.api_url }}" + env: + API_URL: ${{ steps.stack.outputs.api_url }} + run: python3 scripts/upload_menu.py publish --api-url "$API_URL" - name: Upload form to S3 if: env.SKIP_RUN != 'true' + env: + FORM_BUCKET: ${{ steps.stack.outputs.form_bucket }} run: | WEEK=$(date +%Y-W%U) aws s3 cp "output/order-form-$WEEK.html" \ - "s3://${{ steps.stack.outputs.form_bucket }}/index.html" \ + "s3://${FORM_BUCKET}/index.html" \ --content-type "text/html" \ --cache-control "no-cache" aws s3 cp "output/order-form-$WEEK.html" \ - "s3://${{ steps.stack.outputs.form_bucket }}/archive/$WEEK.html" \ + "s3://${FORM_BUCKET}/archive/$WEEK.html" \ --content-type "text/html" - name: Invalidate CloudFront cache if: env.SKIP_RUN != 'true' + env: + DIST_ID: ${{ steps.stack.outputs.dist_id }} run: | aws cloudfront create-invalidation \ - --distribution-id "${{ steps.stack.outputs.dist_id }}" \ + --distribution-id "$DIST_ID" \ --paths "/index.html" - name: Notify Slack if: env.SKIP_RUN != 'true' - run: python3 scripts/notify_slack.py "${{ steps.stack.outputs.form_url }}" + env: + FORM_URL: ${{ steps.stack.outputs.form_url }} + run: python3 scripts/notify_slack.py "$FORM_URL" diff --git a/terraform/iam.tf b/terraform/iam.tf index d24f647..5e49566 100644 --- a/terraform/iam.tf +++ b/terraform/iam.tf @@ -174,6 +174,8 @@ resource "aws_iam_role_policy" "admin_authorizer" { # Role API Gateway assumes to invoke the authorizer Lambda. The authorizer has # no resource policy of its own; this identity-based grant is the only path. +# SourceAccount + execute-api ArnLike close the confused-deputy window without +# pinning the authorizer id (that would cycle: authorizer needs this role). data "aws_iam_policy_document" "apigateway_assume" { statement { effect = "Allow" @@ -183,6 +185,18 @@ data "aws_iam_policy_document" "apigateway_assume" { type = "Service" identifiers = ["apigateway.amazonaws.com"] } + + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.account_id] + } + + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = ["arn:aws:execute-api:${var.aws_region}:${local.account_id}:${aws_apigatewayv2_api.order_api.id}/*"] + } } } @@ -391,8 +405,8 @@ data "aws_iam_policy_document" "email_report" { # Scope SendRawEmail to the verified sender domain/identity rather than "*". # SES still enforces verification; IAM pins the From identity ARNs. statement { - sid = "SendPayrollReport" - effect = "Allow" + sid = "SendPayrollReport" + effect = "Allow" actions = ["ses:SendRawEmail"] resources = [ "arn:aws:ses:${var.aws_region}:${local.account_id}:identity/${var.sender_email}",