# `Ankusa.Admin.Auth`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/admin/auth.ex#L1)

Authentication and authorization for the three control planes: the admin API
(`Ankusa.Admin.Router`), the route-management API (`Ankusa.Routes.Router`)
and the claim-check gateway (`Ankusa.ClaimCheck.Router`).

Each router mounts this module as a plug ahead of `:match`. The plug
authenticates the request, assigns the `Ankusa.Principal` to
`conn.assigns.principal` and halts with `401` when there is none; each route
then calls `authorize/3`, `authorize_tenant/4` or `authorize_global/3` for
the scope it needs, which answers `403` otherwise. `GET /health` and
`GET /ready` are exempt: probes carry no credentials.

## Modes

Each listener has its own `auth` block (`admin.auth`, `routes.admin.auth`,
`claim_check.auth`):

    %{
      mode: :none | :token | :jwt | :mtls,
      allow_insecure: false,
      tokens: [%{id: "ci", sha256: "<64 hex>", scopes: ["dlq:read", "tenant:*"]}],
      jwt: %{issuer: ..., audience: ..., jwks_url: ..., keys: [...], ...},
      mtls: %{subject_map: %{"cn:ops-laptop" => ["dlq:read", "tenant:*"]}}
    }

  * `:none` (the default) authenticates nobody and hands every request the
    all-powerful `Ankusa.Principal.operator/1`. It is only accepted when the
    listener binds loopback: `Ankusa.Config.new/1` refuses `mode: :none` on
    any other address unless `allow_insecure: true` says the deployment is
    protected by something else (a mesh, a proxy, a network policy). Even
    `tls.verify: :verify_peer` does not count: it keeps strangers off the
    port but does not say what a certificate may do; use `mode: :mtls`.
  * `:token` — `Ankusa.Admin.Auth.Token`: static bearer tokens, stored as
    SHA-256 hashes, each with its scopes. For development, CI and
    machine-to-machine scripts.
  * `:jwt` — `Ankusa.Admin.Auth.JWT`: OIDC/OAuth2 access tokens signed by
    your identity provider, verified against its JWKS.
  * `:mtls` — `Ankusa.Admin.Auth.MTLS`: the verified client certificate of a
    `tls.verify: :verify_peer` listener, mapped to a principal by subject.
    Requires that listener's `tls` block.

Further modes are modules implementing `c:authenticate/2`.

## Responses

  * `401` `{"error":"unauthenticated"}` with `www-authenticate: Bearer
    realm="ankusa"` when no credential, or a bad one, was presented. The
    response never says which.
  * `403` `{"error":"forbidden","scope":"dlq:read"}` when the principal lacks
    the named scope. A tenant scope names `tenant:<id>`.

# `opts`

```elixir
@type opts() :: %{instance: atom(), section: section(), config: map()}
```

What an implementation of `c:authenticate/2` is given.

# `section`

```elixir
@type section() :: :admin | :routes_admin | :claim_check
```

Which control plane the plug guards.

# `authenticate`

```elixir
@callback authenticate(Plug.Conn.t(), opts()) ::
  {:ok, Ankusa.Principal.t()} | {:error, :unauthenticated}
```

Identify the caller of `conn`: a principal, or `{:error, :unauthenticated}`
for a missing, malformed, expired or unknown credential. `opts.config` is the
listener's normalised `auth` block.

# `auth_config`

```elixir
@spec auth_config(Ankusa.Config.t(), section()) :: map()
```

The `auth` block of the control plane `section`.

# `authorize`

```elixir
@spec authorize(Plug.Conn.t(), String.t() | [String.t()], (Plug.Conn.t() -&gt;
                                                       Plug.Conn.t())) ::
  Plug.Conn.t()
```

Run `fun` when the principal holds `scope` (or every scope in a list), else
answer `403`.

# `authorize_global`

```elixir
@spec authorize_global(Plug.Conn.t(), String.t() | [String.t()], (Plug.Conn.t() -&gt;
                                                              Plug.Conn.t())) ::
  Plug.Conn.t()
```

`authorize/3` for a resource that spans tenants (the DLQ, the quarantine pen,
replay jobs, route definitions): the principal must also hold `tenant:*`.

# `authorize_tenant`

```elixir
@spec authorize_tenant(
  Plug.Conn.t(),
  String.t() | [String.t()],
  term(),
  (Plug.Conn.t() -&gt;
     Plug.Conn.t())
) ::
  Plug.Conn.t()
```

`authorize/3` for a resource that belongs to one tenant: the principal must
also hold `tenant:<tenant>` or `tenant:*`.

# `authorize_tenants`

```elixir
@spec authorize_tenants(
  Plug.Conn.t(),
  String.t() | [String.t()],
  String.t() | nil,
  (Plug.Conn.t(),
   [String.t()]
   | nil -&gt;
     Plug.Conn.t())
) ::
  Plug.Conn.t()
```

`authorize/3` for a resource that spans tenants but is filtered by tenant
(the DLQ, the quarantine pen, replay jobs, the rate-limit listing).

`requested` is the tenant the request names (`nil` for none). `fun` receives
the conn and the tenants the rows must be limited to, forced server-side:

  * `requested` set: the principal must hold `tenant:<requested>` (or
    `tenant:*`), else `403`; `fun` gets `[requested]`.
  * `requested` unset and the principal holds `tenant:*`: `fun` gets `nil`,
    meaning unfiltered.
  * `requested` unset and the principal holds some `tenant:<id>` scopes:
    `fun` gets exactly those ids.
  * a principal with no tenant at all is `403` naming `tenant:*`.

# `bearer`

```elixir
@spec bearer(Plug.Conn.t()) :: {:ok, binary()} | :error
```

The bearer token of `conn`: exactly one `Authorization: Bearer <token>`
header, the scheme matched case-insensitively. A bare or oversized token, a
second header, or another scheme is `:error`.

# `normalize!`

```elixir
@spec normalize!(
  term(),
  String.t(),
  %{:ip =&gt; term(), :tls =&gt; map() | nil, optional(atom()) =&gt; term()},
  boolean()
) :: map()
```

Validate and normalise one `auth` block of a listener. `key` is its dotted
config name (`"admin.auth"`) and `listener` the section it sits in (its `ip`
and `tls` are what `mode: :none` and `mode: :mtls` are checked against).
`enabled?` says whether this instance runs the listener: a listener that is
not started is not held to the loopback rule. Raises `ArgumentError` naming
the key.

# `secure_compare`

```elixir
@spec secure_compare(binary(), binary()) :: boolean()
```

Constant-time comparison of two equal-length binaries.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
