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
}
+196
View File
@@ -0,0 +1,196 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
const validConfig = `
listen = "127.0.0.1:9999"
[auth]
token_ref = "mpk-self"
[refs."mpk-self"]
backend = "file"
source = "/tmp/keyproxy.env"
key = "KEYPROXY_TOKEN"
[refs."mpk-example"]
backend = "file"
source = "/tmp/example.env"
key = "EXAMPLE_API_KEY"
[refs."mpk-example-env"]
backend = "env"
source = "EXAMPLE_API_KEY"
[refs."mpk-stub-bw"]
backend = "bitwarden"
source = "sm://p/example"
key = "EXAMPLE_API_KEY"
[refs."mpk-stub-vault"]
backend = "vault"
`
func writeConfig(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "keyproxy.toml")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func TestLoadValid(t *testing.T) {
cfg, err := Load(writeConfig(t, validConfig))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Listen != "127.0.0.1:9999" {
t.Fatalf("listen = %q", cfg.Listen)
}
if cfg.AuthTokenRef != "mpk-self" {
t.Fatalf("auth token ref = %q", cfg.AuthTokenRef)
}
if got := strings.Join(cfg.RefNames, ","); got != "mpk-example,mpk-example-env,mpk-self,mpk-stub-bw,mpk-stub-vault" {
t.Fatalf("ref names = %q", got)
}
file := cfg.Refs["mpk-example"]
if file.Backend != "file" || file.Source != "/tmp/example.env" || file.Key != "EXAMPLE_API_KEY" || file.Mode != "0600" {
t.Fatalf("file ref parsed wrong: %+v", file)
}
if cfg.Refs["mpk-stub-bw"].Backend != "bitwarden" {
t.Fatalf("stub ref parsed wrong: %+v", cfg.Refs["mpk-stub-bw"])
}
}
func TestLoadDefaults(t *testing.T) {
cfg, err := Load(writeConfig(t, strings.Replace(validConfig, `listen = "127.0.0.1:9999"`, "", 1)))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Listen != DefaultListen {
t.Fatalf("default listen = %q, want %q", cfg.Listen, DefaultListen)
}
}
func TestLoadErrors(t *testing.T) {
tests := []struct {
name string
mutate func(string) string
wantErr string
}{
{
name: "missing file",
mutate: func(string) string { return "" },
wantErr: "read config",
},
{
name: "bad ref name",
mutate: func(s string) string { return strings.Replace(s, `mpk-example"`, `mpk_ExAMPLE"`, 1) },
wantErr: "must match mpk-",
},
{
name: "unknown backend",
mutate: func(s string) string { return strings.Replace(s, `backend = "env"`, `backend = "s3"`, 1) },
wantErr: "backend must be one of",
},
{
name: "file ref without source",
mutate: func(s string) string { return strings.Replace(s, `source = "/tmp/example.env"`, "", 1) },
wantErr: "file backend requires source",
},
{
name: "file ref without key",
mutate: func(s string) string { return strings.Replace(s, `key = "EXAMPLE_API_KEY"`, "", 1) },
wantErr: "file backend requires key",
},
{
name: "file ref with junk mode",
mutate: func(s string) string { return strings.Replace(s, `key = "EXAMPLE_API_KEY"`, `key = "EXAMPLE_API_KEY"`+"\n"+`mode = "readable"`, 1) },
wantErr: "mode must be an octal",
},
{
name: "env ref with key",
mutate: func(s string) string { return strings.Replace(s, `source = "EXAMPLE_API_KEY"`, `source = "EXAMPLE_API_KEY"`+"\n"+`key = "X"`, 1) },
wantErr: "env backend must not set key",
},
{
name: "env ref with mode",
mutate: func(s string) string { return strings.Replace(s, `source = "EXAMPLE_API_KEY"`, `source = "EXAMPLE_API_KEY"`+"\n"+`mode = "0644"`, 1) },
wantErr: "mode applies to the file backend only",
},
{
name: "unknown key inside ref table",
mutate: func(s string) string { return strings.Replace(s, `key = "EXAMPLE_API_KEY"`, `key = "EXAMPLE_API_KEY"`+"\n"+`ttl = "5m"`, 1) },
wantErr: "unknown key",
},
{
name: "no auth section",
mutate: func(s string) string { return strings.Replace(s, "token_ref = \"mpk-self\"", "", 1) },
wantErr: "token_ref is required",
},
{
name: "auth ref not configured",
mutate: func(s string) string { return strings.Replace(s, `token_ref = "mpk-self"`, `token_ref = "mpk-ghost"`, 1) },
wantErr: "not a configured ref",
},
{
name: "auth ref not file backend",
mutate: func(s string) string {
return strings.Replace(s, `token_ref = "mpk-self"`, `token_ref = "mpk-example-env"`, 1)
},
wantErr: "must use the file backend",
},
{
name: "no refs at all",
mutate: func(s string) string { return "listen = \"127.0.0.1:9999\"\n[auth]\ntoken_ref = \"mpk-self\"\n" },
wantErr: "no refs configured",
},
{
name: "unknown top-level key",
mutate: func(s string) string { return "ttl = \"5m\"\n" + s },
wantErr: "unknown top-level key",
},
{
name: "unknown auth key",
mutate: func(s string) string { return strings.Replace(s, "token_ref = \"mpk-self\"", "token_ref = \"mpk-self\"\nrotation = \"7d\"", 1) },
wantErr: "unknown key",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content := tt.mutate(validConfig)
path := writeConfig(t, content)
if content == "" {
path = filepath.Join(t.TempDir(), "absent.toml")
}
cfg, err := Load(path)
if err == nil {
t.Fatalf("want error %q, got config %+v", tt.wantErr, cfg)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error %q does not contain %q", err, tt.wantErr)
}
})
}
}
func TestValidRef(t *testing.T) {
valid := []string{"mpk-a", "mpk-redmine", "mpk-a-1", "mpk-9"}
invalid := []string{"", "redmine", "MPK-a", "mpk-", "mpk--a", "mpk_a", "mpk-a_b", "mpk-a.b", "mpk-a b", "sk-live-pasted-secret"}
for _, r := range valid {
if !ValidRef(r) {
t.Errorf("ValidRef(%q) = false, want true", r)
}
}
for _, r := range invalid {
if ValidRef(r) {
t.Errorf("ValidRef(%q) = true, want false", r)
}
}
}
+255
View File
@@ -0,0 +1,255 @@
package config
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Doc is the parsed representation of keyproxy.toml: nested tables are
// Doc values; leaves are string, int64 or bool. This is a deliberate
// stdlib-only TOML subset (tables with quoted keys, basic strings with
// escapes, integers, booleans, comments). Anything richer is a parse
// error so a misconfigured file fails loudly instead of silently.
type Doc map[string]any
// Table returns the nested table at the key path, or an empty doc.
func (d Doc) Table(keys ...string) Doc {
cur := any(d)
for _, k := range keys {
m, ok := cur.(Doc)
if !ok {
return Doc{}
}
cur = m[k]
}
if m, ok := cur.(Doc); ok {
return m
}
return Doc{}
}
// Str returns the string leaf at key.
func (d Doc) Str(key string) (string, bool) {
v, ok := d[key].(string)
return v, ok
}
// Int returns the int64 leaf at key.
func (d Doc) Int(key string) (int64, bool) {
v, ok := d[key].(int64)
return v, ok
}
// Bool returns the bool leaf at key.
func (d Doc) Bool(key string) (bool, bool) {
v, ok := d[key].(bool)
return v, ok
}
// Keys lists the doc's keys, sorted.
func (d Doc) Keys() []string {
out := make([]string, 0, len(d))
for k := range d {
out = append(out, k)
}
sort.Strings(out)
return out
}
// parseTOML parses the TOML subset into a Doc. Errors carry line
// numbers, never line contents (a config file may sit next to secrets
// someone pasted in by mistake).
func parseTOML(data []byte) (Doc, error) {
root := Doc{}
cur := root
for i, raw := range strings.Split(string(data), "\n") {
lineno := i + 1
line := stripComment(raw)
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "[") {
if !strings.HasSuffix(line, "]") {
return nil, fmt.Errorf("line %d: malformed table header", lineno)
}
path, err := splitKeyPath(strings.TrimSpace(line[1 : len(line)-1]))
if err != nil || len(path) == 0 {
return nil, fmt.Errorf("line %d: malformed table header", lineno)
}
cur = root
for _, seg := range path {
next, ok := cur[seg].(Doc)
if !ok {
if _, exists := cur[seg]; exists {
return nil, fmt.Errorf("line %d: table conflicts with value key %q", lineno, seg)
}
next = Doc{}
cur[seg] = next
}
cur = next
}
continue
}
eq := strings.IndexByte(line, '=')
if eq < 1 {
return nil, fmt.Errorf("line %d: expected key = value", lineno)
}
key := strings.TrimSpace(line[:eq])
key = strings.Trim(key, `"'`)
if key == "" || strings.ContainsAny(key, `".[]`) {
return nil, fmt.Errorf("line %d: malformed key", lineno)
}
value := strings.TrimSpace(line[eq+1:])
parsed, err := parseValue(value)
if err != nil {
return nil, fmt.Errorf("line %d: %v", lineno, err)
}
if _, dup := cur[key]; dup {
return nil, fmt.Errorf("line %d: duplicate key %q", lineno, key)
}
cur[key] = parsed
}
return root, nil
}
// splitKeyPath splits a.b."c.d" into [a b c.d]; each segment is bare or
// double-quoted.
func splitKeyPath(s string) ([]string, error) {
var segs []string
for i := 0; i < len(s); {
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
if i >= len(s) {
break
}
switch s[i] {
case '"':
j := i + 1
for j < len(s) && s[j] != '"' {
j++
}
if j >= len(s) {
return nil, fmt.Errorf("unterminated quoted key")
}
segs = append(segs, s[i+1:j])
i = j + 1
default:
j := i
for j < len(s) && s[j] != '.' {
j++
}
seg := strings.TrimSpace(s[i:j])
if seg == "" {
return nil, fmt.Errorf("empty key segment")
}
segs = append(segs, seg)
i = j
}
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
if i < len(s) {
if s[i] != '.' {
return nil, fmt.Errorf("unexpected character in table header")
}
i++
}
}
return segs, nil
}
// parseValue parses a basic string, integer or boolean.
func parseValue(v string) (any, error) {
if v == "" {
return nil, fmt.Errorf("missing value")
}
switch v[0] {
case '"':
s, rest, err := parseBasicString(v)
if err != nil {
return nil, err
}
if strings.TrimSpace(rest) != "" {
return nil, fmt.Errorf("trailing characters after string value")
}
return s, nil
case '\'':
end := strings.IndexByte(v[1:], '\'')
if end < 0 {
return nil, fmt.Errorf("unterminated literal string")
}
s := v[1 : 1+end]
if strings.TrimSpace(v[2+end:]) != "" {
return nil, fmt.Errorf("trailing characters after string value")
}
return s, nil
}
switch v {
case "true":
return true, nil
case "false":
return false, nil
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("unsupported value (expected string, int or bool)")
}
return n, nil
}
// parseBasicString parses one "..." basic string with \\ \" \n \t \r
// escapes; it returns the string and the unparsed remainder.
func parseBasicString(v string) (string, string, error) {
var b strings.Builder
for i := 1; i < len(v); i++ {
switch v[i] {
case '"':
return b.String(), v[i+1:], nil
case '\\':
if i+1 >= len(v) {
return "", "", fmt.Errorf("dangling escape in string")
}
i++
switch v[i] {
case '\\':
b.WriteByte('\\')
case '"':
b.WriteByte('"')
case 'n':
b.WriteByte('\n')
case 't':
b.WriteByte('\t')
case 'r':
b.WriteByte('\r')
default:
return "", "", fmt.Errorf("unsupported escape in string")
}
default:
b.WriteByte(v[i])
}
}
return "", "", fmt.Errorf("unterminated string")
}
// stripComment removes a trailing # comment that sits outside quotes.
func stripComment(line string) string {
var quote byte
for i := 0; i < len(line); i++ {
c := line[i]
switch {
case quote != 0:
if c == quote {
quote = 0
}
case c == '"' || c == '\'':
quote = c
case c == '#':
return line[:i]
}
}
return line
}
+97
View File
@@ -0,0 +1,97 @@
package config
import (
"strings"
"testing"
)
func TestParseTOML(t *testing.T) {
tests := []struct {
name string
in string
want map[string]any // dotted paths -> expected values
wantErr string
}{
{
name: "scalars and comments",
in: "# top\nlisten = \"127.0.0.1:8082\" # inline\nflag = true\nnum = 42\n",
want: map[string]any{"listen": "127.0.0.1:8082", "flag": true, "num": int64(42)},
},
{
name: "tables with quoted dotted keys",
in: "[refs.\"mpk-redmine\"]\nbackend = \"file\"\n\n[refs.\"mpk-env\"]\nbackend = \"env\"\n",
want: map[string]any{"refs.mpk-redmine.backend": "file", "refs.mpk-env.backend": "env"},
},
{
name: "escapes in basic strings",
in: "a = \"x\\ty\\\"z\"\n",
want: map[string]any{"a": "x\ty\"z"},
},
{
name: "literal strings keep backslashes",
in: "a = 'C:\\path'\n",
want: map[string]any{"a": `C:\path`},
},
{
name: "duplicate key",
in: "a = \"1\"\na = \"2\"\n",
wantErr: "duplicate key",
},
{
name: "unterminated string",
in: "a = \"oops\n",
wantErr: "unterminated string",
},
{
name: "unsupported richer syntax fails loudly",
in: "a = [\"x\", \"y\"]\n",
wantErr: "unsupported value",
},
{
name: "malformed table header",
in: "[refs\n",
wantErr: "malformed table header",
},
{
name: "bare line without equals",
in: "garbage\n",
wantErr: "expected key = value",
},
{
name: "table conflicts with scalar",
in: "refs = \"x\"\n[refs.\"mpk-a\"]\nbackend = \"file\"\n",
wantErr: "conflicts",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doc, err := parseTOML([]byte(tt.in))
if tt.wantErr != "" {
if err == nil {
t.Fatalf("want error %q, got doc %v", tt.wantErr, doc)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error %q does not contain %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for path, want := range tt.want {
segs := strings.Split(path, ".")
cur := any(doc)
for _, seg := range segs {
m, ok := cur.(Doc)
if !ok {
t.Fatalf("path %s: %v is not a table", path, cur)
}
cur = m[seg]
}
if cur != want {
t.Fatalf("path %s: got %#v, want %#v", path, cur, want)
}
}
})
}
}