mesh-vault: secret_holders lists every record of the vault's store by name and fingerprint (issue 1409)
S1 vault-licence-secrets-listed. secret_holders answers `records` and `recordsCount` beside holders, count and enrolments: each store record's name, generation, since, changed, changedBy/changedVia, origin, length and the vault's own fingerprint of the whole value, plus key and part for licence.<key>.<part>. A record the vault cannot open stays listed with an error. Never a value. Builds: design 39 bus-and-switch §20, §15; design 39 parts §12 (D1); design 24 Every secret lives in the vault, Beyond generate and hold; as-is 06 The vault, as it runs. Lead answers (issue 1409 note 11): V1, V-thorough, V2-V8. V3 drops the map's tokenFingerprint and AC5; V-thorough widens the licence.-prefix list to every record.
This commit is contained in:
@@ -257,7 +257,10 @@ runner's `ledgers` are never retired this way. A waiter on `rotated` or `own-rep
|
||||
|
||||
`secret_export`, `secret_holders`, `secret_verify` answer as the TypeScript vault's did, and its custody
|
||||
ledger (`${dir:ledger}`, the same JSON files) is kept by a pass over the contributions file every minute.
|
||||
`secret_handed` and `secret_vault_export` read the vault's own store. None returns a value.
|
||||
`secret_handed` and `secret_vault_export` read the vault's own store. None returns a value. `secret_holders` also answers
|
||||
`records`: every record the store keeps (a licence's two secrets included), by name, generation, when stored and
|
||||
changed, who changed it, origin, length and fingerprint, never the value; a record the vault cannot open stays in the
|
||||
list with an `error`.
|
||||
|
||||
**The ledger follows ADR 0230.** A contributions file that does not say who is asked for (unreadable, not
|
||||
JSON, no `given` list or a null one, or for another requirement) is not "nobody": the pass records nothing,
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LicencePutVerb is the verb that hands a licence's secret to the vault.
|
||||
@@ -78,3 +80,47 @@ func (v *Vault) LicencePutTake(caller, call, sealed string) (Changed, error) {
|
||||
}
|
||||
return v.keepWith(c.secret, string(value), caller, "", OriginAccepted, keepOptions{generation: c.generation})
|
||||
}
|
||||
|
||||
// RecordHeld is one record of the vault's store as secret_holders lists it (issue 1409, design 39 parts §12): by name
|
||||
// and fingerprint, never the value. For a licence's secret it also says the licence's key and the part.
|
||||
type RecordHeld struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Part string `json:"part,omitempty"`
|
||||
Generation int `json:"generation"`
|
||||
Since time.Time `json:"since"`
|
||||
Changed time.Time `json:"changed"`
|
||||
ChangedBy string `json:"changedBy"`
|
||||
ChangedVia string `json:"changedVia,omitempty"`
|
||||
Origin string `json:"origin"`
|
||||
Length int `json:"length"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
// Error says why the vault could not open its copy; the record stays in the list.
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RecordsHeld lists every record the store keeps, by name, ordered as the store lists them. The fingerprint is the
|
||||
// vault's own of the whole value, as the ledger gives it.
|
||||
func (v *Vault) RecordsHeld() ([]RecordHeld, error) {
|
||||
records, err := v.Store.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]RecordHeld, 0, len(records))
|
||||
for _, r := range records {
|
||||
row := RecordHeld{Name: r.Name, Generation: r.Generation, Since: r.MadeAt, Changed: r.ChangedAt,
|
||||
ChangedBy: r.ChangedBy, ChangedVia: r.ChangedVia, Origin: r.Origin}
|
||||
if rest, ok := strings.CutPrefix(r.Name, "licence."); ok {
|
||||
if i := strings.LastIndex(rest, "."); i > 0 {
|
||||
row.Key, row.Part = rest[:i], rest[i+1:]
|
||||
}
|
||||
}
|
||||
if value, err := openAtRest(r, v); err != nil {
|
||||
row.Error = "the vault could not open its copy: " + err.Error()
|
||||
} else {
|
||||
row.Length, row.Fingerprint = len(value), Fingerprint(value)
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// putLicence hands one part of a licence to the vault in licence-put's two steps.
|
||||
@@ -114,3 +117,54 @@ func TestATokenIsGivenOnlyToAMachineABindingNames(t *testing.T) {
|
||||
t.Fatalf("the manager's grant: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// issue 1409, design 39 bus-and-switch §20, parts §12: secret_holders lists every record of the store by name and
|
||||
// fingerprint, a licence's two secrets with their key and part, a record it cannot open with an error, and no value.
|
||||
func TestSecretHoldersListsEveryStoreRecordByNameAndFingerprintNeverTheValue(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
const manager = "claude-licence-manager"
|
||||
grant, access := `{"refreshToken":"rt-SECRET-grant"}`, `{"accessToken":"at-SECRET-access"}`
|
||||
for part, value := range map[string]string{LicenceGrant: grant, LicenceAccess: access} {
|
||||
if _, err := putLicence(t, v, manager, "jane_at_example.org", part, 3, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := v.Put("pair/postgres/gitea", "other-SECRET-value", "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
broken, _, _ := v.Store.Read("pair/postgres/gitea")
|
||||
broken.Name, broken.AtRest = "pair/broken/one", seal.Box{}
|
||||
if err := v.Store.Write(broken); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_holders").Run(map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(got)
|
||||
if strings.Contains(string(raw), "SECRET") {
|
||||
t.Fatalf("the answer holds a value: %s", raw)
|
||||
}
|
||||
m := got.(map[string]any)
|
||||
rows := map[string]RecordHeld{}
|
||||
for _, r := range m["records"].([]RecordHeld) {
|
||||
rows[r.Name] = r
|
||||
}
|
||||
if m["recordsCount"] != 4 || len(rows) != 4 {
|
||||
t.Fatalf("records %v, want 4 (two licence secrets, a pair, a broken one)", rows)
|
||||
}
|
||||
for part, value := range map[string]string{LicenceGrant: grant, LicenceAccess: access} {
|
||||
r := rows["licence.jane_at_example.org."+part]
|
||||
if r.Key != "jane_at_example.org" || r.Part != part || r.Generation != 3 || r.ChangedBy != manager ||
|
||||
r.Origin != OriginAccepted || r.Length != len(value) || r.Fingerprint != Fingerprint(value) || r.Error != "" {
|
||||
t.Errorf("the %s secret's row is %+v", part, r)
|
||||
}
|
||||
}
|
||||
if r := rows["pair/postgres/gitea"]; r.Key != "" || r.Fingerprint != Fingerprint("other-SECRET-value") {
|
||||
t.Errorf("the pair's row is %+v", r)
|
||||
}
|
||||
if r := rows["pair/broken/one"]; r.Error == "" || r.Fingerprint != "" {
|
||||
t.Errorf("a record the vault cannot open must stay listed with an error: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,15 +380,32 @@ func definitions(env *Env) []toolDef {
|
||||
{Name: "secret_holders",
|
||||
Description: "Who holds a secret from this vault: each consumer's login, node and module, when it was " +
|
||||
"granted, how many times it has been rotated and when, and the fingerprint of the current value. " +
|
||||
"Also every record the vault's own store keeps (`records`, with `recordsCount`): its name, generation, when " +
|
||||
"it was stored and last changed, who changed it, its origin, its length and its fingerprint, and for a " +
|
||||
"licence's secret (licence.<key>.grant, licence.<key>.access) the key and the part. " +
|
||||
"Fingerprints only — the value is never returned.",
|
||||
run: func(string, map[string]any) (any, error) {
|
||||
enrolments := []EnrolmentHeld{}
|
||||
records := []RecordHeld{}
|
||||
recordsError := ""
|
||||
if env.Vault != nil {
|
||||
enrolments, _ = env.Vault.EnrolmentsHeld()
|
||||
var err error
|
||||
if records, err = env.Vault.RecordsHeld(); err != nil {
|
||||
recordsError = "the vault could not list its store: " + err.Error()
|
||||
records = []RecordHeld{}
|
||||
}
|
||||
}
|
||||
stored := func(m map[string]any) map[string]any {
|
||||
m["records"], m["recordsCount"] = records, len(records)
|
||||
if recordsError != "" {
|
||||
m["recordsError"] = recordsError
|
||||
}
|
||||
return m
|
||||
}
|
||||
if env.Ledger == nil {
|
||||
return map[string]any{"holders": []any{}, "count": 0, "enrolments": enrolments,
|
||||
"error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}, nil
|
||||
return stored(map[string]any{"holders": []any{}, "count": 0, "enrolments": enrolments,
|
||||
"error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}), nil
|
||||
}
|
||||
var asked []Contribution
|
||||
if env.Receives != "" {
|
||||
@@ -407,7 +424,7 @@ func definitions(env *Env) []toolDef {
|
||||
}
|
||||
holders = append(holders, row)
|
||||
}
|
||||
return map[string]any{"holders": holders, "count": len(holders), "enrolments": enrolments}, nil
|
||||
return stored(map[string]any{"holders": holders, "count": len(holders), "enrolments": enrolments}), nil
|
||||
}},
|
||||
{Name: "secret_verify",
|
||||
Description: "Check one holder's secret without seeing it: the fingerprint the vault recorded against the " +
|
||||
|
||||
Reference in New Issue
Block a user