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,77 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// File resolves refs from env files on disk (KEY=VALUE lines, parsed in
|
||||
// Go, never sourced). House rule: secret files are 0600. A file whose
|
||||
// permission bits reach beyond the allowed mask (default 0600; stricter
|
||||
// masks such as 0400 always pass) is refused before it is read. Material
|
||||
// is read on demand per resolve and kept in memory only.
|
||||
type File struct{}
|
||||
|
||||
// NewFile returns the file backend.
|
||||
func NewFile() *File { return &File{} }
|
||||
|
||||
// Name implements Backend.
|
||||
func (f *File) Name() string { return "file" }
|
||||
|
||||
// Resolve implements Backend.
|
||||
func (f *File) Resolve(ctx context.Context, ref Ref) (string, error) {
|
||||
path := expandHome(ref.Source)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
return "", Err(ref, f.Name(), ReasonUnreadableSource)
|
||||
}
|
||||
if info.Mode().Perm()&^allowedMode(ref.Mode) != 0 {
|
||||
return "", Err(ref, f.Name(), ReasonInsecureMode)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", Err(ref, f.Name(), ReasonUnreadableSource)
|
||||
}
|
||||
kv, err := parseEnvFile(data)
|
||||
if err != nil {
|
||||
return "", Err(ref, f.Name(), ReasonMalformedSource)
|
||||
}
|
||||
v, ok := kv[ref.Key]
|
||||
if !ok {
|
||||
return "", Err(ref, f.Name(), ReasonMissingKey)
|
||||
}
|
||||
if v == "" {
|
||||
return "", Err(ref, f.Name(), ReasonEmptyValue)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// allowedMode parses ref.Mode ("0600"-style octal, default 0600) into a
|
||||
// permission mask; unparsable masks fail closed to 0 (nothing allowed).
|
||||
func allowedMode(mode string) os.FileMode {
|
||||
if mode == "" {
|
||||
return 0o600
|
||||
}
|
||||
m, err := strconv.ParseUint(strings.TrimPrefix(mode, "0o"), 8, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return os.FileMode(m) & os.ModePerm
|
||||
}
|
||||
|
||||
// expandHome resolves a leading ~ / ~/ to the user's home directory;
|
||||
// every other path is returned unchanged.
|
||||
func expandHome(path string) string {
|
||||
if path == "~" || strings.HasPrefix(path, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
if path == "~" {
|
||||
return home
|
||||
}
|
||||
return home + path[1:]
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user