Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/tenant/tenant.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 9bde0f9a74b89d01980fa658e5253acda7528d0c |
| Branch | per-tenant-routing |
| Path or archive member | internal/platform/tenant/tenant.go |
| Original SHA-256 | 31a64d002141d526f93ff67182b80530540e17601f6b80686c6699ee778d8828 |
| Git blob SHA | abeebac32108bbf1e4ad2717c3cd347d092bbb7b |
| Reading-copy SHA-256 | 31a64d002141d526f93ff67182b80530540e17601f6b80686c6699ee778d8828 |
Every recorded occurrence
These 3 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/platform/tenant/tenant.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/platform/tenant/tenant.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/platform/tenant/tenant.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
Recovered source transcript
Download complete source text ↓192 lines · 7.8 KB
// Package tenant is the multi-tenancy correctness boundary.
//
// Tenant isolation is enforced by Postgres Row-Level Security, NOT by Go WHERE clauses. Every
// tenant-scoped request runs inside a transaction whose `app.current_org_id` GUC is set
// transaction-locally, so Postgres discards it at COMMIT/ROLLBACK and it can never leak to the next
// request that reuses a pooled connection (the pgx-pool gotcha, see docs/STACK_DECISIONS.md §2).
//
// Two safeguards stack here:
// - Go side: no validated org in context => RunInTenantTx refuses to run (fail closed).
// - DB side: an unset GUC makes current_setting(...,true) NULL, so the RLS policy matches zero
// rows (default deny).
//
// The org id MUST originate from a verified session/JWT claim resolved by auth middleware, never
// from a client-supplied body, header, or query parameter.
package tenant
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// ErrNoTenant is returned when tenant-scoped DB work is attempted without a validated organization.
var ErrNoTenant = errors.New("tenant: no organization_id in context (refusing to run unscoped query)")
// ctxKey is an unexported context key so a client cannot spoof the org via a context-key collision.
type ctxKey struct{}
var orgKey = ctxKey{}
// WithOrg stores the validated org id in ctx. Call this ONLY from auth middleware, with a value
// taken from verified claims.
func WithOrg(ctx context.Context, orgID string) context.Context {
return context.WithValue(ctx, orgKey, orgID)
}
// OrgFromContext returns the validated org id and whether a non-empty one is present.
func OrgFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(orgKey).(string)
return v, ok && v != ""
}
// principalCtxKey is a SEPARATE key type from ctxKey. Reusing ctxKey for the user would collide with
// orgKey, because every empty struct value of the same type is equal, so the two context lookups
// would return each other's value. A distinct type keeps the org and the principal independent.
type principalCtxKey struct{}
var principalKey = principalCtxKey{}
// Principal is the authenticated actor resolved from a validated session: the user id and the role
// used for authorization. Like the org, it MUST originate from auth middleware reading verified
// session state, never a client supplied value.
type Principal struct {
UserID string
Role string
}
// WithUser stores the validated principal in ctx. Call this ONLY from auth middleware.
func WithUser(ctx context.Context, p Principal) context.Context {
return context.WithValue(ctx, principalKey, p)
}
// UserFromContext returns the validated principal and whether one with a non-empty user id is
// present.
func UserFromContext(ctx context.Context) (Principal, bool) {
p, ok := ctx.Value(principalKey).(Principal)
return p, ok && p.UserID != ""
}
// Router resolves the leaf tenant DB for an organization in the database per tenant model: it acquires
// that tenant's own pool (and a release the caller must invoke). The per tenant pool manager satisfies
// it. A DB with a nil router is shared mode, where every request uses the one static Pool and tenant
// isolation is Row Level Security; with a router the DB is per_tenant mode, where each request physically
// routes to its tenant's database and RLS is defense in depth.
type Router interface {
Acquire(ctx context.Context, org uuid.UUID) (*DB, func(), error)
}
// DB wraps either a single pgxpool.Pool (shared mode, authenticating as the NOBYPASSRLS application role)
// or a Router (per_tenant mode) that resolves the request's tenant pool. Pool is nil in per_tenant mode.
type DB struct {
Pool *pgxpool.Pool
router Router
}
// New wraps a pool as a tenant-scoped DB (shared mode, or a leaf tenant DB the manager hands back).
func New(pool *pgxpool.Pool) *DB { return &DB{Pool: pool} }
// NewRouting builds a routing DB with no static pool: every access resolves the caller's tenant database
// from the router using the validated org in context. This is how per_tenant mode reaches tenant data
// while keeping every handler and RunInTenantTx call site unchanged.
func NewRouting(r Router) *DB { return &DB{router: r} }
// Close releases the underlying pool (a no op for a routing DB; the manager owns the tenant pools).
func (db *DB) Close() {
if db.Pool != nil {
db.Pool.Close()
}
}
// resolveLeaf returns the DB to actually run against for this request plus a release the caller MUST
// invoke. In shared mode it is the DB itself (no op release). In per_tenant mode it acquires the tenant's
// leaf DB by the validated org in context (fail closed when there is no validated org, so a request with
// no org still reaches zero data).
func (db *DB) resolveLeaf(ctx context.Context) (*DB, func(), error) {
if db.router == nil {
return db, func() {}, nil
}
orgStr, ok := OrgFromContext(ctx)
if !ok {
return nil, nil, ErrNoTenant
}
org, err := uuid.Parse(orgStr)
if err != nil {
return nil, nil, ErrNoTenant
}
return db.router.Acquire(ctx, org)
}
// Acquire exposes the request's leaf DB (and a release the caller MUST invoke) for the readers that touch
// a pool directly: the global reference catalog reads and the stored skill matcher. In shared mode it is
// a no op wrapper over the single pool; in per_tenant mode it routes to the tenant's own database.
func (db *DB) Acquire(ctx context.Context) (*DB, func(), error) { return db.resolveLeaf(ctx) }
// RunInTenantTx is THE correctness boundary. It:
// 1. requires a validated org id (fail-closed: no org => no query, before the pool is touched),
// 2. opens a transaction on a single pooled connection,
// 3. sets app.current_org_id transaction-locally via set_config(..., is_local=true) so it
// auto-resets at COMMIT/ROLLBACK and cannot leak across the pool,
// 4. runs fn against that same tx, then commits (rolling back on any error).
//
// All tenant-scoped repository work MUST go through this method.
func (db *DB) RunInTenantTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) (err error) {
orgID, ok := OrgFromContext(ctx)
if !ok {
return ErrNoTenant
}
// In per_tenant mode this routes to the org's own database (and refuses without a validated org);
// in shared mode leaf is this DB and release is a no op. The org GUC is still set below either way,
// so RLS stays armed as defense in depth inside the tenant database.
leaf, release, err := db.resolveLeaf(ctx)
if err != nil {
return err
}
defer release()
if leaf.Pool == nil {
return ErrNoTenant
}
tx, err := leaf.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tenant tx: %w", err)
}
// Rollback is a no-op once Commit succeeds; this guarantees the connection is released on every path.
defer func() { _ = tx.Rollback(ctx) }()
// Transaction-local GUC, parameterized (no SQL-injection surface). is_local=true is what makes
// this safe on a pooled connection.
if _, err = tx.Exec(ctx, "SELECT set_config('app.current_org_id', $1, true)", orgID); err != nil {
return fmt.Errorf("set tenant guc: %w", err)
}
if err = fn(ctx, tx); err != nil {
return err // defer rolls back
}
if err = tx.Commit(ctx); err != nil {
return fmt.Errorf("commit tenant tx: %w", err)
}
return nil
}
// Middleware resolves the org id from an already-validated request and stores it in context.
// resolveOrg MUST read verified session/JWT claims, never a raw client value. A request whose org
// cannot be resolved is rejected with 403 before any downstream handler runs.
func Middleware(resolveOrg func(*http.Request) (string, bool)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
orgID, ok := resolveOrg(r)
if !ok {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r.WithContext(WithOrg(r.Context(), orgID)))
})
}
}