Ankusa.ClaimCheck.Ref (ankusa v0.7.0)

Copy Markdown View Source

A claim-check reference: one string that names a claim's bytes.

urn:ankusa:claim:v1:<tenant>:<claim_id>
urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>:<signature>
  • tenant — [A-Za-z0-9_-]{1,64}; the same string in the URN, the gateway path, and the storage key, with no encoding anywhere
  • claim_id — a canonical (uppercase) Ankusa.ULID. Its first 48 bits are the pack's creation time and pick the pack's dt= folder; its next 5 bits (the id's eleventh character) pick the pack's shard= folder under the :sharded layout (object_key/3); its last 16 bits are the claim's position in the pack. The pack's own id is the claim id with those 16 bits zeroed, so the ref locates the pack without an index lookup.
  • exp_unix (v2) — the Unix second after which the gateway refuses the ref (410), decimal, no leading zero.
  • signature (v2) — the unpadded base64url (43 characters) HMAC-SHA256, under one of claim_check.ref_signing_keys, of the URN up to and excluding the last colon: urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>.

A v1 ref is an unexpiring bearer string; v2 binds the tenant, the claim and an expiry to a key only the operator holds, so a leaked queue message stops granting reads when it expires and a guessed claim id grants nothing. This module parses and verifies the grammar; Ankusa.ClaimCheck.verify_ref/2 applies the node's policy (which keys, whether a v1 ref is still accepted).

The digest isn't part of the ref: a queue message carries it next to the ref, as sha256, and the reader checks it.

A ref maps to a gateway path by dropping the prefix: GET /v1/claims/<tenant>/<claim_id> for v1, and the same path with ?exp=<exp_unix>&sig=<signature> for v2 — see path/1.

Summary

Types

How a pack's storage key is laid out: :flat is claims/tenant=<t>/dt=<d>/<pack>, :sharded adds shard=<c>/ between the tenant and the date — see object_key/3.

t()

Functions

The id of the claim at index in pack pack_id.

The fixed storage prefix every claim object lives under.

The storage key, under layout, of the pack this ref points into.

Split a claim id into its pack's id and its position in that pack.

A fresh pack id, stamped with the current time.

The storage key for a pack: Hive-style partition folders, so Spark, BigQuery, and Athena discover tenant and dt as columns. dt is the UTC date of the pack id's timestamp, so the key is a pure function of the ref. Only call this with a validated tenant and pack id.

Both storage keys a pack may live under, preferred's first: a reader tries the configured layout, then the other one, so a pack written before the layout changed is still found.

The pack id for a Unix millisecond timestamp and 64 bits of entropy: the timestamp, the entropy, and 16 zero bits that each claim's id fills with its position in the pack.

Parse a URN. Rejects anything that isn't exactly the v1 or the v2 grammar. The signature of a v2 ref is checked for shape only; verify/3 checks it.

The gateway path that serves this ref's bytes: with ?exp=<exp_unix>&sig=<signature> when the ref is signed. Neither value needs encoding.

The pack's shard under the :sharded layout: the id's eleventh character, i.e. the first five of its 64 random bits (the first ten characters are the 48-bit timestamp), one of the 32 Crockford characters. Every claim id of a pack shares it, so a reader computes the key from a claim id as is.

ref as a v2 ref that expires at the Unix second expires_at, signed with key.

A v2 ref from its four parts as strings (the gateway's path segments and its exp and sig query parameters), checked for shape only. {:error, :invalid_tenant | :invalid_id} for a bad tenant or claim id, {:error, :bad_signature} for an exp or sig that is not in the grammar.

The URN form: v2 when the ref is signed, else v1.

Is tenant_id a valid tenant name?

:ok if pack_id is a ULID whose position bits are all zero.

:ok if tenant_id is a valid tenant name.

Check a ref against the signing keys, current first. A ref signed with any of them is :ok; now is a Unix second.

Types

layout()

@type layout() :: :flat | :sharded

How a pack's storage key is laid out: :flat is claims/tenant=<t>/dt=<d>/<pack>, :sharded adds shard=<c>/ between the tenant and the date — see object_key/3.

t()

@type t() :: %Ankusa.ClaimCheck.Ref{
  claim_id: String.t(),
  expires_at: pos_integer() | nil,
  signature: String.t() | nil,
  tenant_id: String.t()
}

Functions

claim_id(pack_id, index)

@spec claim_id(String.t(), non_neg_integer()) :: String.t()

The id of the claim at index in pack pack_id.

claims_prefix()

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

The fixed storage prefix every claim object lives under.

key(ref, layout)

@spec key(t(), layout()) :: String.t()

The storage key, under layout, of the pack this ref points into.

locate(claim_id)

@spec locate(term()) :: {:ok, String.t(), non_neg_integer()} | {:error, :invalid_id}

Split a claim id into its pack's id and its position in that pack.

new_pack_id()

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

A fresh pack id, stamped with the current time.

object_key(tenant_id, pack_id, atom)

@spec object_key(String.t(), String.t(), layout()) :: String.t()

The storage key for a pack: Hive-style partition folders, so Spark, BigQuery, and Athena discover tenant and dt as columns. dt is the UTC date of the pack id's timestamp, so the key is a pure function of the ref. Only call this with a validated tenant and pack id.

  • :flat — claims/tenant=<t>/dt=<yyyy-mm-dd>/<pack_id>.
  • :sharded — claims/tenant=<t>/shard=<c>/dt=<yyyy-mm-dd>/<pack_id>, c being shard/1 of the pack id: one of 32 prefixes per tenant, so a busy tenant's writes spread over 32 object-store partitions instead of landing on the tail of one. shard= sits above dt= so a partition formed on a shard stays warm across days, and below tenant= so one tenant is still one prefix to delete.

object_keys(tenant_id, pack_id, atom)

@spec object_keys(String.t(), String.t(), layout()) :: [String.t()]

Both storage keys a pack may live under, preferred's first: a reader tries the configured layout, then the other one, so a pack written before the layout changed is still found.

pack_id(ms, arg)

@spec pack_id(non_neg_integer(), <<_::64>>) :: String.t()

The pack id for a Unix millisecond timestamp and 64 bits of entropy: the timestamp, the entropy, and 16 zero bits that each claim's id fills with its position in the pack.

parse(arg1)

@spec parse(String.t()) :: {:ok, t()} | {:error, :invalid_ref}

Parse a URN. Rejects anything that isn't exactly the v1 or the v2 grammar. The signature of a v2 ref is checked for shape only; verify/3 checks it.

path(ref)

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

The gateway path that serves this ref's bytes: with ?exp=<exp_unix>&sig=<signature> when the ref is signed. Neither value needs encoding.

shard(id)

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

The pack's shard under the :sharded layout: the id's eleventh character, i.e. the first five of its 64 random bits (the first ten characters are the 48-bit timestamp), one of the 32 Crockford characters. Every claim id of a pack shares it, so a reader computes the key from a claim id as is.

sign(ref, key, expires_at)

@spec sign(t(), binary(), pos_integer()) :: t()

ref as a v2 ref that expires at the Unix second expires_at, signed with key.

signed(tenant, claim_id, exp, signature)

@spec signed(term(), term(), term(), term()) ::
  {:ok, t()} | {:error, :invalid_tenant | :invalid_id | :bad_signature}

A v2 ref from its four parts as strings (the gateway's path segments and its exp and sig query parameters), checked for shape only. {:error, :invalid_tenant | :invalid_id} for a bad tenant or claim id, {:error, :bad_signature} for an exp or sig that is not in the grammar.

to_string(ref)

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

The URN form: v2 when the ref is signed, else v1.

valid_tenant?(tenant_id)

@spec valid_tenant?(term()) :: boolean()

Is tenant_id a valid tenant name?

validate_pack_id(pack_id)

@spec validate_pack_id(term()) :: :ok | {:error, :invalid_id}

:ok if pack_id is a ULID whose position bits are all zero.

validate_tenant(tenant_id)

@spec validate_tenant(term()) :: :ok | {:error, :invalid_tenant}

:ok if tenant_id is a valid tenant name.

verify(ref, keys, now)

@spec verify(t(), [binary()], integer()) ::
  :ok | {:error, :unsigned_ref | :bad_signature | :ref_expired}

Check a ref against the signing keys, current first. A ref signed with any of them is :ok; now is a Unix second.

  • {:error, :unsigned_ref} — a v1 ref: it carries nothing to check
  • {:error, :bad_signature} — no key produced this signature; checked before the expiry, so a forged expiry is never reported as a real one
  • {:error, :ref_expired} — signed, and now is past expires_at