Repository navigation
Rebase onto upstream GoogleCloudPlatform/jit-groups 2.5 - #19
Merged
Merged
Conversation
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…leCloudPlatform#747) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.18.0 to 5.19.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](mockito/mockito@v5.18.0...v5.19.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.19.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oudPlatform#746) Bumps com.google.apis:google-api-services-pubsub from v1-rev20250414-2.0.0 to v1-rev20250807-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-pubsub dependency-version: v1-rev20250807-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atform#745) Bumps `quarkus.version` from 3.25.0 to 3.25.3. Updates `io.quarkus:quarkus-bom` from 3.25.0 to 3.25.3 - [Release notes](https://github.com/quarkusio/quarkus/releases) - [Commits](quarkusio/quarkus@3.25.0...3.25.3) Updates `io.quarkus:quarkus-maven-plugin` from 3.25.0 to 3.25.3 --- updated-dependencies: - dependency-name: io.quarkus:quarkus-bom dependency-version: 3.25.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus:quarkus-maven-plugin dependency-version: 3.25.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#744) Bumps com.google.apis:google-api-services-cloudidentity from v1-rev20250701-2.0.0 to v1-rev20250805-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-cloudidentity dependency-version: v1-rev20250805-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#742) Bumps com.google.apis:google-api-services-admin-directory from directory_v1-rev20250707-2.0.0 to directory_v1-rev20250804-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-admin-directory dependency-version: directory_v1-rev20250804-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…eCloudPlatform#749) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.11.2 to 3.11.3. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](apache/maven-javadoc-plugin@maven-javadoc-plugin-3.11.2...maven-javadoc-plugin-3.11.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](actions/upload-pages-artifact@v3...v4) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…gleCloudPlatform#755) Bumps com.google.auth:google-auth-library-oauth2-http from 1.37.1 to 1.39.0. --- updated-dependencies: - dependency-name: com.google.auth:google-auth-library-oauth2-http dependency-version: 1.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atform#757) Bumps `quarkus.version` from 3.25.3 to 3.26.3. Updates `io.quarkus:quarkus-bom` from 3.25.3 to 3.26.3 - [Release notes](https://github.com/quarkusio/quarkus/releases) - [Commits](quarkusio/quarkus@3.25.3...3.26.3) Updates `io.quarkus:quarkus-maven-plugin` from 3.25.3 to 3.26.3 --- updated-dependencies: - dependency-name: io.quarkus:quarkus-bom dependency-version: 3.26.3 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus:quarkus-maven-plugin dependency-version: 3.26.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…loudPlatform#760) Bumps `surefire-plugin.version` from 3.5.3 to 3.5.4. Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](apache/maven-surefire@surefire-3.5.3...surefire-3.5.4) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](apache/maven-surefire@surefire-3.5.3...surefire-3.5.4) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#759) Bumps com.google.apis:google-api-services-secretmanager from v1-rev20250704-2.0.0 to v1-rev20250902-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-secretmanager dependency-version: v1-rev20250902-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…gleCloudPlatform#758) Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/google/gson/releases) - [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md) - [Commits](google/gson@gson-parent-2.13.1...gson-parent-2.13.2) --- updated-dependencies: - dependency-name: com.google.code.gson:gson dependency-version: 2.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atform#762) Bumps `quarkus.version` from 3.26.3 to 3.28.0. Updates `io.quarkus:quarkus-bom` from 3.26.3 to 3.28.0 - [Release notes](https://github.com/quarkusio/quarkus/releases) - [Commits](quarkusio/quarkus@3.26.3...3.28.0) Updates `io.quarkus:quarkus-maven-plugin` from 3.26.3 to 3.28.0 --- updated-dependencies: - dependency-name: io.quarkus:quarkus-bom dependency-version: 3.28.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus:quarkus-maven-plugin dependency-version: 3.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…gleCloudPlatform#763) Bumps com.google.auth:google-auth-library-oauth2-http from 1.39.0 to 1.39.1. --- updated-dependencies: - dependency-name: com.google.auth:google-auth-library-oauth2-http dependency-version: 1.39.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…eCloudPlatform#766) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.11.3 to 3.12.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](apache/maven-javadoc-plugin@maven-javadoc-plugin-3.11.3...maven-javadoc-plugin-3.12.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#767) Bumps com.google.apis:google-api-services-secretmanager from v1-rev20250902-2.0.0 to v1-rev20250926-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-secretmanager dependency-version: v1-rev20250926-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#768) Bumps com.google.apis:google-api-services-admin-directory from directory_v1-rev20250804-2.0.0 to directory_v1-rev20250930-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-admin-directory dependency-version: directory_v1-rev20250930-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…leCloudPlatform#772) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.19.0 to 5.20.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](mockito/mockito@v5.19.0...v5.20.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.20.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atform#773) Bumps `quarkus.version` from 3.28.0 to 3.28.3. Updates `io.quarkus:quarkus-bom` from 3.28.0 to 3.28.3 - [Release notes](https://github.com/quarkusio/quarkus/releases) - [Commits](quarkusio/quarkus@3.28.0...3.28.3) Updates `io.quarkus:quarkus-maven-plugin` from 3.28.0 to 3.28.3 --- updated-dependencies: - dependency-name: io.quarkus:quarkus-bom dependency-version: 3.28.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus:quarkus-maven-plugin dependency-version: 3.28.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#775) Bumps com.google.apis:google-api-services-secretmanager from v1-rev20250926-2.0.0 to v1-rev20251004-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-secretmanager dependency-version: v1-rev20251004-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#776) Bumps com.google.apis:google-api-services-iamcredentials from v1-rev20250717-2.0.0 to v1-rev20251009-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-iamcredentials dependency-version: v1-rev20251009-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#770) Bumps com.google.apis:google-api-services-cloudidentity from v1-rev20250805-2.0.0 to v1-rev20250923-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-cloudidentity dependency-version: v1-rev20250923-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atform#777) Bumps `quarkus.version` from 3.28.3 to 3.29.0. Updates `io.quarkus:quarkus-bom` from 3.28.3 to 3.29.0 - [Release notes](https://github.com/quarkusio/quarkus/releases) - [Commits](quarkusio/quarkus@3.28.3...3.29.0) Updates `io.quarkus:quarkus-maven-plugin` from 3.28.3 to 3.29.0 --- updated-dependencies: - dependency-name: io.quarkus:quarkus-bom dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus:quarkus-maven-plugin dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…gleCloudPlatform#778) Bumps com.google.auth:google-auth-library-oauth2-http from 1.39.1 to 1.40.0. --- updated-dependencies: - dependency-name: com.google.auth:google-auth-library-oauth2-http dependency-version: 1.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#779) Bumps com.google.apis:google-api-services-admin-directory from directory_v1-rev20250930-2.0.0 to directory_v1-rev20251021-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-admin-directory dependency-version: directory_v1-rev20251021-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oogleCloudPlatform#780) Bumps com.google.apis:google-api-services-cloudidentity from v1-rev20250923-2.0.0 to v1-rev20251021-2.0.0. --- updated-dependencies: - dependency-name: com.google.apis:google-api-services-cloudidentity dependency-version: v1-rev20251021-2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#898) AppEngine sets `skipTests` during deployment, but not `skipITs`.
- Update Terraform modules to remove existing IAP branding resources - Force use of older Terraform provider which still supports these resoruces, and can still remove them The version constraint can be removed once it's safe to assume that existing deplyments have been updated to at least v2.5.
Use correct gcloud command to grant access to organization
Adds the DI graph, configuration plumbing, and Terraform toggle for the forthcoming Slack-based approval flow. Ships with SLACK_NOTIFICATIONS_ENABLED defaulting to false so this commit is a no-op at runtime — upstream Mail/ Debug ProposalHandler factory branches remain unchanged when the flag is off. Java - ApplicationConfiguration: SLACK_NOTIFICATIONS_ENABLED, SLACK_BOT_TOKEN_SECRET, SLACK_FIRESTORE_DATABASE env vars + isSlackConfigured() helper. - Application.produceProposalHandler: when the flag is on, fetch the bot token via SecretManagerClient, construct SlackProposalHandler, return. Falls through to existing SMTP/Debug branches otherwise. Fails loudly on Slack init errors (operator likely thinks notifications are wired). - New SlackProposalHandler extends AbstractProposalHandler — Phase 1 overrides log structured "would notify" lines; Phase 2 will add real Block Kit DMs and chat.update sibling propagation. - New SlackClient — async wrapper for users.lookupByEmail / chat.postMessage / chat.update. Phase 1 stubs return successful futures so the DI graph wires up. - New SlackMessageRegistry — Firestore-backed (channel, ts) tracker keyed by sha256(beneficiary, group, sorted recipients). Phase 1 stubs. Build - pom.xml: com.slack.api:slack-api-client 1.45.3, com.google.cloud:google-cloud-firestore 3.31.7. Terraform (jitgroups-appengine module) - New var.promote_traffic (default true). When false, the google_app_engine_service_split_traffic resource is not managed and the newly-deployed version sits at its versioned URL behind IAP without receiving production traffic — the staging mechanism documented in SLACK_INTEGRATION.md. See infrastructure/jit-groups/SLACK_INTEGRATION.md (in wavemm-iam) for the full design, rollback playbook, and phase plan. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate (#5) * Phase 2a: real Slack + Firestore implementations Replaces the Phase 1 stubs with working code so SlackProposalHandler actually delivers DMs and tracks them across the propose/approve flow. Still gated by SLACK_NOTIFICATIONS_ENABLED (env var, default false in the wavemm-iam Terraform), so this commit alone changes no production behaviour — it just makes the flag's "on" path do real work. What this enables (with flag on): Alice submits MPA join → all qualified reviewers (after group expansion) receive a Slack DM with a Block Kit "Approve in JIT" card. Bob approves in JIT → sibling DMs update in place to "✅ Already approved by Bob — no action needed", and Alice receives a confirmation DM. ActivationSelfApproved still doesn't notify on Slack (audit sink covers it). SlackClient (proposal/SlackClient.java) - Slack.getInstance().methods(token) wraps the bot token. - lookupUserByEmail / postDirectMessage / updateMessage are async via the project's CompletableFutures.supplyAsync. - users_not_found returns null (normal outcome, not an error). - chat.update soft-fails (logs + swallows) — losing a sibling cosmetic update isn't worth failing the approval. SlackMessages (new) - Block Kit DSL templates for the three message shapes: review request, sibling-update, beneficiary confirmation. - escapeBlockquote() neutralises crafted justifications that try to break out of the >justification block. - reviewerSiblingUpdate intentionally omits the action button so a second reviewer can't double-approve via stale Slack message. SlackMessageRegistry (proposal/SlackMessageRegistry.java) - Real Firestore client (constructed in Application.java factory) over the named "pam-slack-bridge" database. - Document key is sha256(beneficiary | groupId | sorted recipients) — stable across propose / accept, order-independent on recipients (covered by tests). - TTL field aligns with JWT expiry; field is created by wavemm-iam TF. SlackProposalHandler (proposal/SlackProposalHandler.java) - onOperationProposed: GroupResolver expands group principals, looks up each user, posts DMs, records the registry. Aggregates per- reviewer failures: at least one DM landing keeps the request viable; zero landed throws so the requester knows. - onProposalApproved: looks up registry by request key, chat.updates siblings (skipping the approver themselves), DMs the beneficiary, deletes the registry entry. Beneficiary DM still fires on registry miss. - All Slack/Firestore failures logged at WARN, never propagated to JIT request thread except the "zero DMs landed" catastrophic case. Application.java (factory wiring) - The Slack branch in produceProposalHandler now also injects CloudIdentityGroupsClient (for group expansion) and constructs a Firestore client locally with the named DB id. Local construction keeps the IAM blast radius scoped to this factory branch — the Firestore client is never produced for project-wide use. Tests - TestSlackMessageRegistry: requestKey is deterministic + invariant to recipient ordering + distinguishes beneficiary/group/recipients. - TestSlackMessages: the JWT URL is on the primary action button (the only way the reviewer can act); sibling-update has no action button; blockquote injection is escaped. - TestSlackProposalHandler: orchestration with mocked deps — DMs all reviewers, skips on lookupByEmail miss, throws when zero land, on approval updates only siblings (not the approver), notifies beneficiary even on registry miss. Phase 2b (next, separate PR): - GroupsResource: GET /reviewers (returns qualified peers + suggested- from-team flag) + POST accepts selectedReviewers[] filter. - JoinOperation.propose overload accepting a reviewerFilter. - SLACK_COPY_LINK_ENABLED env-flag for the "show approval URL in response + opt-out of Slack DMs" UX, defaulting to off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address #4 review feedback from @alex-young Three behavioural fixes from the post-merge review of #4: 1. JWT IDs use SecureRandom, not Random AbstractProposalHandler accepts the random source via constructor; MailProposalHandler and DebugProposalHandler both pass SecureRandom. SlackProposalHandler was passing plain Random — JWT IDs are activation-token nonces and must be unpredictable to resist enumeration. Drop the j.u.Random import, switch to SecureRandom. 2. Fail loudly when SLACK_NOTIFICATIONS_ENABLED=true but companion vars are missing The previous logic short-circuited via isSlackConfigured() (which ANDed the flag with bot-token-secret + firestore-database presence). That made a partial misconfig silently fall through to SMTP/501 — the operator's intent ("turn Slack on") would only manifest at the next MPA request, hours later, when nothing happens. Now we branch on slackNotificationsEnabled alone and throw at startup if either companion var is missing, with a message naming both the missing var and the rollback knob. isSlackConfigured() is dropped — its semantics were exactly the silent-fall-through case we don't want. 3. Single fingerprint() helper instead of duplicating the requestKey computation in onOperationProposed and onProposalApproved The two sides have to compute the SAME key or the lookup misses on approval and siblings never update. Extracting to one helper removes the drift surface. Returns a small RegistryFingerprint record carrying beneficiary, groupId, expanded reviewerEmails, and the SHA-256 key — same shape both sides need. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: add workflow_dispatch to enable manual runs Lets us re-trigger the build from the Actions tab without needing a push or PR. Also serves as the first CI run on the wavemm fork — when this PR opens, the workflow fires for the first time, exercising the Phase 1 (#4) and Phase 2a (#5) code that was merged before Actions was enabled on the fork. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: three compile errors caught by first CI run on the fork The fork's CI was disabled before #4 and #5 merged, so this is the first run that actually exercised mvn test. It surfaced three real issues, all narrow: 1. Application.java: runtime.projectId() returns a ProjectId record, not a String. FirestoreOptions.setProjectId expects a String. Use .id() to extract. 2. SlackProposalHandler.java: GroupResolver.expand() is package-private in the upstream com.google.solutions.jitaccess.auth package. The handler in com.google.solutions.jitaccess.web.proposal can't call it across packages. Make expand() public — the class is already public and the method's signature is suitable for external use; the missing modifier looks like an oversight in upstream rather than a deliberate visibility boundary. 3. SlackProposalHandler.java: catch (CompletionException | RuntimeException) is illegal because CompletionException extends RuntimeException — Java's multi-catch forbids related types. Drop CompletionException from the alternatives; RuntimeException covers it. The extracted cause via e.getCause() still unwraps the original SlackApiException / IOException from inside CompletableFuture.join's wrapper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two reasons: 1. Marks the artifact as a wavemm fork build, distinct from upstream 2.3.0 — useful when reading App Engine logs across versions or when troubleshooting a deploy-vs-upstream-bug question. 2. Forces the App Engine version_id (derived from the source ZIP hash) to change whenever we flip an env-var-only setting without touching Java source. App Engine doesn't allow mutating env vars on an existing version, and our terraform module derives version_id from source hash alone. Bumping the pom.xml between flag-only deploys keeps Terraform from trying to delete-and-recreate a version with the same ID (which App Engine rejects with 409). Subsequent flag-only deploys can bump the trailing integer (.2, .3, …) — bumping the patch number on every wavemm-side re-deploy is cheap and unambiguous in the App Engine version list. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ot causes (#8) Four small fixes shaken out of the first end-to-end test on the staged version: 1. produceProposalHandler is now @singleton Previously @dependent (CDI default for @produces with no scope), so every request that needed a ProposalHandler was rebuilding everything the factory constructs — including a fresh Firestore client and its gRPC ManagedChannel. Each abandoned channel surfaced as an ERROR- severity "ManagedChannel allocation site" stack trace in stderr when gRPC's leak detector eventually fired, polluting logs. Singleton- scope means one construction per JVM, no leak. 2. Filter the requester out of the expanded reviewer set AbstractProposalHandler / JoinOperation.propose drops the requester at the *principal* level (group ≠ user), so when the policy ACL names a group that the requester is in (the typical "team approves team" case used by 0-test-mpa-flow), the requester re-appears after GroupResolver.expand. We'd send them a DM about their own request. Drop them with a case-insensitive email match. 3. Log the root cause before re-throwing in onOperationProposed AbstractProposalHandler.propose catches AccessException | IOException and rethrows a generic AccessDeniedException("Creating a proposal failed", e), which surfaces as a 403 in the JIT UI with no detail. The first E2E test hit a Slack 'missing_scope' on users.lookupByEmail — the per-reviewer slack.dm.failed log captured the symptom, but the IOException("Slack DM delivery failed for every reviewer") that propagated up was emitted with no log of its own. Add explicit error logs at the three throw sites: - slack.fingerprint.failed (AccessException from GroupResolver) - slack.noReviewers (zero individuals after expansion) - slack.allDmsFailed (every per-reviewer DM failed) 4. Bump pom version to 2.3.0-wavemm.2 Forces a new App Engine version_id on the next deploy, so the bot token re-fetched from Secret Manager (rotated to add users:read.email scope) actually takes effect. App Engine doesn't allow mutating env vars on an existing version, and our version_id derives from the source ZIP hash — without the bump TF would fall into the same delete-and-recreate-with-same-id 409 trap. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t copy-link (#9) * Phase 2b + 3: reviewer picker UX + opt-out copy-link End-to-end change covering both halves of the picker UX. The default behaviour stays identical to Phase 2a (no picker selection → notify all qualified peers) so this is safe to deploy with the new endpoint live even before the frontend opts users into the picker. Backend ------- ProposalHandler.propose now takes a ProposeOptions record carrying two per-request flags: - reviewerFilter — narrow the recipients to a requester-selected subset. JoinOperation.propose intersects this with the policy ACL so a stale picker selection naming a non-approver is silently dropped, never widened. - notifyReviewers — when false, the JWT is generated and signed, onOperationProposed is skipped, and the requester gets the approval URL back to share manually. The interface keeps the upstream 2-arg propose signature as a default method delegating to the new 3-arg form, so existing tests and the DebugProposalHandler / MailProposalHandler subclasses don't need to change. SlackProposalHandler is unchanged — opt-out is handled upstream in AbstractProposalHandler before onOperationProposed is called. JitGroupContext.JoinOperation.propose gains a Set<EndUserId> filter overload. The 1-arg upstream form delegates with null (= no filter, upstream behaviour). The filter is applied to the APPROVE_OTHERS-derived set; group principals pass through unchanged because they're expanded later by the proposal handler. GroupsResource: - New GET /reviewers endpoint returning candidate peer reviewers with a 'suggested' flag for those sharing an approver group with the requester. Backed by a new ReviewerCandidates helper that computes the suggested subset via two CloudIdentityGroupsClient calls (listMembershipsByUser on the requester + group expansion on the shared approver groups), avoiding the O(N) per-candidate lookup pattern that would balloon for ACLs naming a broad group like engineering@. - POST now extracts selectedReviewers[] and notifyReviewers form fields before forwarding the rest to constraint inputs, and threads them through ProposeOptions. - JoinInfo gains approvalUrl + copyLinkEnabled, both gated by the new SLACK_COPY_LINK_ENABLED env var. approvalUrl is the same JWT-bearing URL the reviewers receive — surfaced to the requester only when copy-link mode is on. Frontend (vanilla JS + jQuery + MDC, matching upstream style) ------------------------------------------------------------- GroupInfoView._bind now fetches /reviewers and renders a checkbox list under the join form when status is JOIN_ALLOWED_WITH_APPROVAL. Suggested teammates are pre-checked and visually highlighted with a "team" badge. A simple search input filters the list client-side. When copyLinkEnabled is true, the picker also includes a "Notify selected reviewers in Slack" toggle (hidden+checkbox pair so the form submits notifyReviewers=false when unchecked). After submission, if the response carries approvalUrl, a copy-able link box renders with a Clipboard API copy button (text-select fallback when Clipboard isn't available). If /reviewers fails for any reason, the picker silently degrades — the form still submits and the backend defaults to notifying every qualified peer, matching Phase 2a. Build ----- pom.xml bumped to 2.3.0-wavemm.3. Forces a new App Engine version_id on the next deploy so env-var-only changes propagate (the same no-source-change quirk we hit between Phase 2a and the bot-token rotation). Tests ----- TestGroupsResource updated for the new injected `options` field and the 3-arg propose signature on Mockito stubs. TestAbstractProposalHandler updated for the JoinOperation.propose 2-arg form. Existing test assertions about JoinInfo continue to pass because the new fields default to null/false on the legacy paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * security hardening: P0/P1/P2 findings from senior security review Bumps to 2.3.0-wavemm.6. Addresses every finding except P1-5 (PII redaction in logs), P2-12 (dual-approver race test), and P2-14 (0-test-mpa-flow cleanup) which were marked acceptable. P0-1 — Slack mrkdwn injection in justification SlackMessages: render the requester-supplied justification as PlainTextObject (Slack treats those as literal) instead of mrkdwn, so <!here>, <https://evil/|click here>, *bold*, and <@U…> tokens reach reviewers as inert text. Cap at JUSTIFICATION_MAX_LENGTH (2500) with visible "[truncated]" suffix to prevent the 3 KB Block Kit field cap from silently failing chat.postMessage on long input. Inputs.copyValues: defense-in-depth absolute per-input cap (INPUT_MAX_LENGTH=16384) so a misconfigured/unbounded policy can't let a hostile requester ship multi-MB justifications. P1-3 — Bot-token rotation runbook (SLACK_INTEGRATION.md). P1-4 — Resource exhaustion guards SlackProposalHandler: Semaphore-gated per-reviewer fan-out with Options.maxConcurrentFanOut (default 8), bounding chat.postMessage pressure on Slack Tier 4 quota for large approver groups. GroupsResource.getReviewers: per-user Guava RateLimiter (1 req/s steady, ~1s burst) backing GET /reviewers so a polling client can't burn the SA's Cloud Identity quota. P1-6 — Catalog-side filter validation (defense in depth) JitGroupContext.JoinOperation.propose: when the policy ACL has no GroupId approver, reject reviewerFilter entries that aren't in the direct ACL — catches a future caller that forgets the REST-layer subset-after-expansion check. ACL-with-group still trusts the REST layer (catalog can't expand without Cloud Identity). P1-7 — GroupResolver.expand visibility doc + caller audit list, emphasising it is NOT itself an authorisation decision. P1-8 + P2-10 — Distinct audit signal for copy-link path OperationAuditTrail.joinProposed: 3-arg overload taking notifyReviewers; emits proposal/notify_reviewers label and "via copy-link" message body when false. Lets the BigQuery sink slice approval requests not visible to the normal reviewer set. P2-9 — Strict reviewer-email shape validation parseSelectedReviewers: regex ^[^@\s]+@[^@\s]+\.[^@\s]+$ rejects "@@@@", "user@", "spaces in@example.com", multi-@ forms etc. that the previous contains("@") check accepted. P2-11 — HMAC the registry document key under a Secret Manager salt SlackMessageRegistry.requestKey: HMAC-SHA-256 (was bare SHA-256) under a salt held in pam-slack-registry-key-salt. requestKey is now an instance method; constructor takes the salt and rejects a blank value. Application reads SLACK_REGISTRY_KEY_SALT_SECRET via Secret Manager at startup, same lifecycle as the bot token. P2-13 — Slack SDK log level + dependency-check application.properties: pin com.slack.api at INFO so a future DEBUG-everything switch can't leak the Bearer xoxb-… token into Cloud Logging. pom.xml: OWASP dependency-check 11.1.1 plugin (failBuildOnCVSS=8, unbound to default phases — invoked on demand from CI). Tests: 1008/1008 unit tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…10) Reported by a reviewer (Nico): clicking "Approve in JIT" the first time in a fresh browser session lands on the JIT homepage instead of the proposal-acceptance view. Cause: IAP's OAuth roundtrip during sign-in occasionally strips the URL's query string (where we encode the proposal token via `?f=/environments/.../proposal/{token}`). Clicking the Slack button a second time replays the URL with a fresh IAP cookie and works. Until Google preserves query strings reliably across the IAP OAuth roundtrip — or we add frontend detection of "homepage but came from accounts.google.com" — the cheapest fix is to tell reviewers inline in the DM what to do. Adding a one-liner to the existing ContextBlock; the rest of the message is unchanged. Bump to 2.3.0-wavemm.7.
…11) * Auto-narrow reviewers on empty picker selection (SECOP-952) When an MPA elevation request was submitted without picking specific reviewers, JoinOperation.propose fell back to the full policy ACL and SlackProposalHandler expanded any group in it to individuals — so a role whose approver list names a broad group (e.g. engineering@roles.wave.com) DM'd hundreds of people. GroupsResource.post now resolves an effective reviewer filter when the selection is empty: - small, group-free approver set -> unchanged (DM everyone; no Cloud Identity calls); - broad set (contains a group, or > AUTO_NARROW_BROADCAST_LIMIT) -> narrow to the requester's suggested teammates from ReviewerCandidates; - broad set with no teammate signal -> reject with a BadRequest so the requester picks at least one reviewer, rather than broadcasting. Same fail-safe if teammate resolution errors out. Adds three TestGroupsResource cases covering the null-filter, narrow, and reject paths. * Address max-effort review findings on auto-narrow (SECOP-952) Correctness fixes: - Resolve the reviewer filter inside the requiresApproval branch, so self-approve joins never pay reviewer resolution (or its failure modes) for a filter that would go unused. - notifyReviewers=false (copy-link) bypasses auto-narrow: it sends no DMs at all, so there is no broadcast to narrow; the pasted link keeps working for any qualified approver on broad ACLs. - Rate-limit the auto-narrow Cloud Identity fan-out with the same per-user limiter as GET /reviewers (blocking up to 1 s, then 429) so the POST path is not an unthrottled back door to the quota. - Catch UncheckedExecutionException from GroupResolver.expand; group expansion failures now hit the designed fail-safe instead of a 500. - Exclude the requester from the approver set BEFORE the broadcast-size guard (off-by-one: an ACL at the limit plus the requester was taking the expensive path). - On teammate-resolution failure with a group-FREE ACL, degrade to the bounded pre-SECOP-952 broadcast (null filter) instead of rejecting: the named approvers are already enumerated in the policy, and the "please pick someone" guidance is circular while the picker is degraded by the same outage. Group-bearing ACLs still reject. - Narrow to ReviewerCandidates' new uncapped `teammate` flag (score>0, capped at AUTO_NARROW_BROADCAST_LIMIT) instead of the `suggested` badge: the badge is a UI top-3 cap, and an audience of 3 was fragile (a couple of OOO teammates would strand the request) and coupled production fan-out to a presentation constant. Cleanups from the same review: - Extract approversFromPolicy()/reviewerCandidates() helpers (the qualified-approver extraction existed in three diverging copies). - Fix stale SELECTED_REVIEWERS_MAX javadoc (referenced the removed legacy DM-everyone path) and add the auto-narrow entry to GroupResolver's caller-audit list. Tests: 6 new TestGroupsResource cases (self-approve skip, copy-link bypass, requester-at-limit boundary, group-ACL narrowing, group expansion failure -> 400, group-free outage fallback -> null filter). * build: bump version to 2.3.0-wavemm.8
…te, picker copy (SECOP-1093/1094/1095) (#13) * picker: update helper copy for auto-narrowed empty selection (SECOP-1095) Since SECOP-952 (#11) an empty selection no longer notifies every qualified peer on broad ACLs — it narrows to the requester's closest teammates (small group-free sets still notify everyone). Update the three places in the picker UI that still described the old behaviour: the hint text, the default-unchecked comment, and the degraded-picker notice. * slack: update the approver's own DM after they approve (SECOP-1094) The sibling-update loop skipped the approver, leaving their DM frozen with a stale action link. In the picked-single-reviewer flow (sole recipient) nothing changed on screen at all, which read as a failed approval (live-test feedback, 2026-07-09). Every recorded DM is now updated in place: siblings get the existing 'already approved by X' blocks, the approver gets a new 'you approved this request' variant. * security: make proposal approval JWTs single-use (SECOP-1093) Proposal tokens are stateless signed JWTs (~1h validity) with no consumption record, so one approval link authorized unlimited re-grants for the token lifetime, each resetting the membership clock — observed live during the SECOP-952 validation (an approver double-clicked and re-granted a lapsed 15-minute membership). - Proposal.id() surfaces the JWT jti (crypto-random, minted at propose). - ProposalHandler gains verifyNotConsumed/markConsumed hooks; defaults are no-ops so the mail/debug handlers keep upstream semantics. - SlackProposalHandler enforces via SlackMessageRegistry: consumption markers live in the same Firestore collection as DM registry entries (HMAC-keyed under the existing salt, namespaced with a "consumed|" prefix, reaped by the existing expires_at TTL policy at token expiry). - ProposalResource checks consumption on GET (used link renders the "URL no longer valid" banner) and POST (before the approval executes), and records it right after a successful approval. - Failure mode is deliberately fail-open with ERROR logs: the JWT signature remains the authoritative authorization; consumption is anti-replay defense-in-depth and approvals must not hard-depend on Firestore availability. * build: bump version to 2.3.0-wavemm.9
The first production broad-role request auto-picked 15 teammates — judged still too loud. Split the constant: AUTO_NARROW_TEAMMATE_CAP=5 now bounds the set picked on the requester's behalf (candidates are affinity-ranked, so this is their 5 closest colleagues), while AUTO_NARROW_BROADCAST_LIMIT=15 remains the tolerance for DMing an explicitly-named small ACL (or no-signal fallback) in full.
…ard, org-scoped-role retry (SECOP-1096/1097/1099) (#15) * requester visibility: name the auto-selected reviewers (SECOP-1099) When the reviewer set is picked on the requester's behalf (empty picker selection), the requester now learns who was notified: - propose-time Slack DM ('sent to what we think are your closest teammates, since you didn't pick anyone specifically') that also teaches the picker — people watch DMs, not the PAM UI; - the JOIN_PROPOSED response carries notifiedReviewers + reviewersAutoSelected, rendered as a secondary line in the UI. Plumbing: ProposeOptions gains reviewersAutoSelected (back-compat constructor kept); AbstractProposalHandler gets an options-aware onOperationProposed overload defaulting to the legacy hook, so the mail/debug handlers are untouched. * double-submit protection for elevation requests (SECOP-1097) The elevation POST is slow on broad roles (Cloud Identity teammate fan-out + synchronous reviewer DM chains), so an impatient second click used to mint a second proposal token and a duplicate reviewer DM batch (observed live 2026-07-09: two identical 'Posted 15/15' 25s apart). - Frontend: ignore re-entrant submit clicks and disable the button while the POST is in flight. - Backend defense-in-depth: SlackProposalHandler skips the fan-out when a live registry entry already exists for the same (beneficiary, group, recipients) fingerprint — covers multi-tab / post-timeout retries the button guard can't. Fail-open on registry read errors. Not addressed here (bigger change, tracked in SECOP-1097): making the DM fan-out fully async so the POST returns before Slack delivery. * iam: retry SetIamPolicy through new-group member-domain propagation (SECOP-1096) First-ever join of an org-scoped role failed with a 400: JIT lazily creates the Google group, then immediately binds it org-wide, but the iam.allowedPolicyMemberDomains checker can't resolve the seconds-old group yet ('not in permitted organization'). The error previously fell through to NotAuthenticatedException; the user's manual retry succeeded once propagation cleared (2026-07-09, serviceusage-admin). modifyIamPolicy now recognises that transient 400 and retries with backoff (2s/4s/8s, budget separate from the 412 concurrency-control retries so neither starves the other). A genuine domain-restriction denial simply exhausts the bounded retries and surfaces as before. Predicate is deliberately narrow (400 + constraint id / 'not in permitted organization') and unit-tested. * build: bump version to 2.3.0-wavemm.11
…IAM retry, requester honesty (SECOP-1100/1101/1102/1103) (#17) * single-use approvals: atomic claim before execute + non-throwing hook (SECOP-1100) Fixes three defects from the 2026-07-10 adversarial review (live in prod): - Non-atomic check-then-act: verifyNotConsumed→execute→markConsumed(set upsert) let concurrent approvers all pass. Replace with an atomic Firestore create() claim taken immediately BEFORE execute (claimForApproval); the loser of the create is rejected. Release the claim if execute fails before granting so legitimate retries work. - onProposalApproved runs inside execute() AFTER the grant; an uncaught error there 500'd the approver and skipped audit+consume, leaving the token replayable. It's now a non-throwing wrapper (best-effort notification, ERROR log) around notifyProposalApproved. - Approver DM no longer asserts 'the requester has been notified' (the beneficiary DM is best-effort and may not have landed). GET keeps verifyNotConsumed for the friendly 'already used' banner; the claim is the real gate. Fail-open on Firestore errors is preserved. Tests updated: claim-before-execute ordering, reject-when-claim-lost, claim fail-open, release best-effort. * iam retry: honest terminal error + wider budget (SECOP-1102); requester copy honesty (SECOP-1103) SECOP-1102 (AbstractIamClient): - On exhausting the member-domain propagation retries, throw AccessDeniedException naming the iam.allowedPolicyMemberDomains policy instead of falling through to NotAuthenticatedException('Not authenticated') — a genuine external-principal denial now points operators at the org policy, not auth. - Widen the retry budget 3→4 (2/4/8/16 ≈30s) to cover the full 15–26s propagation lag the fix documents; only runs on first-ever join of an org-scoped role. SECOP-1103 (index.html): the JOIN_PROPOSED list is who we *requested*, not a delivery receipt (reviewers not in Slack silently get no DM, and the Slack DM is the delivery-accurate surface) — reword 'Sent to' → 'Reviewers requested' / 'We asked …'. Fix the picker hint that promised 'closest teammates' unconditionally to also cover the small-named-ACL broadcast. (Naming the full expanded broadcast audience is deferred — it needs the recipient list threaded back through propose, folded into SECOP-1101.) * dedupe redesign: pre-mint, atomic, expiry-aware, presence-stable (SECOP-1101) Replaces the SECOP-1097 duplicate-skip, which ran inside the Slack notification layer AFTER a token was minted, with a non-atomic existence check that (a) treated any registry doc as live so an expired request locked out re-submits for up to the ~24h TTL lag, (b) had a lookup→record TOCTOU under concurrency, and (c) left a second live token dangling on every duplicate. Now: AbstractProposalHandler.propose reserves a pending marker BEFORE minting via a new reserveProposal hook (default no-op=true for mail/debug); a live reservation → AccessDeniedException, no token minted. SlackProposalHandler reserves atomically through Firestore create(), and treats an existing-but-expired marker as free (compares expires_at to now, not TTL deletion) — closing both the lockout and the TOCTOU. Reservation released if propose fails after reserving. Fail-open on Firestore errors. Keys auto-selected requests on (beneficiary, group) so a re-submit whose picked set shifted (affinity/presence) is still the same request; user-picked sets include recipients. The old in-handler skip is gone. * build: bump version to 2.3.0-wavemm.12 (review fixes SECOP-1100-1103)
…SECOP-1098) (#16) * auto-narrow: rank auto-picked reviewers availability-first (SECOP-1098) On an empty picker selection we auto-pick the requester's closest teammates; ranking them purely by team affinity meant an asleep or off-shift teammate ranked the same as one online now. Since the point of picking is to minimise how long the requester waits, reorder the affinity shortlist availability-first before capping at 5: tier 0: currently active on Slack tier 1: in a working-hours timezone within 2h of the requester tier 2: everyone else (stable sort, so affinity breaks ties within a tier) Wiring: ProposalHandler gains a rankReviewersByAvailability default (no-op passthrough — mail/debug keep pure affinity); SlackProposalHandler overrides it using users.getPresence + users.info (tz), reusing the SlackClient it already holds, so GroupsResource needs no new dependency. Guarantees: - availability is a tiebreaker, never a filter — an offline teammate is deprioritised, not dropped, so an off-hours request still lands in the closest teammates' DMs; - fail-open: any Slack enrichment error falls back to affinity order; - bounded: only a 15-teammate shortlist is enriched, behind the picker's existing per-user rate limiter. NOTE: presence/tz behaviour under the bot token wants live validation on a real workspace before this deploys (it's fail-open, so no risk to the base flow if it misbehaves). Version -> 2.3.0-wavemm.12. * availability ranking: parallel enrichment, tz wrap, scope visibility, per-candidate degradation (SECOP-1104) Addresses the four defects the adversarial review found in the SECOP-1098 ranking (all in this unmerged #16): - Latency: enrich the shortlist CONCURRENTLY (async lookup→presence+tz chains, joined once) instead of ~3×15 strictly-sequential blocking calls on the already-slow propose POST. - Per-candidate degradation: each chain .exceptionally → 'unknown' (tier 2); one bad candidate no longer aborts ranking for all (the outer fail-open now only catches systemic errors). - Timezone wrap: compare offsets on the 24h circle (min(d, 86400-d)) so antimeridian teammates with identical local clocks rank as close, not ~24h apart. - Scope-error visibility: SlackClient.isActive/timezoneOffsetSeconds WARN on !isOk (e.g. missing users:read), and the ranking INFO log carries the tier distribution (active/tzNear/other) so a silently inert ranking is detectable. Tests: antimeridian promotion, per-candidate degradation vs wholesale fallback. Suite 1050/1050. NOTE: still to do before #16 merges — rebase onto the SECOP-1100..1103 fixes (PR #17) and reconcile the version bump; and the cross-phase lookupUserByEmail cache (ranking re-looks-up winners the fan-out then looks up again) is deferred as a pure optimization. * build: 2.3.0-wavemm.13 (availability ranking on top of the review fixes)
…proval, canonical link host (#18) * Approval-link follow-ups: expired-link message, pending release on approval, 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> * Bump version to 2.3.0-wavemm.14 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[claude] Rebases the Wave commits onto upstream
GoogleCloudPlatform/jit-groupsmaster (cb16848, 2.5.0). That brings in 111 upstream commits, including Java 25, Quarkus 3.39.4, Parameter Manager policy loading and the IAP Admin API deprecation mitigations.This PR is here so CI runs against the rebased history. Don't merge it through the GitHub UI: that would leave both the old and the rebased copies of the Wave commits on master. To land it, force-push
masterto this branch.Conflict resolutions
terraform/jitgroups-appengine/main.tf): upstream replaced both resources withremoved { lifecycle { destroy = false } }blocks, so Terraform forgets them without deleting them in GCP. That covers what Guard IAP brand and client against destructive replaces #12 guarded against (the live client deleted in the 2026-07-07 outage), so I took upstream's version and dropped Guard IAP brand and client against destructive replaces #12. Our App Engineservice_accountline is kept.permissionsblock and ourworkflow_dispatch.Before deploying
terraform applydropsgoogle_iap_brand.iap_brandandgoogle_iap_client.iap_clientfrom state. Check that the plan shows them as removed from state, not destroyed.java17tojava25.🤖 Generated with Claude Code