Ankusa.Principal (ankusa v0.7.0)

Copy Markdown View Source

Who is calling a control plane, and what they may do: the result of Ankusa.Admin.Auth.authenticate/2, assigned to conn.assigns.principal for every handler of the admin API, the route-management API and the claim-check gateway.

  • id — a stable name for audit and logs (a token entry's id, a JWT's sub, a certificate's mapped subject).
  • scopes — operation scopes, e.g. "dlq:read".
  • tenants — :all (the tenant:* scope) or the list of tenant ids the principal may act on (one tenant:<id> scope each).

Scopes

ScopeGrants
metrics:readGET /metrics, GET /v1/wal
config:readGET /v1/config, GET /asyncapi.json
config:writePUT /v1/log-level and POST /v1/config/reload (with tenant:*: both are the node's, for every tenant)
sources:read, sources:writea tenant's sources
dlq:readGET /v1/dlq; dlq:write: starting a dlq replay
replays:read, replays:writereplay jobs (dlq:write / quarantine:write as well for those kinds)
quarantine:read, quarantine:writethe quarantine pen
rate_limits:read, rate_limits:writeper-tenant rate limits
tenants:writeDELETE /v1/tenants/:tenant (a tenant erase) and GET /v1/tenants/:tenant/erase; with tenant:*
routes:read, routes:writeroute definitions, IP rules, the dry run
claims:readclaim-check gateway reads
audit:readreserved for the audit log; no route yet
tenant:<id>, tenant:*the tenants the other scopes apply to

A resource that spans tenants is filtered by tenant, not refused. The DLQ, the quarantine pen, replay jobs and the rate-limit listing take an optional tenant filter: a principal limited to some tenants gets rows for those tenants only (forced server-side) and 403 for a tenant it does not hold; tenant:* is required to read or change them unfiltered. Route definitions and IP rules belong to no tenant and still need tenant:*.

Summary

Functions

Does the principal hold tenant:*?

A principal from id, scope strings and an extra list of tenant ids (the ankusa_tenants claim: "*" means every tenant). Entries that are not scopes of this system are dropped: an identity provider's token carries scopes of its own (openid, email) that mean nothing here.

The principal of an unauthenticated listener (mode: :none, loopback only): every scope, every tenant.

Split scope strings into operation scopes and tenants. Returns {scopes, tenants, unknown}: unknown is every entry that is neither a known scope nor a tenant: scope, in order.

Does the principal hold scope?

Every operation scope (the tenant: scopes are separate).

May the principal act on tenant?

Types

t()

@type t() :: %Ankusa.Principal{
  id: String.t(),
  scopes: [String.t()],
  tenants: :all | [String.t()]
}

Functions

all_tenants?(principal)

@spec all_tenants?(t()) :: boolean()

Does the principal hold tenant:*?

new(id, scope_entries, extra_tenants \\ [])

@spec new(String.t(), [String.t()], [String.t()]) :: t()

A principal from id, scope strings and an extra list of tenant ids (the ankusa_tenants claim: "*" means every tenant). Entries that are not scopes of this system are dropped: an identity provider's token carries scopes of its own (openid, email) that mean nothing here.

operator(id \\ "anonymous")

@spec operator(String.t()) :: t()

The principal of an unauthenticated listener (mode: :none, loopback only): every scope, every tenant.

parse_scopes(entries)

@spec parse_scopes([String.t()]) :: {[String.t()], :all | [String.t()], [String.t()]}

Split scope strings into operation scopes and tenants. Returns {scopes, tenants, unknown}: unknown is every entry that is neither a known scope nor a tenant: scope, in order.

scope?(principal, scope)

@spec scope?(t(), String.t()) :: boolean()

Does the principal hold scope?

scopes()

@spec scopes() :: [String.t()]

Every operation scope (the tenant: scopes are separate).

tenant?(principal, tenant)

@spec tenant?(t(), term()) :: boolean()

May the principal act on tenant?