feat(admin): proactive Stripe key permission self-test on /admin/stripe (BUNYIP-532) #530

Merged
longjacksonle merged 1 commit from feat/BUNYIP-532-stripe-permission-self-test into main 2026-08-13 04:01:52 +02:00

What and why

Resolves BUNYIP-532. The reported incident: an admin saved a Stripe restricted key that was missing the "Webhook Endpoints" permission, and creating a webhook endpoint then failed. BUNYIP-516 already explains such a failure with bunyip-authored copy that names the exact permission, but only reactively, at the moment the admin attempts the operation. This adds a proactive self-test so the admin catches a missing or wrong permission up front, before a customer or a webhook hits it.

What ships

  • A "Test key and permissions" button on /admin/stripe.
  • A read-only API endpoint (GET /v1/admin/stripe/permissions) that probes the saved key with harmless list reads and classifies each result: granted, missing (403 / more_permissions_required), key rejected (401 / expired / invalid) or inconclusive. It reuses the BUNYIP-516 classification predicates, so the proactive test and the reactive message agree on what a Stripe failure means.
  • Live-tested permissions: Products, Prices, Webhook Endpoints. These are the write-heavy operations the admin drives from this page, and the Stripe calls whose errors the shared dunite-stripe crate classifies today (list_products, list_prices, list_webhook_endpoints; the webhook listing gained HTTP-status handling in DUNITE-10, so a 403 there is a classified error and not a silent empty list).
  • A results panel that shows each tested permission's verdict, reports a rejected key once (not as N missing permissions), and tells an unconfigured account to save a key first. It also lists the checkout-time permissions bunyip needs (Customers, Checkout Sessions, Subscriptions, Invoices) as required, marked "Not tested here" so nothing is forgotten without falsely claiming a verdict.
  • Probing runs only when the admin clicks Test (check=permissions), so a plain page load fires no extra Stripe reads.

Explicitly out of scope (follow-up)

Live-probing the checkout-time permissions. The dunite-stripe methods for Customers / Checkout Sessions / Subscriptions / Invoices collapse Stripe's error into a generic internal / not-found error, so a 403 (missing permission) cannot be told apart from a 404 (object absent) there. Classifying them needs a dunite-stripe change (a classified read probe per resource, or a deliberately-invalid create that forces a 400 when authorized vs a 403 when not, which would also catch a Read-only grant where Write is needed). Until then the panel lists them as required without testing them. I can open a dunite ticket for this if you want it.

Tests and verification

  • New domain unit tests: classify_probe maps Ok / 403 / 401 / other correctly, key-rejected takes precedence, and permission keys are stable and unique.
  • New API unit tests on the pure report builder: unconfigured lists all untested, a missing probed permission is flagged while the checkout ones stay untested, a rejected key sets key_status once, and each row carries its access level and stage.
  • just check-container green (fmt + clippy -D warnings + workspace tests). serde-compat guard green (all new response fields default).
  • Screenshot of the panel for the incident case (Webhook Endpoints missing) attached in the conversation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3

## What and why Resolves BUNYIP-532. The reported incident: an admin saved a Stripe restricted key that was missing the "Webhook Endpoints" permission, and creating a webhook endpoint then failed. BUNYIP-516 already explains such a failure with bunyip-authored copy that names the exact permission, but only reactively, at the moment the admin attempts the operation. This adds a proactive self-test so the admin catches a missing or wrong permission up front, before a customer or a webhook hits it. ## What ships - A "Test key and permissions" button on /admin/stripe. - A read-only API endpoint (GET /v1/admin/stripe/permissions) that probes the saved key with harmless list reads and classifies each result: granted, missing (403 / more_permissions_required), key rejected (401 / expired / invalid) or inconclusive. It reuses the BUNYIP-516 classification predicates, so the proactive test and the reactive message agree on what a Stripe failure means. - Live-tested permissions: Products, Prices, Webhook Endpoints. These are the write-heavy operations the admin drives from this page, and the Stripe calls whose errors the shared dunite-stripe crate classifies today (list_products, list_prices, list_webhook_endpoints; the webhook listing gained HTTP-status handling in DUNITE-10, so a 403 there is a classified error and not a silent empty list). - A results panel that shows each tested permission's verdict, reports a rejected key once (not as N missing permissions), and tells an unconfigured account to save a key first. It also lists the checkout-time permissions bunyip needs (Customers, Checkout Sessions, Subscriptions, Invoices) as required, marked "Not tested here" so nothing is forgotten without falsely claiming a verdict. - Probing runs only when the admin clicks Test (check=permissions), so a plain page load fires no extra Stripe reads. ## Explicitly out of scope (follow-up) Live-probing the checkout-time permissions. The dunite-stripe methods for Customers / Checkout Sessions / Subscriptions / Invoices collapse Stripe's error into a generic internal / not-found error, so a 403 (missing permission) cannot be told apart from a 404 (object absent) there. Classifying them needs a dunite-stripe change (a classified read probe per resource, or a deliberately-invalid create that forces a 400 when authorized vs a 403 when not, which would also catch a Read-only grant where Write is needed). Until then the panel lists them as required without testing them. I can open a dunite ticket for this if you want it. ## Tests and verification - New domain unit tests: classify_probe maps Ok / 403 / 401 / other correctly, key-rejected takes precedence, and permission keys are stable and unique. - New API unit tests on the pure report builder: unconfigured lists all untested, a missing probed permission is flagged while the checkout ones stay untested, a rejected key sets key_status once, and each row carries its access level and stage. - `just check-container` green (fmt + clippy -D warnings + workspace tests). serde-compat guard green (all new response fields default). - Screenshot of the panel for the incident case (Webhook Endpoints missing) attached in the conversation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3
feat(admin): proactive Stripe key permission self-test on /admin/stripe (BUNYIP-532)
All checks were successful
E2E / PR gate (pull_request) Successful in 24s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m43s
Create release / Create release from merged PR (pull_request) Has been skipped
781b003387
The reported incident was an admin saving a Stripe restricted key that lacked the "Webhook Endpoints" permission, so creating a webhook endpoint later failed. BUNYIP-516 already made that failure legible, but only reactively: the admin learned a permission was missing only when they attempted the operation that needed it, and some permissions are exercised much later, at customer checkout.

This adds a proactive "Test key and permissions" button on /admin/stripe. It calls a new read-only endpoint (GET /v1/admin/stripe/permissions) that probes the saved key with harmless list reads (list_products, list_prices, list_webhook_endpoints) and classifies each as granted, missing (403 / more_permissions_required), key rejected (401 / expired / invalid) or inconclusive, reusing the BUNYIP-516 classification predicates so the proactive test and the reactive message agree. These three are the write-heavy operations the admin drives from this page, and the only Stripe calls whose errors the shared dunite-stripe crate classifies today (the webhook listing gained HTTP-status handling in DUNITE-10, so a 403 there is a classified error and not a silent empty list).

The results panel shows each tested permission's verdict, reports a rejected key once rather than as N missing permissions, and tells an unconfigured account to save a key first. It also lists the checkout-time permissions bunyip needs (Customers, Checkout Sessions, Subscriptions, Invoices) as required but marks them "Not tested here" rather than claiming a verdict: the dunite-stripe methods for those collapse Stripe's error, so a 403 cannot be told from a 404 there. Classifying them is a separate dunite change, tracked as a follow-up.

Probing only runs when the admin clicks Test (check=permissions), so a plain page load fires no extra Stripe reads. Domain-layer helpers (ProbeStatus, classify_probe, StripePermission::key) and the report assembly are unit-tested without a live Stripe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-13 03:58:19 +02:00
longjacksonle deleted branch feat/BUNYIP-532-stripe-permission-self-test 2026-08-13 04:01:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!530
No description provided.