tenant

package
v0.53.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package tenant carries the resolved tenant (provider) scope through a request.

A tenant is a provider (see model.Provider). In multi-tenant mode a resolver middleware puts a *Scope in the request context from the Host (browser) or the api_token's user (API/agents); every tenant-scoped read then filters by it. In single-tenant mode no Scope is set and the absence means "unscoped — see everything", so existing behaviour is unchanged.

This package is deliberately tiny and dependency-free (only stdlib) so both the HTTP layer and the scoped store can share it without import cycles.

Index

Constants

View Source
const RealmMaxLen = 63

RealmMaxLen bounds a realm: a DNS label's length, because a realm is also the first label of an address domain (`alex@acme.local`).

Variables

View Source
var (
	ErrRealmEmpty    = errors.New("tenant: the realm is empty")
	ErrRealmInvalid  = errors.New("tenant: a realm is lower-case letters, digits and dashes, at most 63 characters, and starts and ends with a letter or a digit")
	ErrRealmReserved = errors.New("tenant: this realm is reserved")
)

Why a realm is refused. The API answers them as `realm_<kind>`.

View Source
var DenyAll = &Scope{ProviderID: 0}

DenyAll is a non-nil scope that grants nothing. The resolver attaches it to an authenticated request whose tenant cannot be resolved, so the request fails closed — it sees no storages and no directory (ProviderID 0 never matches a real provider) — instead of falling through to an unscoped "see everything".

Functions

func CheckRealm added in v0.50.0

func CheckRealm(s string) error

CheckRealm reports why a realm cannot be given to a new tenant, nil when it can. The realm is normalised first.

⚠ The character set is narrower than a slug's has ever been checked to be, on purpose: `/` separates the realm in `realm/name`, `@` and `.` would make the derived address ambiguous (`alex@a.b.local`), and `\` is the Windows domain separator a login name may already carry.

func IsReservedRealm added in v0.50.0

func IsReservedRealm(s string) bool

IsReservedRealm reports whether a realm is one no tenant may be given.

func NormalizeRealm added in v0.50.0

func NormalizeRealm(s string) string

NormalizeRealm folds a realm the way it is stored and compared: trimmed and lower-cased. It does not validate.

func RealmCandidates added in v0.50.0

func RealmCandidates(base string, n int) []string

RealmCandidates lists the realms to try, in order, for a suggestion that may already be taken or reserved: the suggestion itself, then `-2` … `-n` on it (shortened so the result still fits RealmMaxLen). Every candidate passes CheckRealm except, possibly, the first.

func SplitLogin added in v0.50.0

func SplitLogin(s string) (realm, name string, named bool)

SplitLogin reads `realm/name` — the way a protocol user name names a realm when the protocol carries no address. realm is normalised; named says a realm part was written at all.

acme/alex          realm "acme", name "alex"
acme/alex@x.com    realm "acme", name "alex@x.com" (split at the FIRST `/`)
/alex              realm "" named: the platform's own tenant, said outright
alex               no realm part

A `/` that comes after an `@` or a `\` is part of the name, not a realm separator.

func SuggestRealm added in v0.50.0

func SuggestRealm(s string) string

SuggestRealm turns a slug (or a tenant's name) into the realm the tenant screen offers while a tenant is being created: lower case, letters with marks written without them (`Müşteri` → `musteri`), every run of anything that is not a letter or a digit one dash, no dash at either end, at most RealmMaxLen characters. "" when nothing realm-shaped is left.

⚠ A SUGGESTION, shown before the tenant exists and edited by whoever creates it: the realm is immutable once given, so nothing here is applied without a person seeing it. The API still validates what it is sent (CheckRealm), and a suggestion may be reserved or taken; the caller asks for that (RealmCandidates).

func WithScope

func WithScope(ctx context.Context, s *Scope) context.Context

WithScope returns a context carrying s.

Types

type Scope

type Scope struct {
	ProviderID    int64
	Slug          string
	IsSupertenant bool
	// StorageIDs are the provider's linked storages. Empty for a supertenant,
	// which is confine-exempt and reaches every storage (see CanAccessStorage).
	StorageIDs []int64
}

Scope is the resolved tenant for a request: which provider it belongs to, whether that provider is the platform supertenant (confine-exempt and cross-tenant), and the storage ids it may reach.

func FromContext

func FromContext(ctx context.Context) (*Scope, bool)

FromContext returns the request's tenant scope, or (nil, false) when none is set — single-tenant mode, or a pre-auth/public path. Callers must treat the absence as "unscoped": in multi-tenant mode a tenant-scoped operation that finds no scope must fail closed rather than fall back to unscoped.

func (*Scope) CanAccessStorage

func (s *Scope) CanAccessStorage(id int64) bool

CanAccessStorage reports whether this scope may reach storage id. A nil scope (single-tenant mode / unscoped) and a supertenant can reach any storage; a regular tenant only its linked storages. This is the file-layer gate — the isolation circuit that, unlike directory scoping, must never leak.

Jump to

Keyboard shortcuts

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