Add config loader with strict stdlib-only TOML subset parser

Parse keyproxy.toml (listen address, [auth] token_ref, and the mpk-<name>
ref map) using a deliberate TOML subset implemented with the standard
library only. The surface is strict: unknown keys, malformed refs, and
backend-specific misconfigurations (e.g. env refs setting key, file refs
missing key) fail loudly at startup. The bearer-token ref must resolve
through the file backend. Parse errors carry line numbers, never line
contents.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-28 22:30:50 -05:00
parent b43bc55963
commit 7bec2a1905
4 changed files with 705 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
// Package config loads keyproxy.toml: the listen address, the bearer
// auth ref, and the ref map (mpk-<name> -> backend/source/key). The
// config holds locations only — never material; keyproxy.toml.example is
// tracked, the real file is gitignored.
package config
import (
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
"git.knownelement.com/ukrrs/mopac-keyproxy/internal/backend"
)
// DefaultListen is the loopback bind address used when [listen] is unset.
const DefaultListen = "127.0.0.1:8082"
// knownBackends are the backend names the loader accepts.
var knownBackends = map[string]bool{
"file": true,
"env": true,
"bitwarden": true,
"vault": true,
}
// refPattern is the shape every consumer-facing ref must have
// (mpk-<name>, lowercase/digits/hyphens). Because the pattern is
// enforced before any echo, log lines and error bodies can name refs
// safely: an mpk- ref is a placeholder, by construction not material.
var refPattern = regexp.MustCompile(`^mpk-[a-z0-9][a-z0-9-]*$`)
// ValidRef reports whether name is a well-formed mpk- ref.
func ValidRef(name string) bool { return refPattern.MatchString(name) }
// Config is the parsed keyproxy.toml.
type Config struct {
Listen string // bind address, default 127.0.0.1:8082
AuthTokenRef string // [auth] token_ref; must be a file-backend ref
Refs map[string]backend.Ref // ref name -> backend ref
RefNames []string // sorted ref names
}
// Load reads, parses and validates the config at path.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
doc, err := parseTOML(data)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
cfg := &Config{Refs: map[string]backend.Ref{}}
// Strict surface: unknown keys fail loudly (a misconfigured file must
// not start on typo'd best effort).
for _, k := range doc.Keys() {
switch k {
case "listen", "auth", "refs":
default:
return nil, fmt.Errorf("%s: unknown top-level key %q (allowed: listen, auth, refs)", path, k)
}
}
if s, ok := doc.Str("listen"); ok {
cfg.Listen = s
} else {
cfg.Listen = DefaultListen
}
auth := doc.Table("auth")
for _, k := range auth.Keys() {
if k != "token_ref" {
return nil, fmt.Errorf("%s: [auth]: unknown key %q (allowed: token_ref)", path, k)
}
}
cfg.AuthTokenRef, _ = auth.Str("token_ref")
if cfg.AuthTokenRef == "" {
return nil, fmt.Errorf("[auth]: token_ref is required (bearer token for /v1/resolve, resolved through the file backend)")
}
refs := doc.Table("refs")
if len(refs) == 0 {
return nil, fmt.Errorf("[refs]: no refs configured")
}
for _, name := range refs.Keys() {
ref, err := parseRef(name, refs.Table(name))
if err != nil {
return nil, err
}
cfg.Refs[name] = ref
cfg.RefNames = append(cfg.RefNames, name)
}
sort.Strings(cfg.RefNames)
authRef, ok := cfg.Refs[cfg.AuthTokenRef]
if !ok {
return nil, fmt.Errorf("[auth]: token_ref %q is not a configured ref", cfg.AuthTokenRef)
}
if authRef.Backend != "file" {
return nil, fmt.Errorf("[auth]: token_ref %q must use the file backend (auth is bootstrapped from a 0600 env file), got %q", cfg.AuthTokenRef, authRef.Backend)
}
return cfg, nil
}
// parseRef validates one [refs."mpk-<name>"] table.
func parseRef(name string, t Doc) (backend.Ref, error) {
if !ValidRef(name) {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: ref name must match mpk-<name> (lowercase alphanumerics and hyphens)`, name)
}
ref := backend.Ref{Name: name}
ref.Backend, _ = t.Str("backend")
if !knownBackends[ref.Backend] {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: backend must be one of file, env, bitwarden, vault`, name)
}
ref.Source, _ = t.Str("source")
ref.Key, _ = t.Str("key")
ref.Mode, _ = t.Str("mode")
if ref.Mode == "" {
ref.Mode = "0600"
}
for _, k := range t.Keys() {
switch k {
case "backend", "source", "key", "mode":
default:
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: unknown key %q (allowed: backend, source, key, mode)`, name, k)
}
}
switch ref.Backend {
case "file":
if ref.Source == "" {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: file backend requires source (path to a 0600 env file)`, name)
}
if ref.Key == "" {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: file backend requires key (KEY inside the env file)`, name)
}
if m, err := strconv.ParseUint(strings.TrimPrefix(ref.Mode, "0o"), 8, 32); err != nil || os.FileMode(m)&os.ModePerm == 0 {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: mode must be an octal permission mask like "0600"`, name)
}
case "env":
if ref.Source == "" {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: env backend requires source (environment variable name)`, name)
}
if ref.Key != "" {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: env backend must not set key (source IS the variable name)`, name)
}
if _, ok := t.Str("mode"); ok {
return backend.Ref{}, fmt.Errorf(`[refs."%s"]: mode applies to the file backend only`, name)
}
}
return ref, nil
}