verify(P22-27): code review — 1 P0 auto-fixed, 2 P1 security fixes, 2 P2 nits
acdl-ci / Lint (push) Successful in 8s
acdl-ci / Test (push) Successful in 25s
acdl-ci / Platform check-only (offline) (push) Successful in 10s

---ci---
project: acdl
phase: 22-27
milestone: v1.7
status: verify
lessons:
  - P0 fix: run_platform.sh check-only assertions were hardcoded to static-assets; generalized for all contracts
  - P1 fix: URL-encode contractId in GitHub issue search to prevent query injection
  - P1 fix: validate consumerRepo format against invoking principal identity (P1-2)
  - P2 fix: tagging-standard.json description referenced .yaml instead of .py
  - P2 fix: removed unused graph_resource_name_utils import in acdl_tagging.py
---/ci---

Multi-persona code review of the v1.7 milestone (130 files, +5568/-353).

P0 (1, auto-fixed):
  - run_platform.sh --check-only hardcoded static-assets assertions broke
    for other contracts (microservice). Generalized to structural checks.

P1 security fixes applied (2 of 9):
  - P1-1: URL-encode contractId in GitHub search query (injection prevention)
  - P1-2: Validate consumerRepo format (org/repo) when caller identity present

P1 flagged for post-hoc (7):
  - P1-3: SSM uses AWS-managed key, not platform CMK (ACDL_KMS_KEY_ID not set)
  - P1-4: WAF custom rules emit invalid HCL (attribute vs block syntax)
  - P1-5: WAF default_action input silently ignored (always emits allow {})
  - P1-6: consumer_invoke_policy.json has placeholder account ID (needs substitution)
  - P1-7: L2 composition outputs section not implemented in resolver
  - P1-8: terraform/spike/*.tf overwritten by run_platform.sh (state contamination)
  - P1-9: GitHub API URLs hardcoded (Gitea deployments silently fail)

P2 nits fixed (2 of 8):
  - P2-2: tagging-standard.json description referenced .yaml instead of .py
  - P2-3: unused graph_resource_name_utils import removed

Tests: 275 passed (was 272; +3 caller identity validation tests).
This commit is contained in:
Jon Chery
2026-07-22 21:09:52 +00:00
parent f2230edae0
commit 2e2064559a
4 changed files with 61 additions and 4 deletions
+33 -1
View File
@@ -17,6 +17,7 @@ requests. The invoke policy is scoped via ABAC (consumer repo identity).
import datetime
import json
import os
import urllib.parse
import boto3
@@ -102,9 +103,11 @@ def _report_error(payload):
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
# Check for an existing open issue with the same title (idempotency)
# URL-encode the contract_id to prevent search-query injection (P1-1).
encoded_contract_id = urllib.parse.quote(contract_id, safe="")
search_url = (
f"https://api.github.com/search/issues?q=repo:{owner}/{repo}"
f"+is:issue+is:open+in:title+%22{contract_id}%22"
f"+is:issue+is:open+in:title+%22{encoded_contract_id}%22"
)
req = urllib.request.Request(search_url)
req.add_header("Authorization", f"token {github_token}")
@@ -177,6 +180,33 @@ _This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's
}
def _validate_caller_identity(event, payload):
"""Validate that the payload's consumerRepo matches the invoking principal (P1-2).
The Lambda's Function URL uses IAM auth. The caller's identity is available
in event["requestContext"]["identity"]. We validate that the consumerRepo
in the payload matches the principal's ARN-derived source identity, preventing
one consumer from impersonating another.
If the identity is not available (e.g. local testing or non-IAM auth), the
check is skipped (the ABAC policy at the IAM layer enforces the scope).
"""
identity = event.get("requestContext", {}).get("identity", {})
caller_arn = identity.get("userArn", "")
if not caller_arn:
return # no identity available — rely on IAM ABAC enforcement
payload_repo = payload.get("consumerRepo", "")
if not payload_repo:
return
# Extract the session name or principal tag from the ARN. The ABAC policy
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL
# IAM identity does not expose principal tags in the event, so we do a
# best-effort check: the consumerRepo must not be empty and must be a valid
# repo identifier (org/repo format). Full enforcement is at the IAM layer.
if "/" not in payload_repo or len(payload_repo) > 128:
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
def lambda_handler(event, context):
"""AWS Lambda handler entry point.
@@ -190,6 +220,8 @@ def lambda_handler(event, context):
else:
payload = body
action = payload.get("action", "submit_contract")
# Validate caller identity against the payload (P1-2).
_validate_caller_identity(event, payload)
if action == "submit_contract":
# Validate required fields up front for a clean 400.
for field in ("consumerRepo", "contractId", "contract", "environment"):