Files
adr-sml/docs/reference/intake-table-access-verification.md
T
Pouya LajevardiandClaude Opus 5 bd282aa47d
Build and deploy / build-and-deploy (push) Failing after 4s
feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Five items of Pouya's production run, 2026-09-01.

Q61 — scroll-padding-top becomes a max() ramp on `10lh - 83px`, with the
plain calc() first as the fallback for engines without `lh`. Hidden focus
stops under minimumFontSize=32: 290 of 1,455 -> 0, control build still
290. Default settings byte-identical (0 differences over 352 page-widths x
17 fields). The 12 residual cells at minimumFontSize=16/20 are pre-existing
and unchanged-or-better; reported, not widened, per instruction.

Intake backend + CloudFront — docs/09-cutover-runbook.md is the
copy-paste sequence for admin execution: every command followed by its
verification and expected output, rollback per part, and Part 10 is Q60's
TTL test. infra/cloudfront/router.js is the trailing-slash function
(30-case suite; 8 fail against the pre-review version, incl. a
protocol-relative open redirect). infra/cloudfront/configure.mjs is
dry-run-by-default and idempotent. scripts/intake-env.mjs emits the six
Lambda env vars from src/data/site.ts.

Four launch blockers found by reading the running system:
  - handler.mjs wrote pk/sk; the live table's key is submissionId with no
    sort key, so every submission would have failed validation silently
  - the Lambda invoke permission is scoped to the old route path
  - 22 of 23 pages 403 without the router function
  - there was no 404 page; src/pages/404.astro adds it

Claims audit (D20 cutover pass) — five gloss over-reaches corrected on
/practice/energy/, /practice/insurance/ (x2), /practice/technology/ and
/med-arb/. Three findings left open for Pouya: Q62, the /med-arb/ gloss,
and Q60.

Q62 — one frozen-tripwire pattern added under the freeze's own breach
exception, with a probe and four negative fixtures. check:claims exits 1
until the false /legal/privacy/ sentence is corrected, so both deploy
paths are blocked by a mechanism rather than by memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-09-02 06:52:20 -04:00

7.1 KiB

Who can read adr-intake-submissions — verification extract

Why this file exists. /legal/privacy/ makes a statement to the public about who can see the contents of the intake table. AGENTS.md §4 admits no factual claim that cannot be traced, and CLAUDE.md's R14 rule is that anything a spec claims about must be reachable from the repository — "if the artefact lives only in a console, no reviewer can compare the claim against it and the claim is unverifiable by construction." Until this file existed, that sentence was the one claim on the site whose subject was entirely outside the repo.

Raised by claims-auditor in the D20 cutover audit, 2026-09-01, finding 8.

Provenance. Every figure below was read from AWS on 2026-09-01 with the commands listed at the end, run read-only as arn:aws:iam::327082975128:user/pouya. No command in this file creates or changes anything. Re-run them rather than trusting this file; it is dated for that reason.


The claim being checked

src/pages/legal/privacy.astro, §Who can see it:

The table is reachable by the function that writes to it and by one administrative account, which is mine — nobody else has access to the table. There is no team, no assistant and no external administrator.

The finding: it is false

The account has an IAM group admins carrying the AWS managed policy AdministratorAccess, and it has two members: pouya and lars.

iam simulate-principal-policy for dynamodb:GetItem, dynamodb:Query and dynamodb:Scan against arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions, across all five IAM users in the account:

principal GetItem Query Scan
user/pouya allowed allowed allowed
user/lars allowed allowed allowed
user/adr-sml-deploy implicitDeny implicitDeny implicitDeny
user/gitea-deploy-meshkinilaw implicitDeny implicitDeny implicitDeny
user/meshkini-backend-deploy implicitDeny implicitDeny implicitDeny

lars holds exactly the access pouya holds, by the same route: membership of admins. The user's own attachments are only IAMUserChangePassword, so the group is the whole of it.

So the published sentence is wrong on both of its halves — a second account has access, and it belongs to a second administrator of a shared account.

The rest of the surface, recorded so the check is complete rather than partial

  • 5 IAM users: adr-sml-deploy, gitea-deploy-meshkinilaw, lars, meshkini-backend-deploy, pouya. The three deploy users are all implicitDeny above. adr-sml-deploy's scope is S3 + CloudFront and touches no table (docs/reference/deploy-credential-verification.md).
  • 1 IAM group: adminsAdministratorAccess and Billing, two members.
  • 33 IAM roles, 26 of them not service-linked. Two carry AdministratorAccess: cdk-hnb659fds-cfn-exec-role-327082975128-ca-central-1 and …-us-east-1. These are AWS CDK bootstrap CloudFormation execution roles, assumable by CloudFormation for stack deployment. They are a real path to the table for anyone who can deploy a CDK stack in this account — which is the two administrators above — rather than a third party.
  • adr-intake-lambda-role is the writing principal: dynamodb:PutItem on this table, ses:SendEmail/SendRawEmail, plus AWSLambdaBasicExecutionRole. PutItem only — it cannot read the table, which is worth stating because it is a stronger fact than the page currently claims and it is the part of the sentence that is true.
  • No resource-based policy on the table. DynamoDB supports one; this table has none, so access is governed entirely by identity policies.
  • The account is not single-project (AGENTS.md §10). lars and the two meshkini*/gitea* users are evidence of that on the IAM surface, not just in the S3 bucket listing §10 describes.

What has to happen before /legal/privacy/ goes public

Tracked as AGENTS.md §9 Q62. It is one of two things and both are Pouya's:

  1. Remove the access — take lars out of admins, or replace that membership with a policy that denies DynamoDB on this table — and then this sentence becomes true. Note the likely collision: AGENTS.md Q23 records the Gitea instance as jointly administered and blocked on "its second administrator", so this account is probably not the only thing that access is for.
  2. Correct the sentence to what is true. It is a privacy policy, so the honest version is short and specific — the number of people with administrative access, and that the function that writes cannot read.

Do not resolve it by softening. "Access is limited to authorised administrators" is the shape §4 exists to bar: defensible, uninformative, and it would replace a false specific with a true vacancy on the one page where a reader is entitled to the specific.


Commands

Run as user/pouya, ca-central-1, all read-only. Exit status read on each; no stderr suppressed anywhere.

aws iam list-users --query 'Users[].UserName'
aws iam list-groups --query 'Groups[].GroupName'
aws iam get-group --group-name admins --query 'Users[].UserName'
aws iam list-attached-group-policies --group-name admins --query 'AttachedPolicies[].PolicyName'
aws iam list-group-policies --group-name admins --query 'PolicyNames'
aws iam list-groups-for-user --user-name lars --query 'Groups[].GroupName'
aws iam list-attached-user-policies --user-name lars --query 'AttachedPolicies[].PolicyName'
aws iam list-user-policies --user-name lars --query 'PolicyNames'

TARN="arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions"
for U in pouya lars adr-sml-deploy gitea-deploy-meshkinilaw meshkini-backend-deploy; do
  aws iam simulate-principal-policy \
    --policy-source-arn "arn:aws:iam::327082975128:user/${U}" \
    --action-names dynamodb:GetItem dynamodb:Query dynamodb:Scan \
    --resource-arns "$TARN" \
    --query 'EvaluationResults[].{A:EvalActionName,D:EvalDecision}' --output text
done

# Roles: 33 total, 26 non-service-linked; screened for broad policies.
for R in $(aws iam list-roles --query 'Roles[].RoleName' --output text \
             | tr '\t' '\n' | grep -v '^AWSServiceRole'); do
  aws iam list-attached-role-policies --role-name "$R" \
    --query 'AttachedPolicies[].PolicyName' --output text
done

aws iam get-role-policy --role-name adr-intake-lambda-role \
  --policy-name adr-intake-lambda-inline --query PolicyDocument
aws iam list-attached-role-policies --role-name adr-intake-lambda-role

⚠️ simulate-principal-policy was the tool that produced a false negative on this project once alreadyAGENTS.md Q22, where eight checks returned empty because 2>/dev/null was hiding an InvalidInput error caused by a zsh parameter-expansion bug ($ACCT:user/ parses :u as a history modifier). The loop above brace-quotes ${U} for that reason, prints one line per principal so a silently-skipped iteration is visible as a missing row, and suppresses nothing. Five rows, or the run did not happen.