Ankusa.ClaimCheck.Router (ankusa v0.7.0)

Copy Markdown View Source

The :claim_check role's HTTP API: read-only byte transport for claim-check refs. Deliberately its own listener (claim_check.port, default 4001), separate from Ankusa.Edge.Router: the edge is internet-facing, this is internal-network-only.

Every request but GET /health is authenticated (claim_check.auth, see Ankusa.Admin.Auth) and authorized: 401 without a credential, 403 unless the caller holds claims:read and tenant:<tenant_id> (or tenant:*) for the tenant in the path — or, with claim_check.authorize set to {module, fun, args}, unless apply(module, fun, [context | args]) returns :ok, context being %{principal, tenant_id, claim_id, conn} (the path's two segments as given, not yet validated). The hook replaces the built-in check and runs before anything is read. The default mode: :none is accepted on loopback only; anywhere else configure :jwt, :mtls or :token, or say with allow_insecure: true that a mesh or proxy in front of the port authenticates every caller. A shared cache must sit behind that layer, never in front of it.

API

  • GET /v1/claims/:tenant_id/:claim_id — the claim's exact bytes, application/octet-stream, Cache-Control: private, max-age=31536000, immutable (claims are written once and never rewritten; private because the path is a capability, and a shared cache would serve it to whoever asks), Accept-Ranges: bytes. 400 malformed, 404 no such claim, 503 store_unavailable + Retry-After (a random 1–3 seconds, so clients a throttled store turned away don't all come back at once) when the store is unavailable, throttling, or slower than the deadline, or this node is shedding load, 503 store_forbidden + Retry-After: 60 when the store refused this node's credential. A 503 body never carries the store's error; it is logged. 502 integrity_mismatch when the bytes read (or the pack's manifest) are not what the manifest records.
  • The same GET with one Range: bytes=a-b, bytes=a- or bytes=-n — 206 with those bytes and Content-Range: bytes a-b/length (b clamped to the claim's end), read from the store as one ranged read; a range that starts past the end (or a zero-length claim, or bytes=-0) is 416 with Content-Range: bytes */length. Several ranges, another unit, a malformed spec, or any If-Range are ignored: 200 with the whole claim.
  • HEAD /v1/claims/:tenant_id/:claim_id — the same status and headers, content-length included, without the body; it reads only the pack's index, never the claim or its manifest. Range is ignored.
  • GET /health — liveness.

Signed refs

A v2 ref (Ankusa.ClaimCheck.Ref) is redeemed at the same path with ?exp=<exp_unix>&sig=<signature>. After authorization and before any read the gateway holds it to Ankusa.ClaimCheck.verify_ref/2: 400 bad_signature when no claim_check.ref_signing_keys entry signed this tenant, claim and expiry (or exp/sig are missing one, or out of grammar), 410 ref_expired when it did and the expiry has passed, 400 unsigned_ref for a path with neither while claim_check.accept_unsigned_refs is false. All three are permanent. Past claim_check.max_concurrent_reads requests in flight on this node a request is 503 overloaded + Retry-After (random 1–3 seconds) before anything is read.

Integrity

With claim_check.integrity_check: :manifest (the default) a whole-claim GET is checked against the sha256 the pack's manifest records for that claim (Ankusa.ClaimCheck.digest/3: read once per pack and cached with the pack's index, shared by every concurrent request): the bytes that don't match are 502 integrity_mismatch, never sent. A 200 or 206 carries the digest as x-ankusa-sha256 (lowercase hex of the whole claim), so a client reassembling ranges can check what it assembled; HEAD reads only the index, so it sends none. :off serves the store's bytes as they are, with no header. The reader still holds the bytes to the sha256 its queue message carried (Ankusa.ClaimCheck.redeem/3 does this in-process): the manifest is the store's own record of the bytes, the message's digest is the writer's. The whole response is one binary, not a stream: a claim is at most max_body_bytes.

Under load

Every claim request answers within claim_check.read_deadline_ms. Its reads go through Ankusa.ClaimCheck.Cache (pack indexes and claims already read) and Ankusa.ClaimCheck.Reader (coalescing, a per-store adaptive limit on store requests, a bounded queue, and the deadline). Each authorized request emits [:ankusa, :claim_check, :gateway] with its method, status and duration; a 401 or 403 is refused before any of it and emits nothing.

Summary

Functions

Callback implementation for Plug.call/2.

Callback implementation for Plug.init/1.

Functions

call(conn, opts)

Callback implementation for Plug.call/2.

init(opts)

Callback implementation for Plug.init/1.