Skip to content

Commit e748913

Browse files
Manas Srivastavaclaude
andcommitted
feat(server): central audited chokepoint for customer-data drops + CI guard
Addresses the OPEN truehomie-db DROP incident root-cause class: an active customer's DB+role were dropped by an unidentified path with NO audit trail. The provisioner was a dumb executor — DeprovisionResource dropped whatever token it was handed and kept no record of its own. - guardedDrop (drop_chokepoint.go): the single sanctioned wrapper for a customer-data destruction. Every backend Deprovision dispatch in DeprovisionResource now routes through it. Emits a structured `event=provisioner.drop` audit log line (token, provider_resource_id, resource_type, backend, request_id, caller from gRPC peer) BEFORE the drop + instant_provisioner_drop_total{resource_type,backend,outcome}. This is the always-on, app-layer equivalent of the cluster's log_statement='ddl' trap. - drop_guard_test.go: AST-iterating CI guard (rule 18) that FAILS the build if any DROP DATABASE/ROLE/USER SQL literal, ACL DELUSER, mongo dropDatabase, or Database().Drop() call appears outside a sanctioned deprovision function reached through guardedDrop. A new un-audited drop path cannot merge. Proven non-vacuous: a synthetic un-sanctioned DROP is flagged; a commented DROP and a Collection.Drop (not customer data) are not. Behaviour of WHAT gets purged in the TTL reaper / team-deletion / user-DELETE flows is unchanged — the chokepoint only ADDs the audit line + metric around the existing dispatch. The larger deletion-intent proto field + terminality enforcement (needs platform-DB read, flag-gated) is designed + filed in docs/ci/DATA-INTEGRITY-DROP-PATH-AUDIT.md. make gate: green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b60bed3 commit e748913

4 files changed

Lines changed: 616 additions & 8 deletions

File tree

internal/server/drop_chokepoint.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package server
2+
3+
// drop_chokepoint.go — the SINGLE sanctioned wrapper for a customer-data
4+
// destruction (DROP DATABASE / DROP USER / dropDatabase / ACL DELUSER / namespace
5+
// teardown) on the provisioner.
6+
//
7+
// # WHY THIS EXISTS (truehomie-db DROP incident, 2026-06-03)
8+
//
9+
// An active Pro customer's Postgres database + role were dropped on the shared
10+
// postgres-customers cluster by an unidentified path, leaving NO audit_log row.
11+
// The provisioner is a "dumb executor": DeprovisionResource drops whatever
12+
// (token, provider_resource_id, resource_type) it is handed, and — critically —
13+
// it kept no record of its own. So even a drop that *did* go through the gRPC
14+
// funnel produced no provisioner-side trail to attribute it.
15+
//
16+
// guardedDrop closes that gap on the funnel: every backend Deprovision dispatch
17+
// in DeprovisionResource routes through here, which emits a structured DDL-audit
18+
// log line + Prometheus counter BEFORE invoking the backend. This is the
19+
// application-layer analogue of the postgres-customers `log_statement='ddl'` trap
20+
// set during the incident — but always-on, in the provisioner's own log stream,
21+
// and independent of the platform audit_log (which the provisioner cannot write,
22+
// having no platform-DB access).
23+
//
24+
// guardedDrop is the ONLY sanctioned customer-data drop wrapper. The CI guard in
25+
// drop_guard_test.go fails the build if a raw DROP DATABASE / DROP ROLE /
26+
// DROP USER / dropDatabase / ACL DELUSER literal appears in a provisioner backend
27+
// file that is NOT one of the sanctioned deprovision files reached through here —
28+
// so a NEW un-audited drop call site cannot be merged.
29+
//
30+
// SCOPE NOTE: guardedDrop does NOT (yet) refuse a drop based on resource
31+
// terminality / paid-tier — that requires the provisioner to read the platform
32+
// DB (resources.status/tier), a larger change designed + filed in
33+
// docs/ci/DATA-INTEGRITY-DROP-PATH-AUDIT.md (deletion-intent proto field +
34+
// terminality guard, flag-gated). guardedDrop is the always-safe, zero-behaviour-
35+
// change first layer: it makes every sanctioned drop auditable and a new
36+
// un-audited path unmergeable.
37+
38+
import (
39+
"context"
40+
"log/slog"
41+
42+
"github.com/prometheus/client_golang/prometheus"
43+
"github.com/prometheus/client_golang/prometheus/promauto"
44+
"google.golang.org/grpc/peer"
45+
46+
provisionerv1 "instant.dev/proto/provisioner/v1"
47+
48+
"instant.dev/provisioner/internal/circuit"
49+
)
50+
51+
// dropTotal counts every customer-data drop the provisioner performs through the
52+
// sanctioned chokepoint, labelled by resource_type, backend (shared|dedicated),
53+
// and outcome (ok|error|breaker_open). A spike in this counter is the alertable
54+
// signal that drops are happening at an abnormal rate (the truehomie incident
55+
// class). Eager-registered (NewCounterVec via promauto on the default registry)
56+
// so the series exists at /metrics before the first drop — but only label
57+
// combinations actually observed appear (standard *Vec behaviour). Rule 25:
58+
// alert + dashboard tile + catalog row ship alongside this metric.
59+
var dropTotal = promauto.NewCounterVec(prometheus.CounterOpts{
60+
Name: "instant_provisioner_drop_total",
61+
Help: "Customer-data drops performed by the provisioner via the sanctioned chokepoint, by resource_type, backend, and outcome.",
62+
}, []string{"resource_type", "backend", "outcome"})
63+
64+
// dropBackend distinguishes the shared multi-tenant backend from a dedicated
65+
// (per-tenant pod / Neon project) backend in the audit line + metric.
66+
type dropBackend string
67+
68+
const (
69+
dropBackendShared dropBackend = "shared"
70+
dropBackendDedicated dropBackend = "dedicated"
71+
)
72+
73+
// callerFromContext returns a best-effort identifier for the gRPC peer that
74+
// issued the request (e.g. "10.109.3.201:54422"), or "unknown" when the peer is
75+
// not available. This is the provisioner-side attribution that was MISSING in the
76+
// truehomie incident — every sanctioned drop now records who asked for it.
77+
func callerFromContext(ctx context.Context) string {
78+
if p, ok := peer.FromContext(ctx); ok && p.Addr != nil {
79+
return p.Addr.String()
80+
}
81+
return "unknown"
82+
}
83+
84+
// guardedDrop is the single sanctioned wrapper for a customer-data destruction.
85+
// It emits the DDL-audit log line + metric, then invokes fn (the backend
86+
// Deprovision) inside the supplied circuit breaker. Every Deprovision dispatch in
87+
// DeprovisionResource MUST call this rather than callBackendVoid directly.
88+
//
89+
// The audit line is emitted BEFORE the drop so a backend that hangs or crashes
90+
// the process mid-drop still leaves the "we are about to drop X" record — exactly
91+
// the trail that was absent in the incident.
92+
func (s *Server) guardedDrop(
93+
ctx context.Context,
94+
req *provisionerv1.DeprovisionRequest,
95+
backend dropBackend,
96+
breaker *circuit.Breaker,
97+
fn func() error,
98+
) error {
99+
resType := req.ResourceType.String()
100+
caller := callerFromContext(ctx)
101+
102+
// DDL-audit: the always-on, provisioner-side record of the drop. This is the
103+
// in-app equivalent of the cluster's log_statement='ddl' trap.
104+
slog.Info("provisioner.drop",
105+
"event", "provisioner.drop",
106+
"token", req.Token,
107+
"provider_resource_id", req.ProviderResourceId,
108+
"resource_type", resType,
109+
"backend", string(backend),
110+
"request_id", req.RequestId,
111+
"caller", caller,
112+
)
113+
114+
err := callBackendVoid(breaker, fn)
115+
116+
outcome := dropOutcome(err)
117+
dropTotal.WithLabelValues(resType, string(backend), outcome).Inc()
118+
119+
if err != nil {
120+
slog.Warn("provisioner.drop.failed",
121+
"event", "provisioner.drop.failed",
122+
"token", req.Token,
123+
"provider_resource_id", req.ProviderResourceId,
124+
"resource_type", resType,
125+
"backend", string(backend),
126+
"request_id", req.RequestId,
127+
"caller", caller,
128+
"outcome", outcome,
129+
"error", err,
130+
)
131+
}
132+
return err
133+
}
134+
135+
// dropOutcome maps a backend deprovision error to the metric outcome label.
136+
func dropOutcome(err error) string {
137+
switch {
138+
case err == nil:
139+
return "ok"
140+
case err == circuit.ErrOpen:
141+
return "breaker_open"
142+
default:
143+
return "error"
144+
}
145+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package server
2+
3+
// drop_chokepoint_test.go — unit tests for the sanctioned customer-data drop
4+
// chokepoint (guardedDrop). These assert the truehomie-incident invariant: every
5+
// drop the provisioner performs is recorded (metric + audit log) and attributed,
6+
// and the metric outcome label is correct for ok / error / breaker-open.
7+
8+
import (
9+
"context"
10+
"errors"
11+
"net"
12+
"testing"
13+
"time"
14+
15+
"github.com/prometheus/client_golang/prometheus/testutil"
16+
"google.golang.org/grpc/peer"
17+
18+
commonv1 "instant.dev/proto/common/v1"
19+
provisionerv1 "instant.dev/proto/provisioner/v1"
20+
21+
"instant.dev/provisioner/internal/circuit"
22+
)
23+
24+
// freshBreaker returns a closed breaker with a high threshold so a single
25+
// failure in a test never trips it (we drive the open state explicitly).
26+
func freshBreaker() *circuit.Breaker {
27+
return circuit.NewBreaker("test", 1000, time.Minute)
28+
}
29+
30+
func dropReq(token string) *provisionerv1.DeprovisionRequest {
31+
return &provisionerv1.DeprovisionRequest{
32+
Token: token,
33+
ProviderResourceId: "local:0",
34+
ResourceType: commonv1.ResourceType_RESOURCE_TYPE_POSTGRES,
35+
RequestId: "req-test",
36+
}
37+
}
38+
39+
func TestGuardedDrop_Success_IncrementsOkOutcome(t *testing.T) {
40+
s := &Server{breakers: circuit.NewBreakers()}
41+
before := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "ok"))
42+
43+
called := false
44+
err := s.guardedDrop(context.Background(), dropReq("tok-ok"), dropBackendShared, freshBreaker(), func() error {
45+
called = true
46+
return nil
47+
})
48+
if err != nil {
49+
t.Fatalf("guardedDrop returned error: %v", err)
50+
}
51+
if !called {
52+
t.Fatal("backend fn was not invoked")
53+
}
54+
after := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "ok"))
55+
if after != before+1 {
56+
t.Fatalf("ok counter: got %v want %v", after, before+1)
57+
}
58+
}
59+
60+
func TestGuardedDrop_BackendError_IncrementsErrorOutcome_AndPropagates(t *testing.T) {
61+
s := &Server{breakers: circuit.NewBreakers()}
62+
before := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "error"))
63+
64+
sentinel := errors.New("boom")
65+
err := s.guardedDrop(context.Background(), dropReq("tok-err"), dropBackendShared, freshBreaker(), func() error {
66+
return sentinel
67+
})
68+
if !errors.Is(err, sentinel) {
69+
t.Fatalf("expected sentinel error to propagate, got %v", err)
70+
}
71+
after := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "error"))
72+
if after != before+1 {
73+
t.Fatalf("error counter: got %v want %v", after, before+1)
74+
}
75+
}
76+
77+
func TestGuardedDrop_BreakerOpen_DoesNotInvokeBackend_AndRecordsBreakerOpen(t *testing.T) {
78+
s := &Server{breakers: circuit.NewBreakers()}
79+
before := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "breaker_open"))
80+
81+
// Trip a fresh breaker: threshold 1, so one recorded failure opens it.
82+
br := circuit.NewBreaker("test-open", 1, time.Minute)
83+
br.Record(errors.New("trip"))
84+
if br.Allow() {
85+
t.Fatal("breaker should be open after exceeding threshold")
86+
}
87+
88+
called := false
89+
err := s.guardedDrop(context.Background(), dropReq("tok-open"), dropBackendShared, br, func() error {
90+
called = true
91+
return nil
92+
})
93+
if !errors.Is(err, circuit.ErrOpen) {
94+
t.Fatalf("expected circuit.ErrOpen, got %v", err)
95+
}
96+
if called {
97+
t.Fatal("backend fn must NOT be invoked when breaker is open — a drop must never reach the backend through an open breaker")
98+
}
99+
after := testutil.ToFloat64(dropTotal.WithLabelValues("RESOURCE_TYPE_POSTGRES", "shared", "breaker_open"))
100+
if after != before+1 {
101+
t.Fatalf("breaker_open counter: got %v want %v", after, before+1)
102+
}
103+
}
104+
105+
func TestCallerFromContext_WithPeer_ReturnsAddr(t *testing.T) {
106+
ctx := peer.NewContext(context.Background(), &peer.Peer{
107+
Addr: &net.TCPAddr{IP: net.IPv4(10, 109, 3, 201), Port: 54422},
108+
})
109+
got := callerFromContext(ctx)
110+
if got != "10.109.3.201:54422" {
111+
t.Fatalf("callerFromContext: got %q want %q", got, "10.109.3.201:54422")
112+
}
113+
}
114+
115+
func TestCallerFromContext_NoPeer_ReturnsUnknown(t *testing.T) {
116+
if got := callerFromContext(context.Background()); got != "unknown" {
117+
t.Fatalf("callerFromContext: got %q want %q", got, "unknown")
118+
}
119+
}
120+
121+
func TestDropOutcome_Mapping(t *testing.T) {
122+
cases := []struct {
123+
name string
124+
err error
125+
want string
126+
}{
127+
{"nil", nil, "ok"},
128+
{"breaker", circuit.ErrOpen, "breaker_open"},
129+
{"other", errors.New("x"), "error"},
130+
}
131+
for _, c := range cases {
132+
if got := dropOutcome(c.err); got != c.want {
133+
t.Errorf("%s: dropOutcome=%q want %q", c.name, got, c.want)
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)