tenancy

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 12 Imported by: 0

README

tenancy

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

tenancy is a small explicit foundation for tenant isolation. It transports validated tenant identity and makes tenant-bound, system-wide, and deliberately unscoped work distinct in Go APIs. It does not authenticate callers, decide membership, or authorize access.

Core model

Tenant IDs are case-sensitive opaque ASCII values. They are preserved exactly, bounded to 128 bytes, and never inferred from arbitrary request data. Their String representation is redacted; use Value or serialization methods only at trusted transport and persistence boundaries.

id, err := tenancy.ParseTenantID("customer-42")
if err != nil {
    return err
}
scope, err := tenancy.NewTenantScope(id, tenancy.Metadata{})
if err != nil {
    return err
}
ctx, err = tenancy.WithScope(ctx, scope)

WithScope preserves the parent context and rejects any attempt to replace an existing distinct scope. RequireTenant, AssertTenant, and AssertScope provide fail-closed application and persistence seams.

System-wide work requires a deliberately constructed SystemCapability with an actor and purpose. This records intent; it does not grant permission. The application remains responsible for authorizing capability construction.

Opaque namespaces

NamespaceEncoder uses HMAC-SHA-256 over versioned, length-delimited scope, domain, and logical-key input. Its tn2_ lowercase hexadecimal output is safe for first-party provider resource names, including OpenSearch indexes. It prevents ambiguous concatenation and keeps raw tenant IDs and logical keys out of cache, search, queue, scheduler, idempotency, event, workflow, rate-limit, and telemetry namespaces. Callers own and rotate the encoder key. Version 1 names require the bounded migration described in docs/migration.md.

Security boundary

Tenant identity is routing and isolation data, never authorization evidence. This module cannot guarantee isolation when a consumer bypasses its enforcement seams. Transport trust, PostgreSQL patterns, administrative iteration, async integration, analyzers, migration guidance, and the complete threat model are documented with the corresponding adapters.

Propagation and trust

PropagationCodec is the transport-neutral contract used by queue, outbox, Kafka, CloudEvents, audit, correlation, idempotency, cache, rate-limit, search, scheduler, workflow, event-sourcing, and telemetry integrations. Extraction parses metadata but accepts it only when the caller supplies an explicit trust decision. Missing, repeated, conflicting, untrusted, malformed, oversized, and pre-existing values fail with distinct errors.

The http adapter requires a trust function for the authenticated immediate peer. Direct backend requests and forwarded headers are untrusted unless that function proves the boundary. It scans header names case-insensitively so map entries with different casing cannot conceal duplicates. The jsonrpc adapter parses a bounded raw metadata object before map decoding, so duplicate JSON keys cannot be silently collapsed.

Integration names every first-party boundary and provides Send, Receive, and opaque Key operations. Queue retries and redeliveries carry tenant scope explicitly; extraction never promotes system or unscoped work into a tenant.

Background and administrative work

Group owns every goroutine it starts. Submission is bounded and cancellable; Drain stops intake, waits for accepted work, and then releases the group-owned task context; Shutdown stops intake and cancels accepted work before waiting. Concurrent lifecycle calls join the same terminal state. Any Shutdown makes an active Drain or Close forceful by cancelling the shared task context; each caller waits for accepted work only until its own wait context ends. The deprecated Close(ctx) method delegates to Drain(ctx) for source-compatible migration. After terminal completion, repeated Drain, Close, and Shutdown calls return success even when their per-call wait context is already cancelled. Each task receives only its submitted immutable scope while preserving the submission context's values, deadline, and cancellation. Group-parent cancellation also cancels every task. If a graceful wait context ends first, accepted work continues and its final completion releases the group-owned context without requiring another lifecycle call. Submit rejects a conflicting scope synchronously before acquiring capacity or starting a goroutine. GroupOptions.HandleError may be called concurrently by independently completing tasks.

IterateTenants requires a system scope with an administrative actor, purpose, and optional reference, plus a mandatory audit callback. It reads bounded pages from a consumer-owned TenantPager, returns exact page-and-offset resume tokens, and derives every operation from the original unscoped base context. Tenant state therefore cannot survive into the next iteration. The capability records intent but applications must still authorize the operation.

PostgreSQL

The postgres adapter keeps query and session enforcement explicit:

  • Predicate returns a quoted tenant equality clause and its owned argument; callers still place the clause in every applicable query.
  • Manager.WithTenant leases one database/sql connection, clears any stale session value, begins a transaction, installs the tenant with transaction-local set_config, verifies it by reading it back, and resets the same leased connection before it can return to the pool.
  • Manager.WithSystem accepts only explicit system scope and installs an empty tenant setting. It does not bypass RLS or grant database privileges.
  • NewRLSPlan returns quoted ENABLE, FORCE, paired permissive and restrictive CREATE POLICY, and rollback statements. Applications apply these statements through their migration owner and should run application traffic through a non-owner role because table owners and privileged roles can otherwise bypass RLS.

The supplied RLS expression fails closed when the custom setting is absent or has been reset. Connection reset failures cause the physical connection to be discarded. Operations must use only the transaction passed to their callback; opening another connection or issuing tenant queries outside it bypasses this enforcement seam.

Testing

tenancytest provides test-only tenant and system scope constructors, context installation, and tenant assertions. The package includes hostile-input fuzz targets, randomized cross-tenant namespace models, concurrent isolation stress, and allocation-reporting benchmarks for context propagation, namespace encoding, and tenant assertions. These tests prove the owned enforcement seams; they cannot prove isolation in application paths that bypass those seams.

Detailed adoption and security guidance is in docs/: trust and service propagation, integrations, PostgreSQL/RLS, administration, migration, the exhaustive boundary inventory, static-analysis boundaries, hardening evidence, security caveats, and FAQ. The checked-in analysis.yml and make analyzers fixture provide executable negative proof for declared direct-provider, context-replacement, and telemetry cardinality bypasses. The current threat-to-test mapping and residual trust boundaries are recorded in docs/security-review.md. Shared construction, ownership, lifecycle, and composition expectations are in the versioned Golib ecosystem index and its Foundations family.

Documentation

Overview

Package tenancy provides explicit tenant identity, propagation, and isolation contracts. Tenant identifiers are routing data, never proof of authentication, membership, or authorization.

Example
package main

import (
	"context"
	"fmt"

	"github.com/faustbrian/go-tenancy"
)

func main() {
	id, err := tenancy.ParseTenantID("customer-42")
	if err != nil {
		panic(err)
	}
	scope, err := tenancy.NewTenantScope(id, tenancy.Metadata{})
	if err != nil {
		panic(err)
	}
	ctx, err := tenancy.WithScope(context.Background(), scope)
	if err != nil {
		panic(err)
	}
	resolved, err := tenancy.RequireTenant(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Println(resolved.Equal(id))
}
Output:
true

Index

Examples

Constants

View Source
const DefaultTenantField = "tenant_id"

DefaultTenantField is the transport-neutral metadata field name.

View Source
const (
	// MaxTenantIDBytes is the fixed wire and storage bound for tenant IDs.
	MaxTenantIDBytes = 128
)

Variables

View Source
var (
	// ErrInvalidGroup reports invalid ownership, limits, or receiver state.
	ErrInvalidGroup = errors.New("tenancy: invalid background group")
	// ErrGroupClosed reports submission after graceful close or shutdown begins.
	ErrGroupClosed = errors.New("tenancy: background group closed")
)
View Source
var (
	// ErrInvalidContext reports a nil context or invalid scope input.
	ErrInvalidContext = errors.New("tenancy: invalid context")
	// ErrScopeRequired reports an operation without explicit scope.
	ErrScopeRequired = errors.New("tenancy: scope required")
	// ErrTenantScopeRequired reports an operation without tenant-bound scope.
	ErrTenantScopeRequired = errors.New("tenancy: tenant scope required")
	// ErrSystemScopeRequired reports an operation without system-wide scope.
	ErrSystemScopeRequired = errors.New("tenancy: system scope required")
	// ErrConflictingScope reports an attempt to replace an existing scope.
	ErrConflictingScope = errors.New("tenancy: conflicting scope")
)
View Source
var (
	// ErrInvalidNamespaceKey reports a missing, weak, or oversized HMAC key.
	ErrInvalidNamespaceKey = errors.New("tenancy: invalid namespace key")
	// ErrInvalidNamespaceInput reports invalid scope, domain, or logical key.
	ErrInvalidNamespaceInput = errors.New("tenancy: invalid namespace input")
)
View Source
var (
	// ErrInvalidPropagation reports invalid codec or carrier configuration.
	ErrInvalidPropagation = errors.New("tenancy: invalid propagation")
	// ErrTenantMetadataMissing reports absent required tenant metadata.
	ErrTenantMetadataMissing = errors.New("tenancy: tenant metadata missing")
	// ErrTenantMetadataDuplicate reports repeated identical metadata.
	ErrTenantMetadataDuplicate = errors.New("tenancy: tenant metadata duplicate")
	// ErrTenantMetadataConflicting reports repeated distinct metadata.
	ErrTenantMetadataConflicting = errors.New("tenancy: tenant metadata conflicting")
	// ErrTenantMetadataOversized reports too many values at one boundary.
	ErrTenantMetadataOversized = errors.New("tenancy: tenant metadata oversized")
	// ErrTenantMetadataUntrusted reports metadata from an untrusted boundary.
	ErrTenantMetadataUntrusted = errors.New("tenancy: tenant metadata untrusted")
	// ErrTenantMetadataOverwrite reports injection into a populated field.
	ErrTenantMetadataOverwrite = errors.New("tenancy: tenant metadata overwrite")
)
View Source
var (
	// ErrInvalidMetadata reports malformed or unbounded optional metadata.
	ErrInvalidMetadata = errors.New("tenancy: invalid metadata")
	// ErrCapabilityRequired reports an attempt to construct system scope without
	// an explicit capability.
	ErrCapabilityRequired = errors.New("tenancy: system capability required")
	// ErrInvalidAdministrativeReason reports missing or malformed audit intent.
	ErrInvalidAdministrativeReason = errors.New("tenancy: invalid administrative reason")
)
View Source
var ErrInvalidIntegration = errors.New("tenancy: invalid integration")

ErrInvalidIntegration reports an unknown or incomplete integration boundary.

View Source
var ErrInvalidIteration = errors.New("tenancy: invalid administrative iteration")

ErrInvalidIteration reports an unsafe administrative iteration contract.

View Source
var ErrInvalidOperation = errors.New("tenancy: invalid operation")

ErrInvalidOperation reports a nil scoped callback.

View Source
var ErrInvalidTenantID = errors.New("tenancy: invalid tenant ID")

ErrInvalidTenantID reports an empty, oversized, or non-canonical tenant ID.

View Source
var ErrTenantMismatch = errors.New("tenancy: tenant mismatch")

ErrTenantMismatch reports cross-tenant use without disclosing either ID.

Functions

func AssertScope

func AssertScope(ctx context.Context, expected Scope) error

AssertScope fails closed unless ctx contains exactly expected scope.

func AssertTenant

func AssertTenant(ctx context.Context, expected TenantID) error

AssertTenant fails closed unless ctx is tenant-bound to expected.

func RunScoped

func RunScoped(ctx context.Context, scope Scope, operation func(context.Context) error) error

RunScoped invokes operation synchronously with explicit immutable scope. It derives from ctx, so cancellation, deadlines, and caller values are retained.

func WithScope

func WithScope(ctx context.Context, scope Scope) (context.Context, error)

WithScope returns a context carrying scope. An existing equal scope is retained; any attempt to replace scope fails deterministically. Parent cancellation, values, and deadlines are preserved by the returned context.

Types

type AdministrativeAudit

type AdministrativeAudit func(context.Context, AdministrativeReason, TenantID) error

AdministrativeAudit records system intent before each tenant operation. Implementations own durability and redaction. Returning an error fails closed.

type AdministrativeReason

type AdministrativeReason struct {
	// contains filtered or unexported fields
}

AdministrativeReason is auditable intent for exceptional operations. It is not an authorization decision.

func NewAdministrativeReason

func NewAdministrativeReason(actor, purpose, reference string) (AdministrativeReason, error)

NewAdministrativeReason validates explicit exceptional-operation intent.

func (AdministrativeReason) Actor

func (reason AdministrativeReason) Actor() string

Actor identifies the accountable administrative caller.

func (AdministrativeReason) GoString

func (reason AdministrativeReason) GoString() string

GoString returns a redacted representation for Go-syntax diagnostics.

func (AdministrativeReason) LogValue

func (reason AdministrativeReason) LogValue() slog.Value

LogValue redacts administrative intent when passed directly to log/slog.

func (AdministrativeReason) Purpose

func (reason AdministrativeReason) Purpose() string

Purpose describes why exceptional scope is required.

func (AdministrativeReason) Reference

func (reason AdministrativeReason) Reference() string

Reference returns an optional external audit or change reference.

func (AdministrativeReason) String

func (reason AdministrativeReason) String() string

String returns a redacted representation for diagnostics.

type Boundary

type Boundary string

Boundary names a first-party propagation and namespace integration.

const (
	// BoundaryQueue identifies queued deliveries and retries.
	BoundaryQueue Boundary = "queue"
	// BoundaryOutbox identifies transactional outbox records.
	BoundaryOutbox Boundary = "outbox"
	// BoundaryKafka identifies Kafka records.
	BoundaryKafka Boundary = "kafka"
	// BoundaryCloudEvents identifies CloudEvents attributes and extensions.
	BoundaryCloudEvents Boundary = "cloudevents"
	// BoundaryAudit identifies audit records.
	BoundaryAudit Boundary = "audit"
	// BoundaryCorrelation identifies correlation integration metadata.
	BoundaryCorrelation Boundary = "correlation"
	// BoundaryIdempotency identifies idempotency records.
	BoundaryIdempotency Boundary = "idempotency"
	// BoundaryCache identifies cache keys.
	BoundaryCache Boundary = "cache"
	// BoundaryRateLimit identifies rate-limit keys.
	BoundaryRateLimit Boundary = "rate-limit"
	// BoundarySearch identifies search indexes and documents.
	BoundarySearch Boundary = "search"
	// BoundaryScheduler identifies scheduled executions.
	BoundaryScheduler Boundary = "scheduler"
	// BoundaryWorkflow identifies workflow executions.
	BoundaryWorkflow Boundary = "workflow"
	// BoundaryEventSourcing identifies event streams and aggregates.
	BoundaryEventSourcing Boundary = "event-sourcing"
	// BoundaryTelemetry identifies telemetry attributes and links.
	BoundaryTelemetry Boundary = "telemetry"
)

type Carrier

type Carrier interface {
	Values(string) []string
	Set(string, string)
}

Carrier is an explicit metadata boundary. Values must return an immutable, transport-bounded view or copy and no more than nine values; Set must replace the field with exactly one value.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group owns every goroutine it starts. Submit is bounded and cancellable; callers must invoke Drain for graceful completion or Shutdown for cancellation. Concurrent lifecycle calls join one terminal state while each call observes its own wait context, and any Shutdown cancels the shared task context. No task scope is retained between submissions.

func NewGroup

func NewGroup(parent context.Context, options GroupOptions) (*Group, error)

NewGroup derives task lifetime from parent and validates a hard concurrency bound. The group owns the derived cancellation function.

func (*Group) Close deprecated

func (group *Group) Close(ctx context.Context) error

Close delegates to Drain and preserves its graceful completion behavior.

Deprecated: use Drain.

func (*Group) Drain added in v1.1.0

func (group *Group) Drain(ctx context.Context) error

Drain stops new submissions and waits for active tasks without cancelling them. Waiting observes ctx. Drain is safe for concurrent and repeated calls; once accepted work completes, it releases the group-owned task context. A concurrent Shutdown cancels shared tasks while Drain continues waiting for the same terminal state.

func (*Group) Shutdown

func (group *Group) Shutdown(ctx context.Context) error

Shutdown stops new submissions, cancels active tasks, and waits for their return. Waiting observes ctx, while task cancellation uses the group context. Concurrent and repeated lifecycle calls join the same terminal state; any Shutdown makes an active Drain or Close forceful by cancelling shared tasks.

func (*Group) Submit

func (group *Group) Submit(
	submitCtx context.Context,
	scope Scope,
	operation func(context.Context) error,
) error

Submit validates and installs scope synchronously before acquiring bounded capacity. The task preserves submitCtx values, deadline, and cancellation while also remaining owned and cancellable by the group lifetime.

type GroupOptions

type GroupOptions struct {
	MaxConcurrent int
	HandleError   func(Scope, error)
}

GroupOptions define bounded concurrency and task error ownership. HandleError may be invoked concurrently by independently completing tasks.

type Integration

type Integration struct {
	// contains filtered or unexported fields
}

Integration gives queue, outbox, Kafka, CloudEvents, audit, correlation, idempotency, cache, rate-limit, search, scheduler, workflow, event-sourcing, and telemetry adapters one small explicit contract.

func NewIntegration

func NewIntegration(boundary Boundary, options PropagationOptions) (*Integration, error)

NewIntegration validates a semantic boundary and propagation policy.

func (*Integration) Boundary

func (integration *Integration) Boundary() Boundary

Boundary returns the immutable semantic integration name.

func (*Integration) Key

func (integration *Integration) Key(
	encoder *NamespaceEncoder,
	scope Scope,
	logicalKey string,
) (string, error)

Key creates a boundary-separated opaque namespace for scope and logicalKey.

func (*Integration) Receive

func (integration *Integration) Receive(
	ctx context.Context,
	carrier Carrier,
	trusted bool,
) (context.Context, error)

Receive accepts tenant metadata only after trusted is explicitly supplied.

func (*Integration) Send

func (integration *Integration) Send(ctx context.Context, carrier Carrier) error

Send injects the tenant-bound scope required from ctx.

type IterationOptions

type IterationOptions struct {
	PageSize   int
	MaxTenants int
	Resume     ResumeToken
	Audit      AdministrativeAudit
}

IterationOptions bound work and make audit integration mandatory.

type IterationResult

type IterationResult struct {
	Processed int
	Resume    ResumeToken
	Complete  bool
}

IterationResult reports this run's progress and exact continuation point.

func IterateTenants

func IterateTenants(
	ctx context.Context,
	system Scope,
	source TenantPager,
	options IterationOptions,
	operation func(context.Context, TenantID) error,
) (IterationResult, error)

IterateTenants executes bounded sequential work from an unscoped base context. system must be an explicit system scope. Audit callbacks receive a system-scoped child; operations receive a fresh tenant-scoped child derived from the original base, so tenant state cannot survive between iterations.

type MapCarrier

type MapCarrier map[string][]string

MapCarrier is an owned in-memory carrier useful for message metadata and adapters. Values returns a copy so callers cannot mutate stored metadata.

func (MapCarrier) Set

func (carrier MapCarrier) Set(field, value string)

Set replaces field with exactly one value.

func (MapCarrier) Values

func (carrier MapCarrier) Values(field string) []string

Values returns an independently owned copy of a field's values.

type Metadata

type Metadata struct {
	// contains filtered or unexported fields
}

Metadata is immutable optional routing metadata. It is copied at every ownership boundary and must not carry authorization decisions or secrets.

func NewMetadata

func NewMetadata(values map[string]string) (Metadata, error)

NewMetadata validates and owns a copy of values.

func (Metadata) Get

func (metadata Metadata) Get(key string) (string, bool)

Get returns one metadata value.

func (Metadata) GoString

func (metadata Metadata) GoString() string

GoString returns a redacted representation for Go-syntax diagnostics.

func (Metadata) LogValue

func (metadata Metadata) LogValue() slog.Value

LogValue redacts metadata when passed directly to log/slog.

func (Metadata) String

func (metadata Metadata) String() string

String returns a redacted representation for diagnostics.

func (Metadata) Values

func (metadata Metadata) Values() map[string]string

Values returns an independently owned copy.

type NamespaceDomain

type NamespaceDomain string

NamespaceDomain separates independently owned isolation key spaces.

const (
	// NamespaceCache isolates cache entries.
	NamespaceCache NamespaceDomain = "cache"
	// NamespaceIdempotency isolates idempotency records.
	NamespaceIdempotency NamespaceDomain = "idempotency"
	// NamespaceRateLimit isolates rate-limit counters.
	NamespaceRateLimit NamespaceDomain = "rate-limit"
	// NamespaceSearch isolates search indexes and documents.
	NamespaceSearch NamespaceDomain = "search"
	// NamespaceQueue isolates queue routing and deduplication keys.
	NamespaceQueue NamespaceDomain = "queue"
	// NamespaceScheduler isolates scheduled work.
	NamespaceScheduler NamespaceDomain = "scheduler"
	// NamespaceEvent isolates event streams and event identities.
	NamespaceEvent NamespaceDomain = "event"
	// NamespaceWorkflow isolates workflow executions.
	NamespaceWorkflow NamespaceDomain = "workflow"
	// NamespaceTelemetry produces opaque telemetry attributes.
	NamespaceTelemetry NamespaceDomain = "telemetry"
)

type NamespaceEncoder

type NamespaceEncoder struct {
	// contains filtered or unexported fields
}

NamespaceEncoder creates opaque, collision-resistant, versioned keys. It owns a copy of its HMAC key and is safe for concurrent use.

Example
package main

import (
	"fmt"

	"github.com/faustbrian/go-tenancy"
)

func main() {
	encoder, err := tenancy.NewNamespaceEncoder([]byte("0123456789abcdef0123456789abcdef"))
	if err != nil {
		panic(err)
	}
	scope, _ := tenancy.NewTenantScope(tenancy.MustTenantID("customer-42"), tenancy.Metadata{})
	key, err := encoder.Encode(scope, tenancy.NamespaceCache, "orders/17")
	if err != nil {
		panic(err)
	}
	fmt.Println(len(key), key[:4])
}
Output:
68 tn2_

func NewNamespaceEncoder

func NewNamespaceEncoder(key []byte) (*NamespaceEncoder, error)

NewNamespaceEncoder validates and copies a secret HMAC key.

func (*NamespaceEncoder) Encode

func (encoder *NamespaceEncoder) Encode(scope Scope, domain NamespaceDomain, key string) (string, error)

Encode composes scope, domain, and key with length-delimited HMAC input. Raw tenant IDs and logical keys never appear in the returned namespace.

type PropagationCodec

type PropagationCodec struct {
	// contains filtered or unexported fields
}

PropagationCodec extracts and injects tenant-bound scope without deciding whether an inbound carrier is trusted.

func NewPropagationCodec

func NewPropagationCodec(options PropagationOptions) (*PropagationCodec, error)

NewPropagationCodec validates and copies codec configuration.

func (*PropagationCodec) Accept

func (codec *PropagationCodec) Accept(
	ctx context.Context,
	carrier Carrier,
	trusted bool,
) (context.Context, error)

Accept extracts trusted tenant metadata and installs it without replacing an existing context scope.

func (*PropagationCodec) Extract

func (codec *PropagationCodec) Extract(carrier Carrier, trusted bool) (Scope, error)

Extract parses exactly one tenant value after the immediate boundary has explicitly established trust. Presence alone never establishes trust.

func (*PropagationCodec) Inject

func (codec *PropagationCodec) Inject(carrier Carrier, scope Scope) error

Inject writes one tenant value and refuses overwrite or exceptional scope.

func (*PropagationCodec) InjectFromContext

func (codec *PropagationCodec) InjectFromContext(carrier Carrier, ctx context.Context) error

InjectFromContext requires and injects tenant-bound context scope.

type PropagationOptions

type PropagationOptions struct {
	Field string
}

PropagationOptions configure an immutable tenant field name.

type ResumeToken

type ResumeToken struct {
	Cursor string
	Offset int
}

ResumeToken identifies an exact page position. Offset is the next tenant in the page at Cursor, allowing retry after partial-page failure.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope is an immutable explicit operation scope. Its zero value is invalid.

func NewSystemScope

func NewSystemScope(capability SystemCapability, metadata Metadata) (Scope, error)

NewSystemScope requires an explicit, valid system capability.

func NewTenantScope

func NewTenantScope(id TenantID, metadata Metadata) (Scope, error)

NewTenantScope constructs work isolated to exactly id.

func NewUnscopedScope

func NewUnscopedScope(reason AdministrativeReason, metadata Metadata) (Scope, error)

NewUnscopedScope constructs deliberately non-tenant work with an audit reason.

func RequireScope

func RequireScope(ctx context.Context) (Scope, error)

RequireScope fails closed when ctx has no explicit valid scope.

func RequireSystem

func RequireSystem(ctx context.Context) (Scope, error)

RequireSystem returns scope only for explicitly capable system-wide work.

func RequireUnscoped

func RequireUnscoped(ctx context.Context) (Scope, error)

RequireUnscoped returns scope only for intentionally unscoped work.

func ScopeFromContext

func ScopeFromContext(ctx context.Context) (Scope, bool)

ScopeFromContext retrieves explicit scope without treating absence as valid.

func (Scope) AdministrativeReason

func (scope Scope) AdministrativeReason() AdministrativeReason

AdministrativeReason returns exceptional-operation intent. Tenant scope returns the zero reason.

func (Scope) Equal

func (scope Scope) Equal(other Scope) bool

Equal compares all owned scope data.

func (Scope) GoString

func (scope Scope) GoString() string

GoString returns a redacted representation for Go-syntax diagnostics.

func (Scope) Kind

func (scope Scope) Kind() ScopeKind

Kind returns the explicit scope kind.

func (Scope) LogValue

func (scope Scope) LogValue() slog.Value

LogValue redacts complete scope state when passed directly to log/slog.

func (Scope) Metadata

func (scope Scope) Metadata() Metadata

Metadata returns an independently owned metadata value.

func (Scope) String

func (scope Scope) String() string

String returns a redacted representation for diagnostics.

func (Scope) TenantID

func (scope Scope) TenantID() TenantID

TenantID returns the tenant ID for tenant scope and the invalid zero value for every other scope kind.

func (Scope) Valid

func (scope Scope) Valid() bool

Valid reports whether scope can be used at an enforcement boundary.

type ScopeKind

type ScopeKind uint8

ScopeKind distinguishes tenant-bound, system-wide, and deliberately unscoped work. The zero value is invalid.

const (
	// ScopeTenant identifies work isolated to exactly one tenant.
	ScopeTenant ScopeKind = iota + 1
	// ScopeSystem identifies explicitly capable cross-tenant work.
	ScopeSystem
	// ScopeUnscoped identifies work that deliberately has no tenant semantics.
	ScopeUnscoped
)

type SystemCapability

type SystemCapability struct {
	// contains filtered or unexported fields
}

SystemCapability makes system-wide intent visible in construction APIs. It does not grant permission; applications must authorize its creation.

func NewSystemCapability

func NewSystemCapability(reason AdministrativeReason) SystemCapability

NewSystemCapability records the audited intent used by NewSystemScope.

func (SystemCapability) GoString

func (capability SystemCapability) GoString() string

GoString returns a redacted representation for Go-syntax diagnostics.

func (SystemCapability) LogValue

func (capability SystemCapability) LogValue() slog.Value

LogValue redacts the capability when passed directly to log/slog.

func (SystemCapability) String

func (capability SystemCapability) String() string

String returns a redacted representation for diagnostics.

type TenantAssertFunc

type TenantAssertFunc func(context.Context, TenantID) error

TenantAssertFunc adapts a function to TenantAsserter.

func (TenantAssertFunc) AssertTenant

func (assert TenantAssertFunc) AssertTenant(ctx context.Context, expected TenantID) error

AssertTenant implements TenantAsserter.

type TenantAsserter

type TenantAsserter interface {
	AssertTenant(context.Context, TenantID) error
}

TenantAsserter is the small consumer-facing application and persistence boundary for operations that require one expected tenant.

type TenantID

type TenantID struct {
	// contains filtered or unexported fields
}

TenantID is a validated opaque tenant identifier. Its zero value is invalid. IDs use the case-sensitive ASCII alphabet [A-Za-z0-9._:/-], must begin with an alphanumeric byte, and are preserved exactly: no case folding, trimming, or Unicode normalization occurs.

String deliberately returns a redacted value. Use Value, MarshalText, or MarshalJSON only at an explicit trusted propagation or persistence boundary.

func MustTenantID

func MustTenantID(value string) TenantID

MustTenantID is ParseTenantID for static configuration and tests.

func ParseTenantID

func ParseTenantID(value string) (TenantID, error)

ParseTenantID validates value without changing it.

func RequireTenant

func RequireTenant(ctx context.Context) (TenantID, error)

RequireTenant returns the tenant ID only for tenant-bound scope.

func (TenantID) Equal

func (id TenantID) Equal(other TenantID) bool

Equal compares canonical opaque identifiers exactly.

func (TenantID) GoString

func (id TenantID) GoString() string

GoString returns a redacted representation for Go-syntax diagnostics.

func (TenantID) LogValue

func (id TenantID) LogValue() slog.Value

LogValue redacts the identifier when passed directly to log/slog.

func (TenantID) MarshalJSON

func (id TenantID) MarshalJSON() ([]byte, error)

MarshalJSON serializes the canonical raw identifier as a JSON string.

func (TenantID) MarshalText

func (id TenantID) MarshalText() ([]byte, error)

MarshalText serializes the canonical raw identifier for a trusted boundary.

func (TenantID) Redacted

func (id TenantID) Redacted() string

Redacted returns a non-identifying representation safe for diagnostics.

func (TenantID) String

func (id TenantID) String() string

String returns a redacted representation to prevent accidental disclosure.

func (*TenantID) UnmarshalJSON

func (id *TenantID) UnmarshalJSON(data []byte) error

UnmarshalJSON validates a JSON string and clears the receiver on failure.

func (*TenantID) UnmarshalText

func (id *TenantID) UnmarshalText(text []byte) error

UnmarshalText validates input and clears the receiver on every failure.

func (TenantID) Valid

func (id TenantID) Valid() bool

Valid reports whether id was created from a valid canonical value.

func (TenantID) Value

func (id TenantID) Value() string

Value returns the canonical raw identifier for explicit trusted boundaries.

type TenantPage

type TenantPage struct {
	Tenants    []TenantID
	NextCursor string
}

TenantPage is one bounded page from a resumable tenant source. NextCursor is empty only when the source is complete.

type TenantPager

type TenantPager interface {
	ListTenants(context.Context, string, int) (TenantPage, error)
}

TenantPager is the consumer-defined source for bounded administrative work.

Directories

Path Synopsis
Package tenancyhttp provides explicit tenant propagation for net/http.
Package tenancyhttp provides explicit tenant propagation for net/http.
Package tenancyjsonrpc propagates tenant scope through a bounded JSON-RPC metadata object.
Package tenancyjsonrpc propagates tenant scope through a bounded JSON-RPC metadata object.
Package tenancypostgres provides explicit PostgreSQL predicates, transaction-local tenant settings, and Row-Level Security plans.
Package tenancypostgres provides explicit PostgreSQL predicates, transaction-local tenant settings, and Row-Level Security plans.
Package tenancytest provides concise constructors and assertions for tests that exercise explicit tenancy contracts.
Package tenancytest provides concise constructors and assertions for tests that exercise explicit tenancy contracts.

Jump to

Keyboard shortcuts

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