sentry: keep a disabled project closed, and test the gate that exists

ResolveIngest was rewritten to attribute ingest by the org's publishable key —
the same key /v1/event takes — instead of a per-project DSN watermark. Two
things did not come along.

The project's own status stopped being consulted. Disabling a project is how a
tenant stops ingest without rotating an org-wide key, and ProjectStatus says as
much where it is declared, so an existing row that is not active now refuses.
Only an EXISTING row can refuse: a project first seen on this ingest is
provisioned active a line later, and a store that errors must not be read as
permission.

TestResolveIngest_FailsClosed still built per-project versioned keys and
rotated watermarks, none of which the code has any more — that was the red.
It now covers the gate as written: no resolver installed, a key the resolver
rejects, an empty key, and a resolver that answers ok while naming no org, all
closed; a good key resolves to the org it names and provisions the project.

TestResolveIngest_DisabledProjectFailsClosed was GREEN and proved nothing —
with no resolver installed every call returns false, so its assertion held for
the wrong reason. It installs one now, and checked both ways: it fails without
the status check and passes with it.

Full suite green (`go build ./...`, `go test -count=1 -skip '^TestEmailRejected$' ./...`).

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-08-09 23:24:07 -07:00
co-authored by Hanzo Dev
parent 29f0202e06
commit e80fa26967
2 changed files with 73 additions and 30 deletions
+8
View File
@@ -128,6 +128,14 @@ func (m *module) ResolveIngest(ctx context.Context, projectID valuer.UUID, prese
return valuer.UUID{}, false
}
orgID := iamidentn.OrgUUID(orgSlug)
// A project that already exists and has been disabled stops accepting ingest,
// which is what disabling one is for and what ProjectStatus documents. Only an
// existing row can say this: a project unseen until now is provisioned active
// just below, and a store that cannot answer must not silently open the door.
if _, _, status, found, err := m.projects.Resolve(ctx, projectID); err == nil && found &&
status != sentrytypes.ProjectActive {
return valuer.UUID{}, false
}
m.ensureProject(ctx, orgID, projectID)
return orgID, true
}
+65 -30
View File
@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/hanzoai/o11y/pkg/identn/iamidentn"
"github.com/hanzoai/o11y/pkg/modules/errortracking"
"github.com/hanzoai/o11y/pkg/modules/errortracking/implerrortracking"
"github.com/hanzoai/o11y/pkg/modules/sentry"
@@ -163,58 +164,92 @@ func TestIngest_WritesEventsAndIssues(t *testing.T) {
assert.Equal(t, int64(2), issues.Items[0].Count)
}
// TestResolveIngest_FailsClosed is the DSN gate: unknown project, disabled project,
// wrong key and a below-watermark (rotated) key all return ok=false; only a correct,
// current key resolves — to the OWNING org.
// withIngestKeyResolver installs a resolver for one test and puts back whatever
// was there. The resolver is package state read per request, so a test that left
// its own behind would decide the answer for every test after it.
func withIngestKeyResolver(t *testing.T, fn IngestKeyResolver) {
t.Helper()
prev := getIngestKeyResolver()
SetIngestKeyResolver(fn)
t.Cleanup(func() { SetIngestKeyResolver(prev) })
}
const testOrgSlug = "acme"
// resolverFor answers only for key, and names no org for anything else — a stand-in
// for the host asking IAM whether a publishable key belongs to somebody.
func resolverFor(key string) IngestKeyResolver {
return func(_ context.Context, presented string) (string, bool) {
if presented == key {
return testOrgSlug, true
}
return "", false
}
}
// TestResolveIngest_FailsClosed is the ingest gate. Attribution is the org's
// publishable key — the same one /v1/event takes — resolved by the host, so what
// has to hold is that every way of not presenting a good one stays shut: no
// resolver installed at all, a key the resolver rejects, an empty key, and a
// resolver that answers ok with no org. Only a key naming an org resolves, and the
// project in the DSN is born under that org on first sight.
func TestResolveIngest_FailsClosed(t *testing.T) {
ctx := context.Background()
h := newModuleHarness(t)
org := valuer.GenerateUUID()
proj := mustProject(t, h, org, "web")
pid := proj.Project.ID
pid := valuer.GenerateUUID()
const goodKey = "pk-live-good"
validKey := implerrortracking.PublicKeyForVersion([]byte(testSecret), pid.String(), 1)
// The default. A host that never installed a resolver ingests nothing.
_, ok := h.mod.ResolveIngest(ctx, pid, goodKey)
assert.False(t, ok, "no resolver installed must not resolve")
// Valid key + active project -> resolves to the owning org.
gotOrg, ok := h.mod.ResolveIngest(ctx, pid, validKey)
withIngestKeyResolver(t, resolverFor(goodKey))
gotOrg, ok := h.mod.ResolveIngest(ctx, pid, goodKey)
require.True(t, ok)
assert.Equal(t, org, gotOrg)
assert.Equal(t, iamidentn.OrgUUID(testOrgSlug), gotOrg, "resolves to the org the key names")
// Unknown project -> closed.
_, ok = h.mod.ResolveIngest(ctx, valuer.GenerateUUID(), validKey)
// The DSN's project is provisioned under that org, so nobody had to create it.
_, _, status, found, err := h.projects.Resolve(ctx, pid)
require.NoError(t, err)
require.True(t, found, "first keyed ingest provisions the project")
assert.Equal(t, sentrytypes.ProjectActive, status)
// A key the resolver does not recognise.
_, ok = h.mod.ResolveIngest(ctx, pid, "pk-live-someone-else")
assert.False(t, ok)
// Wrong key -> closed.
_, ok = h.mod.ResolveIngest(ctx, pid, "1:deadbeef")
assert.False(t, ok)
// Empty key -> closed.
// No key at all.
_, ok = h.mod.ResolveIngest(ctx, pid, "")
assert.False(t, ok)
// After rotation, the old v1 key is below the watermark -> closed; the new one works.
_, err := h.projects.Rotate(ctx, org, pid)
require.NoError(t, err)
_, ok = h.mod.ResolveIngest(ctx, pid, validKey)
assert.False(t, ok, "a below-watermark (pre-rotation) key must stop resolving")
v2 := implerrortracking.PublicKeyForVersion([]byte(testSecret), pid.String(), 2)
_, ok = h.mod.ResolveIngest(ctx, pid, v2)
assert.True(t, ok)
// A resolver that says yes but names no org is not an authorisation.
withIngestKeyResolver(t, func(context.Context, string) (string, bool) { return "", true })
_, ok = h.mod.ResolveIngest(ctx, pid, goodKey)
assert.False(t, ok, "ok with an empty org slug must not resolve")
}
// TestResolveIngest_DisabledProjectFailsClosed covers the other half: the key is
// good and the org is real, and the project still refuses because it was disabled.
// Turning a project off is how ingest is stopped without rotating an org's key.
func TestResolveIngest_DisabledProjectFailsClosed(t *testing.T) {
ctx := context.Background()
h := newModuleHarness(t)
org := valuer.GenerateUUID()
org := iamidentn.OrgUUID(testOrgSlug)
proj := mustProject(t, h, org, "web")
pid := proj.Project.ID
const goodKey = "pk-live-good"
withIngestKeyResolver(t, resolverFor(goodKey))
// Active first, so the disabled result below is the disabling and nothing else.
_, ok := h.mod.ResolveIngest(ctx, pid, goodKey)
require.True(t, ok, "an active project resolves with a good key")
// Disable the project directly in the store.
disableProject(t, h, org, pid)
key := implerrortracking.PublicKeyForVersion([]byte(testSecret), pid.String(), 1)
_, ok := h.mod.ResolveIngest(ctx, pid, key)
assert.False(t, ok, "a disabled project must not resolve even with a valid key")
_, ok = h.mod.ResolveIngest(ctx, pid, goodKey)
assert.False(t, ok, "a disabled project must not resolve even with a good key")
}
// TestReads_ForeignProjectDenied is the mandatory read isolation: a project id that