tenancy

package
v1.97.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package tenancy provides the storage-layer view of a principal's tenancy (Claims), the Provider abstraction for enforcement, and helpers to bind tenancy to a request context. The package is intentionally narrow: it depends only on security (for the default auth-claims derivation) and gorm.io/gorm.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewClaimsInterceptor

func NewClaimsInterceptor() connect.Interceptor

NewClaimsInterceptor returns a Connect interceptor that derives tenancy.Claims from auth claims and binds them to ctx for downstream code. The interceptor performs no database activity — it is cheap and safe for streaming RPCs.

Register after the authentication interceptor so auth claims are available when this interceptor reads them.

func WithClaims

func WithClaims(ctx context.Context, c *Claims) context.Context

WithClaims binds Claims to ctx. Returns the parent ctx unchanged when c is nil to avoid hiding a "no claims" signal behind a non-empty context.

func WithExtraPartitions

func WithExtraPartitions(ctx context.Context, partitionIDs ...string) context.Context

WithExtraPartitions reads the current Claims from ctx, extends them with the supplied partition IDs (preserving TenantID, AccessID, Skip), and binds the extended Claims to a child ctx. Returns ctx unchanged when no claims are present.

Use for service-on-behalf-of flows, cross-branch reporting, or any case where a principal legitimately needs visibility over additional partitions without changing tenant.

func WithSkipEnforcement

func WithSkipEnforcement(ctx context.Context) context.Context

WithSkipEnforcement returns a context that bypasses tenancy enforcement for any database query made through it. Use for migration scripts, admin tools, or system-level operations that legitimately need full-table access.

Internally this binds a Claims value with Skip=true. Providers honour Skip by performing no session binding for the connection, which makes the database-side policy's empty-match-all branch fire — i.e. every row is visible.

Types

type Capabilities

type Capabilities struct {
	// EnforcesAtStorage is true when the provider installs DB-side
	// rules that block access without per-query gating (e.g., RLS,
	// views). Used by the pool to skip any fallback scope it might
	// otherwise have applied.
	EnforcesAtStorage bool
}

Capabilities describes the runtime behaviour of a Provider.

type Claims

type Claims struct {
	// TenantID is the single tenant this principal belongs to.
	TenantID string

	// PartitionIDs are every partition this principal can access. One
	// principal may legitimately span multiple partitions (e.g., an
	// operator with access to several branches, an analyst aggregating
	// across groups). Single-partition principals carry one element.
	PartitionIDs []string

	// AccessID is an optional access-control hint propagated through
	// queue metadata and lifecycle hooks.
	AccessID string

	// Skip is true for internal/system callers that should bypass
	// tenancy enforcement. Providers honour Skip by performing no
	// session binding for the conn — the database-side policy's
	// empty-match-all branch then keeps every row visible.
	Skip bool
}

Claims is the storage-layer view of a principal's tenancy. Treat as immutable: every transformation returns a new instance.

func ClaimsFromAuth

func ClaimsFromAuth(ctx context.Context, auth *security.AuthenticationClaims) *Claims

ClaimsFromAuth derives Claims from auth claims using the frame default mapping:

TenantID     = auth.GetTenantID()
PartitionIDs = auth.GetPartitionIDs()
AccessID     = auth.GetAccessID()
Skip         = auth.IsInternalSystem() || security.IsTenancyChecksOnClaimSkipped(ctx)

Not overridable — callers needing different semantics build Claims directly and bind via WithClaims.

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) *Claims

ClaimsFromContext returns the bound Claims with graceful fallback:

  1. Explicit Claims bound via WithClaims (fastest path).
  2. Derived from security.AuthenticationClaims if present in ctx (job workers / services that haven't run the tenancy interceptor still get correct enforcement).
  3. nil — caller is unscoped (system services, migrations).

func (*Claims) ExtendPartitions

func (c *Claims) ExtendPartitions(partitionIDs ...string) *Claims

ExtendPartitions returns a new Claims with the supplied partition IDs merged in. Preserves TenantID, AccessID, and Skip unchanged. Empty strings are ignored; duplicates are removed; existing order is kept and new IDs appended after.

A nil receiver yields a fresh Claims carrying only the deduplicated non-empty partition IDs; TenantID, AccessID, and Skip default to zero values in that path.

func (*Claims) IsEmpty

func (c *Claims) IsEmpty() bool

IsEmpty reports whether the claims carry enforceable tenancy. Empty claims behave identically to "no claims attached" from a provider's perspective.

type ModelInfo

type ModelInfo struct {
	// Table is the SQL table name resolved through GORM's naming
	// strategy.
	Table string

	// TenantColumn is the SQL column carrying the tenant identifier
	// (conventionally "tenant_id").
	TenantColumn string

	// PartitionColumn is the SQL column carrying the partition
	// identifier (conventionally "partition_id").
	PartitionColumn string
}

ModelInfo describes one tenancy-enrolled model for Install. Built by the tenancy package via reflective enrollment; providers don't reimplement detection.

All fields are required. The conventional values are "tenant_id" / "partition_id"; the tenancy package's enrollment code populates them from those conventions, and providers MUST NOT assume defaults if a caller hand-builds a ModelInfo.

func EnrolledModels

func EnrolledModels(db *gorm.DB, models []any) ([]ModelInfo, error)

EnrolledModels filters the supplied migration models, returning ModelInfo for those that satisfy the Tenanted interface and do NOT satisfy Unscoped. Tenant and partition column names default to the conventional "tenant_id" / "partition_id"; future overrides can come from per-model tags but are not required today.

The supplied *gorm.DB is used only as a statement context for table name resolution — no queries are executed. GORM's statement parser honours any TableName() string method or gorm struct tags on the model, so custom table-name overrides are respected.

type Provider

type Provider interface {
	// Name returns a short, stable identifier ("postgres-rls") used in
	// logs and diagnostics.
	Name() string

	// Capabilities advertises what the provider does so the pool can
	// decide whether a complementary fallback (e.g., GORM scope) is
	// required.
	Capabilities() Capabilities

	// Install applies storage-side enforcement schema (RLS policies,
	// views, etc.) for the supplied models. Called once per migration.
	// Implementations MUST be idempotent — Frame re-runs migration on
	// every boot.
	Install(ctx context.Context, db *gorm.DB, models []ModelInfo) error

	// WireAdapter registers dialect-level hooks. Called once when the
	// pool is constructed, BEFORE any connection is opened. Providers
	// that enforce per-acquire (Postgres-RLS) register here.
	WireAdapter(adapter dialect.DialectAdapter) error

	// WireGorm registers GORM-level callbacks on the supplied *gorm.DB.
	// Called once per opened connection. Providers that enforce
	// per-query (alternative dialects without per-acquire hooks)
	// register here. Postgres-RLS implements as a no-op.
	WireGorm(db *gorm.DB) error
}

Provider installs and enforces tenancy isolation at the storage layer. Implementations are database-specific; the bundled Postgres provider uses Row-Level Security policies, others might use views, query rewriting, or a different scheme entirely.

type Tenanted

type Tenanted interface {
	GetTenantID() string
	GetPartitionID() string
	GetAccessID() string
	SetTenantID(string)
	SetPartitionID(string)
	SetAccessID(string)
}

Tenanted is the structural interface a model must satisfy to be enrolled in tenancy enforcement. data.BaseModel satisfies it; custom models that want enrollment can satisfy it explicitly.

The tenancy package never imports the data package — enrollment is purely structural, so downstream services can roll their own tenanted base type if needed.

type Unscoped

type Unscoped interface {
	TenancyUnscoped()
}

Unscoped opts a model out of tenancy enforcement. Implement this interface to skip RLS policy installation for the model's table. The canonical way to satisfy it is to embed UnscopedMarker:

type LookupTable struct {
    ID string
    tenancy.UnscopedMarker
}

type UnscopedMarker

type UnscopedMarker struct{}

UnscopedMarker is an empty struct satisfying Unscoped. Embed it in a model to opt out of tenancy enforcement.

func (UnscopedMarker) TenancyUnscoped

func (UnscopedMarker) TenancyUnscoped()

TenancyUnscoped implements Unscoped.

Directories

Path Synopsis
Package postgres provides the Postgres concrete tenancy.Provider.
Package postgres provides the Postgres concrete tenancy.Provider.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL