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

Who is calling a control plane, and what they may do: the result of
`c: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

| Scope | Grants |
| --- | --- |
| `metrics:read` | `GET /metrics`, `GET /v1/wal` |
| `config:read` | `GET /v1/config`, `GET /asyncapi.json` |
| `config:write` | `PUT /v1/log-level` and `POST /v1/config/reload` (with `tenant:*`: both are the node's, for every tenant) |
| `sources:read`, `sources:write` | a tenant's sources |
| `dlq:read` | `GET /v1/dlq`; `dlq:write`: starting a `dlq` replay |
| `replays:read`, `replays:write` | replay jobs (`dlq:write` / `quarantine:write` as well for those kinds) |
| `quarantine:read`, `quarantine:write` | the quarantine pen |
| `rate_limits:read`, `rate_limits:write` | per-tenant rate limits |
| `tenants:write` | `DELETE /v1/tenants/:tenant` (a tenant erase) and `GET /v1/tenants/:tenant/erase`; with `tenant:*` |
| `routes:read`, `routes:write` | route definitions, IP rules, the dry run |
| `claims:read` | claim-check gateway reads |
| `audit:read` | reserved 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:*`.

# `t`

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

# `all_tenants?`

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

Does the principal hold `tenant:*`?

# `new`

```elixir
@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`

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

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

# `parse_scopes`

```elixir
@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?`

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

Does the principal hold `scope`?

# `scopes`

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

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

# `tenant?`

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

May the principal act on `tenant`?

---

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