Arc.Tenancy behaviour (cratis_arc v0.1.0)

Copy Markdown View Source

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"]
ResolverOptionsDefault
:header:header"x-cratis-tenant-id" (or the :tenant_header option)
:query:query_parameter"tenantId"
:claim:claim_type"tenant_id"
:subdomain:base_domain (required), :headerfalls 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

resolve(t, t, keyword)

@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

chronicle_options(chronicle, tenant)

@spec chronicle_options(keyword(), String.t() | nil) :: keyword()

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.

default?(tenant)

@spec default?(String.t() | nil) :: boolean()

Whether a tenant is the default: not set, empty, or "Default".

options(config)

@spec options(keyword()) :: keyword()

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.

resolve(conn, principal, config)

@spec resolve(Plug.Conn.t(), Arc.Principal.t(), keyword()) :: String.t() | nil

Resolves the tenant of a request with the configured resolver.

validate!(config)

@spec validate!(keyword()) :: :ok

Validates the tenancy configuration, raising ArgumentError when it is unusable.