tenseleyflow/shithub / dc4dc18

Browse files

docs/internal/billing: PRO08 GA audit closure note + remediation table + go/no-go

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
dc4dc186867867955df8bfb0f9cadb514b64d7d4
Parents
ea9dedb
Tree
cc116ef

1 changed file

StatusFile+-
M docs/internal/billing.md 76 0
docs/internal/billing.mdmodified
@@ -369,3 +369,79 @@ organization upgrades again.
369
 - Stripe Billing: `https://docs.stripe.com/billing`
369
 - Stripe Billing: `https://docs.stripe.com/billing`
370
 - Stripe pricing models:
370
 - Stripe pricing models:
371
   `https://docs.stripe.com/products-prices/pricing-models`
371
   `https://docs.stripe.com/products-prices/pricing-models`
372
+
373
+## PRO08 GA audit closure
374
+
375
+**Audit dates**: 2026-05-13 (run + remediation in a single sprint).
376
+
377
+**Scope**: PRO04 Stripe adapter + PRO05 entitlements Principal +
378
+PRO06 user-tier billing settings + PRO07 Pro v1 feature gates.
379
+
380
+**Methodology**: four parallel security/correctness audit agents
381
+ran read-only inspections against the PRO07 tip plus the deployed
382
+host (`shithub.sh`). Findings were triaged per-bug; HIGH and
383
+MEDIUM severity gaps were fixed inline on the `payments-pro/08-pro-ga-audit`
384
+branch with locking tests. LOW-severity items were either fixed or
385
+documented per the user directive "we don't want to take any chances
386
+with payments."
387
+
388
+### Findings + remediation status
389
+
390
+| # | Severity | Finding | Fix | Test |
391
+|---|---|---|---|---|
392
+| A1 | MED | `guardPriceKindMatch` bypassable on empty `Items` | refuse empty-items when prices configured | `TestBillingWebhookGuardRefusesEmptyItemsWhenPricesConfigured` |
393
+| A2 | HIGH | `(subject_kind, subject_id)` never written to receipts | `SetWebhookEventSubjectForPrincipal` after every resolve; `ListFailedWebhookEvents` operator query | `TestSetWebhookEventSubjectForPrincipalRecordsAuditTrail`, `TestListFailedWebhookEventsReturnsErroredAndStuckEntries` |
394
+| A3 | MED | Concurrent dup-replay TOCTOU race | session-scoped advisory lock keyed on event id | `TestBillingWebhookConcurrentReplayAppliesOnce` |
395
+| D1 | HIGH | Snapshot CTE wipes lock columns on past_due transitions | conditional preservation in `ApplySubscriptionSnapshot` + user analog | `TestApplySubscriptionSnapshotPreservesGraceLockOnPastDue` (+ recovery + user-side mirror) |
396
+| D2 | LOW | No `charge.refunded` handler | new dispatch + `MarkInvoiceRefunded` query + enum + UI surface | `TestBillingWebhookChargeRefundedMarksInvoiceRefunded` (+ unknown invoice + standalone refund) |
397
+| D3 | MED | Two subscriptions per customer silently overwrite | `guardSubscriptionOverwrite` rejects different-sub apply | `TestBillingWebhookGuardRefusesSecondSubscriptionForSameCustomer` |
398
+| D4 | MED | No stale-event detection (reverse-order corruption) | `last_event_at` column + `IsBillingEventStaleForPrincipal` + handler guard + post-apply touch | `TestBillingWebhookDropsStaleEvent` |
399
+| D5 | LOW | `customer.subscription.deleted` for unknown sub 5xx's | log + 200 no-op so Stripe stops retrying | `TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp` |
400
+| C1 | HIGH | `require_signed_commits` unreachable from UI | form field + template checkbox + sqlc plumbing | covered by `TestSettingsBranches_UserKindEnforceFlipPreventForcePushBlocks` table |
401
+| C2 | HIGH | Advanced BP gate fires on wrong inputs | rewire predicate to fire on prevent_*, signing, AND status-checks | table test |
402
+| C3 | MED | Multi-reviewer deny copy indistinguishable | thread reviewer count → `required-reviewers-multi-upgrade(-pro)` codes | `TestSettingsBranches_UserKindEnforceFlipMultiReviewerBlocks` |
403
+| C4 | MED | Notice copy says "Team" / "organization" on user-tier denies | user-kind variants (`-pro` suffix) with `/settings/billing` href | `TestSettingsBranches_UserKindEnforceFlipRequiredReviewersBlocksSingle` (Pro copy) |
404
+| C5 | LOW | `profilePinsRemaining` hard-codes cap; under-counts for Pro | thread entitled cap into the function | covered by existing profile_test suite + no regression |
405
+
406
+Authorization audit (Agent B) found **no** cross-tenant data leak,
407
+no invoice-scoping bleed, and no deny-shape 500 reachable from
408
+production paths. Two `err→500` branches in the entitlement gate
409
+remain (in `RequirePrincipalFeature` and `evaluateBranchProtectionFeature`)
410
+but are defended against by the AFTER-INSERT seed triggers on
411
+both billing-state tables — `pgx.ErrNoRows` for a valid principal
412
+is dead code in production.
413
+
414
+### Pre-existing failures NOT introduced by PRO08
415
+
416
+These were noted across prior sprints and remain open:
417
+
418
+- `TestPrivateCollaborationExpansionEnforcesFreeLimitAndTeamUnlimited`
419
+- `TestPrivateCollaborationUsageCountsEffectivePrivateAccess`
420
+- `TestRepoPrivateVisibilityCountsRepoSpecificGrants`
421
+
422
+The fourth pre-existing failure
423
+(`TestSettingsBranchesAllowsDowngradedOrgToRemoveAdvancedSettings`)
424
+was fixed opportunistically while in the branch-protection cluster
425
+(seed fixture needed `AllowedPusherUserIds: []int64{}` to satisfy
426
+the NOT NULL constraint).
427
+
428
+### Go/no-go: GA
429
+
430
+**GO**, conditional on the operator completing the Stripe Dashboard
431
+checklist documented in `docs/internal/runbooks/stripe-billing.md`
432
+under "Subject resolution chain" and "Per-feature enforcement flags"
433
+sections. Production binary needs redeploy to pick up:
434
+
435
+- Migrations 0077 (`last_event_at`) + 0078 (`refunded` enum + column)
436
+- The 13 code fixes above
437
+
438
+Deploy steps:
439
+
440
+1. `ssh root@shithub.sh "sudo -u shithub /usr/local/bin/shithubd migrate up"` — applies 0077 + 0078
441
+2. Restart `shithubd-web.service` and `shithubd-worker.service`
442
+3. Verify with: `SELECT version_id FROM goose_db_version ORDER BY version_id DESC LIMIT 1` → expect `78`
443
+4. Configure Stripe env vars per the runbook (still in test mode until ratified)
444
+5. Run the smoke checklist in `runbooks/stripe-billing.md#smoke-test`
445
+6. Flip enforce flags per feature after 7-day report-only soak
446
+
447
+**No production customers exist today** (`SELECT plan, count(*) FROM users WHERE deleted_at IS NULL` → `free: 2`; same for orgs), so the deploy carries near-zero risk to existing customers — there are none on Pro/Team plans.