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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user