Resolves which tenant a request is for.
Tenancy has three separate decisions. Selection (this module) resolves the tenant id.
Membership authorization checks that the caller may use it, and is yours to enforce.
Storage isolation decides where data lives: ArcMongo names the database after the tenant,
and Arc maps the tenant to the Chronicle event store namespace. None of these replaces the
others, and a resolver only selects: a caller-controlled header, query string or host is not
membership evidence.
The resolved tenant is ctx.tenant: a string, or nil when nothing selected one. nil and
"Default" are both the default tenant (see default?/1) and select the default database
and the default Chronicle namespace; they are not rejected.
Configuration
The :tenancy option of the Arc application selects one resolver:
config :my_app, MyApp.Arc,
tenancy: [resolver: :header, header: "x-cratis-tenant-id"]| Resolver | Options | Default |
|---|---|---|
:header | :header | "x-cratis-tenant-id" (or the :tenant_header option) |
:query | :query_parameter | "tenantId" |
:claim | :claim_type | "tenant_id" |
:subdomain | :base_domain (required), :header | falls back to the header |
:fixed | :tenant_id | "development" |
:development | :tenant_id | "development", same behavior as :fixed |
resolver may also be a module implementing this behaviour, for example one that consults a
session. The :claim resolver reads the principal, so authentication must have run; the
:subdomain resolver's fallback header is client-supplied, so strip it at your ingress.
A subdomain resolves a tenant only when it is exactly one DNS label in front of :base_domain:
acme.myapp.com is acme, while myapp.com, acme.staging.myapp.com, IP literals and
unrelated hosts such as evil-myapp.com fall back to the header. The base domain is required
and must be a registrable domain you own (at least two letter-digit-hyphen labels, never an
address literal); the application refuses to start otherwise, because without it every request
would silently take its tenant from the client-supplied header.
Hosts are matched after Unicode compatibility mapping (NFKC), the ideographic and fullwidth
full stops count as label separators and zero-width characters are ignored. Internationalized
labels are not converted to punycode: write them as xn-- labels in configuration and traffic.
Summary
Callbacks
Selects the tenant id for a request.
Functions
The Chronicle options that scope a call to the tenant: namespace: tenant for a named tenant,
nothing for the default one. An explicit :namespace in chronicle wins.
Whether a tenant is the default: not set, empty, or "Default".
The normalized tenancy options of an application configuration.
Resolves the tenant of a request with the configured resolver.
Validates the tenancy configuration, raising ArgumentError when it is unusable.
Callbacks
@callback resolve(Plug.Conn.t(), Arc.Principal.t(), keyword()) :: String.t() | nil
Selects the tenant id for a request.
principal is the authenticated caller (or anonymous) and conn the request.
Functions
The Chronicle options that scope a call to the tenant: namespace: tenant for a named tenant,
nothing for the default one. An explicit :namespace in chronicle wins.
Whether a tenant is the default: not set, empty, or "Default".
The normalized tenancy options of an application configuration.
Raises ArgumentError when the configuration cannot select a tenant safely, for example a
subdomain resolver without a usable base domain.
@spec resolve(Plug.Conn.t(), Arc.Principal.t(), keyword()) :: String.t() | nil
Resolves the tenant of a request with the configured resolver.
@spec validate!(keyword()) :: :ok
Validates the tenancy configuration, raising ArgumentError when it is unusable.