apiauth

package
v0.7.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package apiauth resolves the bearer-token principal for a request. It is shared by the REST (internal/api/rest) and MCP (internal/api/mcp) HTTP surfaces so the two transports authenticate identically for the same credentials — admin-key semantics, table-key lookup, disabled-key rejection, and the auth-enforcement edge cases around an empty admin key are implemented exactly once here rather than risking the two copies drifting (see the K2 brief's cross-surface consistency requirement).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateSecret

func GenerateSecret() (string, error)

GenerateSecret returns a fresh 32-byte hex-encoded (64 char) random secret, the plaintext credential handed to a caller exactly once — over the CLI (cmd/memini/key.go) or the REST key-management API (K3b) — and never stored; only HashToken's digest of it is persisted. Exported as the one canonical secret generator, mirroring HashToken: both surfaces must mint secrets identically rather than risk two implementations drifting apart.

func HashToken

func HashToken(token string) string

HashToken hashes a presented bearer secret with hex SHA-256, matching store.APIKey.Hash's format (see its doc). Exported as the one canonical hashing helper: the CLI (cmd/memini/key.go, generating and rotating table keys) and this package's own auth-path lookups must never risk drifting onto two different hash implementations.

Types

type Config

type Config struct {
	APIKey   string
	KeyStore store.APIKeyStore // nil disables table auth entirely
	// contains filtered or unexported fields
}

Config is the shared bearer-token auth policy: the admin env key (checked first, constant-time), an optional immutable set of declaratively managed keys loaded once at boot from MEMINI_API_KEYS_FILE (checked second), and an optional table of named keys (SHA-256 hash lookup, checked last). Copy freely — the embedded cache pointer is shared across copies, which is what lets the emptiness cache persist across requests even though each middleware invocation works from its own copy of Config. fileKeys is safe to share across copies too: FileKeySet is immutable once loaded.

func New

func New(apiKey string, ks store.APIKeyStore) Config

New builds a Config ready to Authenticate. ks may be nil — no table capability, e.g. a store predating APIKeyStore, or the feature unused. Use WithFileKeys to additionally wire in a declarative keys file.

func (Config) Authenticate

func (c Config) Authenticate(ctx context.Context, token string) (p *Principal, ok bool, err error)

Authenticate resolves token (the raw bearer, "" when absent) against the admin key, then the file keys (MEMINI_API_KEYS_FILE, K2b), then the table. Semantics (binding, K2/K2b briefs):

  • Admin key configured and token matches it (constant-time) → allowed, principal nil.
  • A bearer is presented AND fileKeys is set → looked up by hex SHA-256 hash. Found (enabled or not) → resolved here, final: enabled means allowed with a principal identifying the key; disabled means REJECTED outright, same as a disabled table key below. Found-but-disabled never falls through to the table — a file entry that names this hash is authoritative. Not found in the file falls through to the table below (a token merely not being a file key doesn't make it invalid — it might still be a DB key).
  • A bearer is presented AND KeyStore is set → looked up by hex SHA-256 hash. Found and enabled → allowed, principal identifies the key. Found-but-disabled, or not found → REJECTED outright; this never falls through to the "no usable token" allowance below, because a wrong credential is not the same as no credential.
  • No usable token (absent, or fileKeys/KeyStore that can't be consulted because they're nil): allowed, principal nil, IFF nothing requires auth — no admin key AND fileKeys is empty/nil AND (no KeyStore, or its table is empty). An admin key configured with no/wrong token is rejected (unchanged pre-existing behavior). A non-empty file key set, OR a configured non-empty table, with no/wrong token is rejected too — either one makes auth mandatory the instant it holds any key, not merely additive to dev-mode. The file set's emptiness is a plain field read (it's immutable once loaded, unlike the table); the table's is the cached, possibly-stale tableNonEmpty read.

A KeyStore error while looking up a presented token surfaces as err (the caller should respond 500, not 401 — an inability to check the table is not the same as an invalid credential). A KeyStore error while merely probing table emptiness does NOT surface as err; it fails closed (treated as non-empty, i.e. auth required) and is absorbed — see tableNonEmpty. FileKeySet lookups never error (it's an in-memory map), so there is no equivalent failure mode on the file side.

func (Config) FileKeys

func (c Config) FileKeys() []store.APIKey

FileKeys returns metadata (including hash, never a plaintext secret) for every key loaded from MEMINI_API_KEYS_FILE, ordered by name — nil when no file is configured. This is the seam a future read-only /v1/keys listing (K3b) uses to fold file keys into its output alongside table keys.

func (Config) Invalidate

func (c Config) Invalidate()

Invalidate clears the cached table-emptiness reading so the very next Authenticate call re-queries the store instead of riding out keyTableCacheTTL. Callers that mutate the api_keys table in the SAME process as a running server (K3b's REST create/update/delete handlers) must call this immediately after a successful write: without it, a just-created first key would not enforce auth for up to keyTableCacheTTL (breaking the UI-first bootstrap flow's "create key → auth enforced NOW" guarantee) and a just-deleted last key would keep requiring auth for the same window. The CLI, which writes to the store directly and is very possibly a different OS process than any running server, cannot invalidate a live server's cache this way — that revocation/bootstrap lag against a separately-running server is an accepted, documented TTL tradeoff, not a bug this method can fix. Safe to call even when cache is shared across copies of Config (see the struct doc); resets to the zero Time, which Since() always reports as >= TTL, forcing a re-check on the next read.

func (Config) IsFileKey

func (c Config) IsFileKey(name string) bool

IsFileKey reports whether name identifies a key sourced from MEMINI_API_KEYS_FILE. K3b's future key-mutation endpoints must consult this and refuse to rename/rotate/delete a file-sourced key by name — the file owns that identity, not the API.

func (Config) WithFileKeys

func (c Config) WithFileKeys(fk *FileKeySet) Config

WithFileKeys returns a copy of c with fk attached as the declaratively managed key set (see FileKeySet's doc), consulted after the admin key and before the table — see Authenticate. fk may be nil, e.g. MEMINI_API_KEYS_FILE is unset: this leaves file-key auth disabled with zero behavior change, same as never calling WithFileKeys at all.

type FileKeySet

type FileKeySet struct {
	// contains filtered or unexported fields
}

FileKeySet is the immutable, in-memory result of loading MEMINI_API_KEYS_FILE at boot (see LoadFileKeys). It is consulted by Config.Authenticate after the admin key and before the table (store.APIKeyStore) — see Authenticate's doc for the full precedence and auth-mode rules. There is deliberately no mutation API: the file is the source of truth for these keys, reloaded only by restarting the process (a SIGHUP-triggered reload is a reasonable future addition but is not built here — GitOps rollouts already restart the pod on a file change, so a boot-time load is sufficient for now).

func LoadFileKeys

func LoadFileKeys(path string) (*FileKeySet, error)

LoadFileKeys parses and validates the declarative API keys file at path. It is meant to run exactly once, at server boot: see FileKeySet's doc for why there is no live reload. Every validation failure names path and the offending entry (by index and, when available, by name) so an operator can fix the file without hunting — callers should treat any error here as fatal to boot (fail loud), never as "start with the file's keys disabled".

Validation performed, each a fatal error:

  • the file must parse as YAML matching the documented shape
  • every entry needs a non-empty name, unique within the file
  • every entry needs EXACTLY one of hash (hex-encoded SHA-256 of the secret) or secret (the plaintext, hashed here and never retained — see fileKeyEntry.Secret)
  • hash, when given, must decode as exactly 32 bytes of hex
  • no two entries may resolve to the same hash (i.e. share a secret), regardless of whether each declared it as hash or secret — the error names both entries by name, never echoing the hash or secret
  • home / default_namespace, when given, are normalized (httputil.NormalizeNamespace) and must pass httputil.ValidateNamespace
  • settings, when given, is a per-key ClientSettings override that must carry only known keys (strict decode) and pass ClientSettings.Validate

func (*FileKeySet) FileKeys

func (fk *FileKeySet) FileKeys() []store.APIKey

FileKeys returns every declaratively managed key ordered by name — the same shape as store.APIKeyStore.ListAPIKeys, including the hash (not sensitive: ListAPIKeys already exposes it for table keys) but never a plaintext secret. This is the seam K3b's read-only key listing consumes to fold file keys into its output. A nil receiver (feature off) returns nil.

func (*FileKeySet) IsFileKey

func (fk *FileKeySet) IsFileKey(name string) bool

IsFileKey reports whether name identifies a declaratively managed key. K3b's future key-mutation endpoints (rename/rotate/delete) must consult this and refuse by name — the file, not the API, owns these keys' identity. A nil receiver (feature off) always reports false.

func (*FileKeySet) ShadowedDBKeyNames

func (fk *FileKeySet) ShadowedDBKeyNames(ctx context.Context, ks store.APIKeyStore) ([]string, error)

ShadowedDBKeyNames returns the names of DB-stored keys (ks.ListAPIKeys) that share a name with a file key — those DB rows still exist, but the file wins at auth time (see Authenticate), so they're effectively dead weight until removed or the file entry is deleted. Meant to be called once at boot, only when a file is configured, so an operator sees a warning naming exactly which DB keys are shadowed. fk nil or ks nil (no file configured, or no table capability) returns (nil, nil) — nothing to check.

The check is ADVISORY: a ListAPIKeys error is returned so the caller can log it, but it must never be treated as boot-fatal — the same query failing inside tableNonEmpty is absorbed too (fail closed and continue), and refusing a boot over a warning the server can live without would be strictly worse than that precedent. See cmd/memini/root.go's newServer.

type Principal

type Principal struct {
	Name      string
	HomeNS    string
	DefaultNS string
	Admin     bool
}

Principal identifies a request as authenticated by a NAMED key (table or file) — never the admin env key, which authenticates with no principal at all (see Config.Authenticate). Name is the attribution identity consumed by RememberInput.Author; HomeNS/DefaultNS carry the key's bound home and per-key default namespace, consumed by the caller's home/namespace resolution. Admin carries the key's per-key admin capability (store.APIKey.Admin) — adminness is now a capability any named principal can hold, no longer solely the nil-principal env key's exclusive property.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL