agent/secrets: separate admin slot with backwards-compatible decode
Split the on-disk bundle into repo + admin slots. Legacy flat Repo blobs are detected at load time by the presence of "repo_url" at the top level and transparently promoted into the new shape on the next Save/SaveAdmin. Adds ErrNoAdmin sentinel, LoadAdmin, SaveAdmin, and three new tests.
This commit is contained in:
@@ -2,6 +2,8 @@ package secrets
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -97,3 +99,139 @@ func TestSaveIsAtomic(t *testing.T) {
|
||||
t.Errorf("dir should hold one file post-save, got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsLoadAdminEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
// No file yet: LoadAdmin must return ErrNoAdmin, not a hard error.
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "secrets.enc")
|
||||
st, err := New(path, freshKey(t))
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
_, err = st.LoadAdmin()
|
||||
if !errors.Is(err, ErrNoAdmin) {
|
||||
t.Errorf("expected ErrNoAdmin, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsAdminSlotIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "secrets.enc")
|
||||
st, err := New(path, freshKey(t))
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
|
||||
repo := Repo{URL: "rest:https://repo/host", Username: "user", Password: "pw"}
|
||||
admin := Repo{URL: "rest:https://repo/host", Username: "admin", Password: "adminpw"}
|
||||
|
||||
if err := st.Save(repo); err != nil {
|
||||
t.Fatalf("save repo: %v", err)
|
||||
}
|
||||
if err := st.SaveAdmin(admin); err != nil {
|
||||
t.Fatalf("save admin: %v", err)
|
||||
}
|
||||
|
||||
// Load returns the repo slot unchanged.
|
||||
gotRepo, err := st.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if gotRepo != repo {
|
||||
t.Errorf("repo slot mismatch: got %+v want %+v", gotRepo, repo)
|
||||
}
|
||||
|
||||
// LoadAdmin returns the admin slot.
|
||||
gotAdmin, err := st.LoadAdmin()
|
||||
if err != nil {
|
||||
t.Fatalf("load admin: %v", err)
|
||||
}
|
||||
if gotAdmin != admin {
|
||||
t.Errorf("admin slot mismatch: got %+v want %+v", gotAdmin, admin)
|
||||
}
|
||||
|
||||
// SaveAdmin a second time replaces admin only; repo unchanged.
|
||||
admin2 := Repo{URL: "rest:https://repo/host", Username: "admin2", Password: "pw2"}
|
||||
if err := st.SaveAdmin(admin2); err != nil {
|
||||
t.Fatalf("save admin2: %v", err)
|
||||
}
|
||||
gotRepo2, err := st.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load after admin2 save: %v", err)
|
||||
}
|
||||
if gotRepo2 != repo {
|
||||
t.Errorf("repo slot changed unexpectedly: got %+v want %+v", gotRepo2, repo)
|
||||
}
|
||||
gotAdmin2, err := st.LoadAdmin()
|
||||
if err != nil {
|
||||
t.Fatalf("load admin2: %v", err)
|
||||
}
|
||||
if gotAdmin2 != admin2 {
|
||||
t.Errorf("admin2 slot mismatch: got %+v want %+v", gotAdmin2, admin2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsLegacyFlatBlobMigrates(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "secrets.enc")
|
||||
key := freshKey(t)
|
||||
|
||||
// Write a legacy flat Repo blob directly — bypassing bundle wrapping.
|
||||
legacy := Repo{URL: "rest:https://legacy/host", Username: "legacyuser", Password: "legacypw"}
|
||||
plain, err := json.Marshal(legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal legacy: %v", err)
|
||||
}
|
||||
a, err := crypto.NewAEAD(key)
|
||||
if err != nil {
|
||||
t.Fatalf("aead: %v", err)
|
||||
}
|
||||
ct, err := a.Encrypt(plain, []byte(additionalData))
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt legacy: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(ct), 0o600); err != nil {
|
||||
t.Fatalf("write legacy file: %v", err)
|
||||
}
|
||||
|
||||
// Open via secrets.New + Load — must return the legacy Repo.
|
||||
st, err := New(path, key)
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
got, err := st.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load legacy: %v", err)
|
||||
}
|
||||
if got != legacy {
|
||||
t.Errorf("legacy decode mismatch: got %+v want %+v", got, legacy)
|
||||
}
|
||||
|
||||
// SaveAdmin should write both slots; re-opening must have both.
|
||||
admin := Repo{URL: "rest:https://legacy/host", Username: "admin", Password: "adminpw"}
|
||||
if err := st.SaveAdmin(admin); err != nil {
|
||||
t.Fatalf("save admin after legacy: %v", err)
|
||||
}
|
||||
|
||||
st2, err := New(path, key)
|
||||
if err != nil {
|
||||
t.Fatalf("reopen: %v", err)
|
||||
}
|
||||
gotRepo, err := st2.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load repo after migration: %v", err)
|
||||
}
|
||||
if gotRepo != legacy {
|
||||
t.Errorf("repo after migration: got %+v want %+v", gotRepo, legacy)
|
||||
}
|
||||
gotAdmin, err := st2.LoadAdmin()
|
||||
if err != nil {
|
||||
t.Fatalf("load admin after migration: %v", err)
|
||||
}
|
||||
if gotAdmin != admin {
|
||||
t.Errorf("admin after migration: got %+v want %+v", gotAdmin, admin)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user