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 ¶
- func NewClaimsInterceptor() connect.Interceptor
- func WithClaims(ctx context.Context, c *Claims) context.Context
- func WithExtraPartitions(ctx context.Context, partitionIDs ...string) context.Context
- func WithSkipEnforcement(ctx context.Context) context.Context
- type Capabilities
- type Claims
- type ModelInfo
- type Provider
- type Tenanted
- type Unscoped
- type UnscopedMarker
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 ¶
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 ¶
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 ¶
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 ¶
ClaimsFromContext returns the bound Claims with graceful fallback:
- Explicit Claims bound via WithClaims (fastest path).
- Derived from security.AuthenticationClaims if present in ctx (job workers / services that haven't run the tenancy interceptor still get correct enforcement).
- nil — caller is unscoped (system services, migrations).
func (*Claims) ExtendPartitions ¶
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.
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 ¶
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.