Add backend resolver interface with file and env backends plus phase-3 stubs
Introduce the single Backend interface every credential source implements.
v0 ships two working backends: file (0600 KEY=VALUE env files, parsed in
pure Go, never sourced; looser permission masks refused before read) and
env (process-environment indirection). Bitwarden Secrets Manager and
HashiCorp Vault ship as explicit not-implemented stubs behind the same
interface so the phase-3 connectors are drop-ins. All failures are typed
ResolveErrors carrying only the ref, backend, and a fixed reason enum —
never material.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// Package backend defines the resolver interface every credential source
|
||||
// implements. Backends receive opaque refs (mpk-<name>) and return key
|
||||
// material; material is held in memory only, never persisted, never
|
||||
// written to logs. Errors identify the ref, the backend, and a
|
||||
// machine-readable reason — never material.
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Ref is one configured mpk-<name> entry from keyproxy.toml. Source and
|
||||
// Key name WHERE material lives; they are locations, not material.
|
||||
type Ref struct {
|
||||
Name string `json:"ref"` // consumer-facing opaque ref, e.g. "mpk-redmine"
|
||||
Backend string `json:"backend"` // file | env | bitwarden | vault
|
||||
Source string `json:"-"` // backend-specific source (file path, env var name, vault path)
|
||||
Key string `json:"-"` // key inside the source (env-file key, secret name)
|
||||
Mode string `json:"-"` // file backend: allowed permission mask, default "0600"
|
||||
}
|
||||
|
||||
// Reason is a machine-readable failure class. Reason strings are the only
|
||||
// failure detail that may cross the wire or reach logs: they are fixed
|
||||
// enums and can never embed source contents.
|
||||
type Reason string
|
||||
|
||||
const (
|
||||
ReasonUnknownRef Reason = "unknown_ref"
|
||||
ReasonNotImplemented Reason = "not_implemented"
|
||||
ReasonUnreadableSource Reason = "unreadable_source"
|
||||
ReasonInsecureMode Reason = "insecure_source_mode"
|
||||
ReasonMalformedSource Reason = "malformed_source"
|
||||
ReasonMissingKey Reason = "missing_key"
|
||||
ReasonEmptyValue Reason = "empty_value"
|
||||
)
|
||||
|
||||
// ResolveError names the ref, the backend and a reason. It never carries
|
||||
// material; its Error() output is safe to log.
|
||||
type ResolveError struct {
|
||||
Ref string
|
||||
Backend string
|
||||
Reason Reason
|
||||
}
|
||||
|
||||
func (e *ResolveError) Error() string {
|
||||
return fmt.Sprintf("ref %s: backend %s: %s", e.Ref, e.Backend, e.Reason)
|
||||
}
|
||||
|
||||
// Err wraps err as a ResolveError for ref/backend, deduplicating if it
|
||||
// already is one.
|
||||
func Err(ref Ref, backend string, reason Reason) error {
|
||||
return &ResolveError{Ref: ref.Name, Backend: backend, Reason: reason}
|
||||
}
|
||||
|
||||
// AsResolveError extracts a *ResolveError, or wraps an unexpected error
|
||||
// as a generic unreadable_source failure (the underlying text is dropped:
|
||||
// it is never safe to assume it is material-free).
|
||||
func AsResolveError(err error, ref Ref, backend string) *ResolveError {
|
||||
var re *ResolveError
|
||||
if err != nil && errors.As(err, &re) {
|
||||
return re
|
||||
}
|
||||
return &ResolveError{Ref: ref.Name, Backend: backend, Reason: ReasonUnreadableSource}
|
||||
}
|
||||
|
||||
// Backend resolves one Ref to its material. Implementations must not
|
||||
// persist material, cache it to disk, or write it to any log.
|
||||
type Backend interface {
|
||||
// Name is the backend's config-facing name (file, env, ...).
|
||||
Name() string
|
||||
// Resolve returns the material for ref. Errors must be (or wrap) a
|
||||
// *ResolveError; returned strings exist in memory only.
|
||||
Resolve(ctx context.Context, ref Ref) (string, error)
|
||||
}
|
||||
|
||||
// Registry maps backend names to implementations.
|
||||
type Registry struct {
|
||||
m map[string]Backend
|
||||
}
|
||||
|
||||
// NewRegistry builds a registry from the given backends.
|
||||
func NewRegistry(backends ...Backend) *Registry {
|
||||
r := &Registry{m: make(map[string]Backend, len(backends))}
|
||||
for _, b := range backends {
|
||||
r.m[b.Name()] = b
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Get returns the backend registered under name.
|
||||
func (r *Registry) Get(name string) (Backend, bool) {
|
||||
b, ok := r.m[name]
|
||||
return b, ok
|
||||
}
|
||||
|
||||
// Names lists registered backend names, sorted.
|
||||
func (r *Registry) Names() []string {
|
||||
out := make([]string, 0, len(r.m))
|
||||
for n := range r.m {
|
||||
out = append(out, n)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user