multitenant

package
v0.73.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidTenantID = errors.New("multitenant: tenant id does not match the tenant-ID grammar")

ErrInvalidTenantID reports a tenant identifier that does not match DefaultTenantIDPattern.

View Source
var ErrNoTenant = errors.New("multitenant: no tenant in context")

ErrNoTenant reports a context that carries no tenant. It is the error form of GetTenant's ok result, so every lane answers "no tenant" the same way.

View Source
var ErrTenantResolutionFailed = errors.New("tenant resolution failed")

ErrTenantResolutionFailed is returned when a resolver cannot determine the tenant identifier.

View Source
var ErrUnknownTenant error = &unknownTenantError{}

ErrUnknownTenant is returned for a well-formed tenant ID that names no tenant the deployment serves. The HTTP layer answers it as a client fault (400). It wraps ErrTenantResolutionFailed, so a tenant-owned family's Tenant step that returns it is a client fault.

View Source
var ErrUntrustedForwardedHost = errors.New("multitenant: X-Forwarded-Host from an untrusted peer")

ErrUntrustedForwardedHost is returned by a SubdomainResolver with TrustProxies set when a peer it does not trust sends X-Forwarded-Host. CompositeResolver stops on it instead of trying the next sub-resolver.

Functions

func DefaultTenantIDPattern added in v0.61.0

func DefaultTenantIDPattern() *regexp.Regexp

DefaultTenantIDPattern returns the framework's tenant identifier grammar.

func FanOutRetentionCleanup added in v0.42.0

func FanOutRetentionCleanup(
	ctx context.Context,
	log logger.Logger,
	tenants []string,
	getDB func(context.Context) (dbtypes.Interface, error),
	retention time.Duration,
	name string,
	del RetentionDelete,
) error

FanOutRetentionCleanup runs a retention delete once per tenant, resolving each tenant's database via getDB with that tenant injected into the context (SetTenant). Per-tenant errors AND panics are collected with errors.Join so one unhealthy tenant cannot block the others. Single-tenant callers pass tenants=[""] (SetTenant("") is a no-op). name labels the job in log and error messages (e.g. "outbox", "inbox").

This is shared by the outbox and inbox cleanup jobs, whose Execute bodies are otherwise identical — only the store delete and the label differ.

func ForEachTenant added in v0.72.0

func ForEachTenant(ctx context.Context, tenants []string, fn func(ctx context.Context, tenantID string) error) error

ForEachTenant runs fn once per tenant, in the given order, each inside that tenant's own lease scope (ADR-032): the tenant's context carries the tenant (SetTenant; "" runs fn once with no tenant) and a fresh scope that shadows any enclosing request, message or job scope, drained when fn returns or panics. A tenant-sweeping job then holds about one tenant's leased handles at a time instead of every tenant's until the job returns.

ctx is checked before each tenant: once it is done no further tenant starts and ctx.Err() joins the result. Errors from fn are joined unwrapped (fn receives the tenant ID and owns its text) and the sweep continues. A panic in fn is not recovered; it propagates after that tenant's scope has drained. A nil or empty list returns nil.

Every borrow inside fn must use fn's ctx or a context derived from it: a borrow on the outer context lands in the enclosing scope and is not bounded. A handle, database.Session or transaction obtained inside fn must not be used after fn returns. Releasing a lease does not close a cached handle, so the bound matters only when a sweep covers more tenants than the manager's max size. The caller supplies the list: Config.PerTenantJobKeys() for static tenants, or its own list for dynamic sources.

func GetTenant

func GetTenant(ctx context.Context) (string, bool)

GetTenant extracts the tenant identifier from the context.

func MayReadPath added in v0.73.0

func MayReadPath(resolver TenantResolver) bool

MayReadPath reports whether Resolve can answer fromPath true for resolver: it is false only when every member able to produce an id is a *HeaderResolver or a *SubdomainResolver. It walks the resolver tree, so call it once at setup and keep the answer. A cycle (a *CompositeResolver or *ValidatingResolver reachable from itself) fails closed: the walk answers true rather than recurse without end.

func Resolve added in v0.73.0

func Resolve(ctx context.Context, resolver TenantResolver, req *http.Request) (tenantID string, fromPath bool, err error)

Resolve resolves the tenant as resolver.ResolveTenant does, with the same error and, when err is nil, the same id; on error the id is always empty and fromPath false. It reports whether the id may have come from the request path: fromPath is true when a *PathResolver produced it, or a resolver whose provenance is unknown. Provenance is judged on the concrete framework types alone, so any other type, including one embedding or wrapping a framework resolver, counts as unknown.

func SetTenant

func SetTenant(ctx context.Context, tenantID string) context.Context

SetTenant stores the tenant identifier in the provided context.

func TenantID added in v0.61.0

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

TenantID is GetTenant in error form: the tenant identifier, or ErrNoTenant.

func ValidateTenantID added in v0.61.0

func ValidateTenantID(id string) error

ValidateTenantID checks an identifier against DefaultTenantIDPattern.

SECURITY: the identifier is caller-written (an HTTP header, a message stamp), so the error carries its byte length only — never the value itself.

Types

type CompositeResolver

type CompositeResolver struct {
	Resolvers   []TenantResolver
	TenantRegex *regexp.Regexp // Optional validation pattern
}

CompositeResolver tries multiple resolvers until one succeeds. ErrUntrustedForwardedHost is terminal: it is returned at once and no later resolver is tried.

func (*CompositeResolver) ResolveTenant

func (r *CompositeResolver) ResolveTenant(ctx context.Context, req *http.Request) (string, error)

ResolveTenant implements TenantResolver.

type HeaderResolver

type HeaderResolver struct {
	HeaderName string
}

HeaderResolver extracts the tenant identifier from a configured request header.

func (*HeaderResolver) ResolveTenant

func (r *HeaderResolver) ResolveTenant(ctx context.Context, req *http.Request) (string, error)

ResolveTenant implements TenantResolver.

type PathResolver added in v0.34.0

type PathResolver struct {
	Segment int
	Prefix  string
}

PathResolver extracts the tenant identifier from a path segment.

Segment is 1-indexed (segment 1 is the first non-empty path part after the leading slash). When Prefix is non-empty, the resolver only attempts extraction for requests whose path equals Prefix exactly or starts with "Prefix/" — non-matching paths return ErrTenantResolutionFailed so the composite chain (if any) can fall through to other resolvers.

The resolver reads the path the framework's echo router matches: URL.RawPath when net/url keeps one, URL.Path otherwise. The prefix is compared and the segment returned as sent, still percent-escaped and untrimmed, exactly as the router binds a path parameter at that position (/a%2Fb/acme/x, segment 2, is "acme"; /%61cme/x, segment 1, is "%61cme", which the tenant-ID grammar refuses). A router that unescapes path values, such as net/http's ServeMux, can bind a different value for the same segment.

func (*PathResolver) ResolveTenant added in v0.34.0

func (r *PathResolver) ResolveTenant(ctx context.Context, req *http.Request) (string, error)

ResolveTenant implements TenantResolver.

type RetentionDelete added in v0.42.0

type RetentionDelete func(ctx context.Context, db dbtypes.Interface, cutoff time.Time) (int64, error)

RetentionDelete removes rows older than cutoff from the resolved tenant database. It is the per-vendor delete a retention-cleanup job runs (e.g. the outbox's DeletePublished or the inbox's DeleteProcessed) and matches those store methods' signatures directly.

type SubdomainResolver

type SubdomainResolver struct {
	RootDomain   string
	TrustProxies bool
	// TrustedProxies adds peer ranges trusted to send X-Forwarded-Host, on top of
	// loopback, link-local, private and unix-socket peers.
	TrustedProxies []*net.IPNet
}

SubdomainResolver extracts the tenant identifier from the request host.

func (*SubdomainResolver) ResolveTenant

func (r *SubdomainResolver) ResolveTenant(ctx context.Context, req *http.Request) (string, error)

ResolveTenant implements TenantResolver.

type TenantChecker added in v0.73.0

type TenantChecker interface {
	HasTenant(tenantID string) bool
}

TenantChecker reports whether a tenant id names a tenant the deployment holds; *config.TenantStore implements it. The check is in memory, a stated Context-First exception: no context, no I/O, no blocking. A source backed by a remote catalog refreshes its own set out of band and answers from it.

The contract every implementation must keep:

  • tenantID may be an unadmitted request value: a caller of HasTenant may pass it before admitting it. An implementation must never log it, echo it, put it in an error, send it outbound, pass it to a deps.* call, or use it as a cache, store or metric key. It may only compare it against the set it holds.
  • HasTenant answers false for "" and for any key that is not a tenant id, including a config.NamedDatabasePrefix key.
  • HasTenant is safe for concurrent use.
  • The complete tenant set is held before app.New: no first call may answer false for a tenant only because a load is still in flight.
  • On a failed refresh the implementation keeps the last good set; it never shrinks to empty because a refresh failed.
  • A source that refreshes logs the size of each refresh's result, a count and never the ids, so operators can tell when a pod's set is stale.
  • HasTenant never panics.
  • The answer is existence, not authorization: true says the deployment holds the tenant, not that the caller may act for it.

type TenantResolver

type TenantResolver interface {
	ResolveTenant(ctx context.Context, req *http.Request) (string, error)
}

TenantResolver resolves the tenant identifier from an incoming request.

type ValidatingResolver

type ValidatingResolver struct {
	Resolver    TenantResolver
	TenantRegex *regexp.Regexp
}

ValidatingResolver wraps any resolver with tenant ID validation.

func (*ValidatingResolver) ResolveTenant

func (r *ValidatingResolver) ResolveTenant(ctx context.Context, req *http.Request) (string, error)

ResolveTenant implements TenantResolver with validation.

Jump to

Keyboard shortcuts

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