Skip to content

Approval-link follow-ups: expired-link message, pending release on approval, canonical link host#18

Merged
phosphore merged 2 commits into
masterfrom
lorenzo/approval-link-expiry
Jul 22, 2026
Merged

Approval-link follow-ups: expired-link message, pending release on approval, canonical link host#18
phosphore merged 2 commits into
masterfrom
lorenzo/approval-link-expiry

Conversation

@phosphore

@phosphore phosphore commented Jul 22, 2026

Copy link
Copy Markdown
Member

Context

PAM approval proposal tokens expire after APPROVAL_TIMEOUT (currently the 1h default). This means most cross-timezone approvals die on the 1h window, and the error message sends people bug-hunting instead of re-requesting.

The wavemm-iam tf will raise APPROVAL_TIMEOUT to 12h (companion PR there). This PR contains the app-side changes that make that safe and fix the two UX problems it exposed.

Changes

  1. Expired links say soAbstractProposalHandler.accept() now distinguishes an expired token from an invalid one: "This approval link has expired: links can only be approved within 12 hours of the request being made. Ask the requester to submit a new request." The expiry probe parses the JWT payload without signature verification and is only ever used to pick the error message — both paths still deny access, so a forged exp gains nothing.

  2. Approval releases the SECOP-1101 pending reservation — previously the duplicate-submit marker was only released when propose failed, and otherwise lived until token expiry. With a 12h token TTL and 2h elevations, an approved request would have blocked same-day re-requests of the same group for the remaining ~10h. Proposal.onCompleted() now releases both key variants (auto-selected + hand-picked; the variant isn't recoverable from the token — releases are idempotent and best-effort, Firestore TTL reaps stragglers).

  3. Canonical link host, staging-safe — new optional PUBLIC_BASE_URL setting + CanonicalLinkBuilder. Approval links inherit the host the requester was browsing, so a request made on wavemm-workspace-groups-pam.nw.r.appspot.com fans out appspot links to every reviewer even though prod is pam.wavemm.net. With PUBLIC_BASE_URL set, links are rewritten to the canonical URL only when the request arrived on the default appspot host. Versioned hosts (rev-…-dot-…appspot.com) never match — they are the documented staging surface (promote_traffic=false smoke tests), and staging-generated links keep pointing at the staged version. Custom domains and localhost pass through unchanged. Unset = exact upstream behaviour.

Also: restored the executable bit on sources/mvnw (it was committed non-executable).

Testing

  • sh mvnw test -DskipITs=true: 1070 tests, 0 failures (ITest* need a GCP test environment).
  • New tests: expired vs invalid vs malformed token messages; onCompleted releases both reservation variants then notifies; CanonicalLinkBuilder host matrix (default/legacy appspot rewritten; versioned -dot-, custom domain, localhost passthrough; scheme forcing preserved); PUBLIC_BASE_URL parsing/validation.
  • Verified against today's incident data: the failing token round-trips TokenObfuscator cleanly and its signature verifies OK — expiry was the only failure, which this message change would have surfaced immediately.

Rollout

Deploy this before or together with the wavemm-iam APPROVAL_TIMEOUT=720 bump (usual target submodule re-point). The env vars are inert until set: without PUBLIC_BASE_URL links behave exactly as today, and item 2 only ever releases markers.

🤖 Generated with Claude Code

phosphore and others added 2 commits July 22, 2026 10:24
…proval, canonical link host

Three related fixes around proposal tokens, prompted by the 2026-07-22
production incident where every overnight approval link 403'd with
"The proposal token is invalid" (the tokens were merely expired — see
trace db0322e6a6b1861fb22321941bf771e6):

1. accept() now distinguishes an expired token from a genuinely
   invalid one and says so, including the configured validity window
   and what to do next. The expiry probe parses the payload without
   signature verification and only ever selects the error message.

2. Approving a proposal now releases the SECOP-1101 duplicate-submit
   reservation (both key variants, since the variant isn't encoded in
   the token). Without this, raising APPROVAL_TIMEOUT would block
   re-requests of the same group for the full token lifetime after an
   approval.

3. New optional PUBLIC_BASE_URL setting + CanonicalLinkBuilder:
   approval links generated from requests on the app's *default*
   appspot host are rewritten to the canonical URL (pam.wavemm.net in
   prod). Versioned staging hosts (rev-…-dot-…appspot.com), custom
   domains, and localhost are never rewritten, so promote_traffic=false
   smoke tests keep versioned links.

Full unit suite: 1070 tests, 0 failures (ITest* skipped, need GCP env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@phosphore
phosphore merged commit 5bc231a into master Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants