Opt-in URL-prefix tenant resolver (D-12 sugar).
Reads context.path_params["tenant_id"] and returns {:ok, tid} when
present and non-empty, or {:error, :missing_path_param} otherwise.
Adopters with a router shape that embeds the tenant identifier in the
URL path (e.g. /tenants/:tenant_id/webhooks/postmark) compose this
resolver to avoid hand-writing the same one-line extraction.
Why this is a separate module
Mailglass.Tenancy.SingleTenant is the zero-config default (always
{:ok, "default"}). ResolveFromPath is the minimum viable
multi-tenant resolver; adopters wire it by setting:
config :mailglass, tenancy: Mailglass.Tenancy.ResolveFromPathImportant: this module does NOT implement a real scope/2
ResolveFromPath is SUGAR for webhook tenant extraction only. Its
scope/2 raises — the module fails CLOSED when mistakenly used as a
complete Tenancy implementation. Adopters using it for the full
Tenancy contract MUST configure their own module that delegates
resolve_webhook_tenant/1 to this module while implementing
scope/2 for their data layer:
defmodule MyApp.Tenancy do
@behaviour Mailglass.Tenancy
@impl Mailglass.Tenancy
def scope(query, context), do: # ... WHERE tenant_id = ? / repo prefix
@impl Mailglass.Tenancy
defdelegate resolve_webhook_tenant(context),
to: Mailglass.Tenancy.ResolveFromPath
endThreat mitigation (T-04-08)
ResolveFromPath EXTRACTS path_params["tenant_id"] only — it does
NOT validate that the tenant exists in any persistence layer.
Cross-tenant data access is prevented downstream by the configured
Tenancy module's scope/2 (which adopters implement for their own
data layer). Forged tenant_id values in the URL path can ONLY
access whatever data the adopter Repo's Tenancy scope exposes for
that ID — there is no implicit trust in this module. Mitigation
verified by the scope/2 raise behaviour + the documentation
contract that composition with a real Tenancy is mandatory.