Scope reactor lookups to the calling tenant - #42
Conversation
|
Claude finished @bruth's task in 3m 3s —— View job PR Review
SummaryThe core change is sound. One issue worth deciding before merge: Cross-tenant mutation gap (medium) — Fix sketch — add an ownership check before the destructive call (same pattern for // In DeleteReactor, after requireTenant():
if _, err := s.GetReactor(ctx, name); err != nil {
return err // ErrReactorNotFound if out-of-scope or truly absent
}This reuses the scope logic already introduced in this PR and avoids duplicating the prefix logic. Minor (noted inline): Everything else looks good:
|
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if !s.reactorInTenantScope(handle.cons.CachedInfo().Config.FilterSubjects) { |
There was a problem hiding this comment.
GetReactor always pays a JetStream round-trip before the scope check, so a cross-tenant request for a real consumer (one owned by another tenant) takes a longer code path than a request for a non-existent consumer. Both return ErrReactorNotFound to the caller, but an adversarial tenant that can measure latency gets a weak oracle for "does a durable by this name exist for some other tenant?"
For most Rita deployments this is negligible, but worth noting if tenants are considered mutually adversarial.
A tenant-scoped handle saw every tenant's reactors through GetReactor and ListReactors, inconsistent with the subject-level isolation enforced everywhere else on a tenant store. Filter both by the tenant subject prefix carried in each durable's FilterSubjects: a scoped handle now sees only its own reactors, while an unscoped handle keeps the full cross-tenant view. Durable names remain a single stream-wide namespace, so tenants must still name reactors distinctly; mutations and the untenanted path are unchanged. Signed-off-by: Byron Ruth <byron@synadia.com>
Signed-off-by: Byron Ruth <byron@synadia.com>
e6b3faa to
be5fd4c
Compare
|
Claude finished @bruth's task in 2m 4s —— View job Review of updated PR
SummaryThis update closes the gap flagged in the previous review.
No new correctness or security issues found. Two pre-existing, non-blocking notes carried over from the first pass:
The isolation model (unscoped-sees-all, scoped-sees-mine, ownership derived from the same subject-prefix logic used at creation) is internally consistent across list/get/update/delete/upsert, and the test suite now covers both the read and write side of that contract. |
Summary
On a tenant store,
GetReactorandListReactorsignored the handle's tenantand returned reactors across all tenants — inconsistent with the subject-level
isolation enforced everywhere else on a tenant store. This scopes both lookups
to the calling handle's tenant.
es.Tenant("acme")) now lists only its own reactors,and
GetReactorreturnsErrReactorNotFoundfor another tenant's durable.durable's
FilterSubjects(exactly how Create/Update build them). A reactorwith no filters, or filters spanning more than one tenant (e.g. a consumer
created outside Rita), is claimed by no tenant and appears only in the
unscoped listing.
name reactors distinctly. Mutations and the untenanted path are unchanged.
Addresses @bruth's review comment on #41:
#41 (comment)
Tests
Adds
TestTenantReactorLookupScoped: two tenants each create a reactor, thenasserts each scoped handle lists only its own, the unscoped handle lists both,
and a cross-tenant
GetReactorreturnsErrReactorNotFound.Follow-up
PR #41's
docs/tenancy.mdand thereactor.gogodoc currently describe theprior "lookups are not tenant-filtered" behavior; #41's docs will be updated to
match once this merges.