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).
68 lines
2.9 KiB
Go
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}
|
|
}
|