Files
cairnobs/enterprise/internal/tenant/tenant.go
T
jcoffey-dev 3eb0f4c589 Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a
new enterprise/ module (session issuance, audit logging, RBAC storage,
OIDC/SAML protocol wiring) that core never imports -- only calls over
HTTP. Found and fixed a real cross-tenant vulnerability in dashboards
(no tenant_id filtering at all) while writing the threat model doc.

Two things are explicitly NOT done, documented rather than hidden:
tenant isolation for log data itself (/query still shares one ClickHouse
connection and Tantivy index across every tenant -- RBAC controls who
can query, not what a query can see), and human SSO login (protocol
wiring exists, no HTTP handler calls it yet). See
docs/security/threat-model.md and docs/phase-4-runbook.md.

Also adds deploy/ (Go Operator + Helm chart, validated offline only --
no cluster was reachable in this environment).
2026-08-13 22:16:59 -07:00

68 lines
2.9 KiB
Go

// Package tenant is the single source of truth for "which tenant is
// this request for" -- see /docs/phase-4-isolation-design.md's "TenantID:
// an honest framing, not an oversold one" section before changing
// anything here.
//
// The unexported field on ID and the single production constructor make
// *accidental* misuse cheap to audit (grep for call sites) -- they do
// not make misuse impossible by the Go compiler alone. The real
// invariant: TrustFromValidatedSession has exactly one production call
// site, verified by CI (hack/check-tenant-boundary.sh) and code review
// at every change to this package. The database/index grant layer in
// internal/chrunner and internal/searchclient is the actual backstop.
// Do not add a second exported or reflection-accessible construction
// path (e.g. an UnmarshalJSON method) without re-reading that design
// doc section first -- it exists specifically because a future
// "convenience" constructor is the most realistic way this boundary
// gets quietly reopened.
package tenant
import "context"
// ID identifies a tenant. The zero value is not a valid ID -- always
// check the bool from FromContext.
type ID struct {
value string
}
func (id ID) String() string {
return id.value
}
// contextKey is unexported specifically so nothing outside this package
// can set or shadow the context value via context.WithValue with a
// string or exported key -- see the design doc's "context key collision"
// gap.
type contextKey struct{}
// FromContext is the only read path for a request's tenant.
func FromContext(ctx context.Context) (ID, bool) {
id, ok := ctx.Value(contextKey{}).(ID)
return id, ok
}
// WithContext attaches id to ctx. Called once, by auth middleware, right
// after TrustFromValidatedSession.
func WithContext(ctx context.Context, id ID) context.Context {
return context.WithValue(ctx, contextKey{}, id)
}
// TrustFromValidatedSession is the only construction path from a raw
// string. In production code, DO NOT CALL OUTSIDE auth middleware
// (internal/session) -- enforced by hack/check-tenant-boundary.sh, which
// greps *.go files (excluding _test.go) for call sites outside an
// allowlist. Other packages' tests calling this directly is expected and
// fine: test code isn't attacker-controlled the way a network-facing
// handler is, so there's no separate "test constructor" here -- an
// earlier draft of this design proposed one living in a _test.go file,
// on the mistaken assumption that would make it importable by other
// packages' tests as a compiler-enforced guarantee. It doesn't: Go never
// compiles _test.go files into what other packages (or other packages'
// tests) import, so a same-package-only test constructor would have been
// unreachable from anywhere outside this package, including its
// intended callers. This function, called directly, is simpler and
// actually works.
func TrustFromValidatedSession(raw string) ID {
return ID{value: raw}
}