Answer a move the vault refuses for holding another value with fields, not words alone (issue 774)
move-in's refusal "the vault already holds another value for ..." was an error's text, which the node-engine and the
controller could only quote. It is now an answer: `refused` with the reason holds-another-value and the held value's
record (generation, changed by, changed at, origin; never the value), `why` in words, and no read-back, so an
engine that does not read `refused` still fails as before ("answered with no read-back"). keepLocked's refusal is a
typed error (HeldAnother) whose text is unchanged for every other path.
Quick fix: a stable prefix in the text. Thorough fix: the fields. Chose thorough, as asked; mesh-host's
moveIntoTheVault has to read `refused` to use them.
This commit is contained in:
@@ -74,8 +74,22 @@ type MovedIn struct {
|
||||
// ReadBack is the value the vault's store holds now, sealed to the key the second step sent (NaCl anonymous
|
||||
// box, base64).
|
||||
ReadBack string `json:"read_back"`
|
||||
// Refused is set, with no read-back, when nothing was taken because the vault holds another value for the name: its
|
||||
// reason ("holds-another-value") and the held value's record, never the value (the sweep of 2026-10-30, issue 774:
|
||||
// the refusal was words alone). Why is the same in words.
|
||||
Refused *MoveRefused `json:"refused,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// MoveRefused is why a move took nothing, as fields.
|
||||
type MoveRefused struct {
|
||||
Reason string `json:"reason"`
|
||||
Held *HeldAnother `json:"held,omitempty"`
|
||||
}
|
||||
|
||||
// RefusedHeldAnother is MoveRefused's reason when the vault holds another value for the name.
|
||||
const RefusedHeldAnother = "holds-another-value"
|
||||
|
||||
// PairSecret is the vault's name for one row of the controller's pair credentials.
|
||||
func PairSecret(provider, provision, consumer, consumerModule, local string) (string, error) {
|
||||
parts := map[string]string{"the provider's machine": provider, "the provision": provision,
|
||||
@@ -213,6 +227,14 @@ func (v *Vault) MoveTake(caller, call, sealed, readBackTo string) (MovedIn, erro
|
||||
m := c.move
|
||||
changed, err := v.keepWith(c.secret, string(value), caller, "", movedOrigins[m.Origin], keepOptions{
|
||||
neverReplace: true, madeAt: m.MadeAt, movedFrom: &MovedFrom{By: caller, Generation: m.Generation}})
|
||||
var another *HeldAnother
|
||||
if errors.As(err, &another) {
|
||||
// Answered, not failed: the caller reads the reason and the held record. A caller that reads no `refused` finds
|
||||
// no read-back and fails as loudly as before.
|
||||
v.log("did not move in %s from %s: %v", c.secret, caller, err)
|
||||
return MovedIn{Name: c.secret, Generation: another.Generation,
|
||||
Refused: &MoveRefused{Reason: RefusedHeldAnother, Held: another}, Why: err.Error()}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return MovedIn{}, err
|
||||
}
|
||||
|
||||
@@ -198,9 +198,13 @@ func TestTheMoveNeverReplacesAValueTheVaultHolds(t *testing.T) {
|
||||
if _, err := v.Rotate(ControllerCaller, "pair/anchor/database/shanks/gitea", "jochen", "a test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := moveIn(t, v, "node.anchor", pair, "the-pair-value"); err == nil ||
|
||||
!strings.Contains(err.Error(), "never replaces") {
|
||||
t.Fatalf("a move over a value the vault changed since: %v", err)
|
||||
// Refused as fields, not words alone (the sweep of 2026-10-30, issue 774): the reason and the held record, no
|
||||
// read-back, so a caller that reads no `refused` still fails.
|
||||
refused, err := moveIn(t, v, "node.anchor", pair, "the-pair-value")
|
||||
if err != nil || refused.Refused == nil || refused.Refused.Reason != RefusedHeldAnother || refused.ReadBack != "" ||
|
||||
refused.Refused.Held == nil || refused.Refused.Held.Generation != 2 || refused.Refused.Held.Origin != OriginMade ||
|
||||
!strings.Contains(refused.Why, "never replaces") {
|
||||
t.Fatalf("a move over a value the vault changed since: %+v, %v", refused, err)
|
||||
}
|
||||
rec, _, _ := v.Store.Read("pair/anchor/database/shanks/gitea")
|
||||
if rec.Generation != 2 || rec.Origin != OriginMade {
|
||||
|
||||
@@ -147,6 +147,22 @@ func (v *Vault) keepWith(name, value, by, via, origin string, opt keepOptions) (
|
||||
return c, err
|
||||
}
|
||||
|
||||
// HeldAnother is the refusal of a value the vault never replaces: it holds another value for the name. Its fields are the
|
||||
// held value's record, never the value, so a caller reads them rather than the words.
|
||||
type HeldAnother struct {
|
||||
Name string `json:"name"`
|
||||
Generation int `json:"generation"`
|
||||
ChangedBy string `json:"changed_by"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
Origin string `json:"origin,omitempty"`
|
||||
}
|
||||
|
||||
func (h *HeldAnother) Error() string {
|
||||
return fmt.Sprintf("the vault already holds another value for %s (generation %d, changed by %s at %s), and the move "+
|
||||
"never replaces a value the vault holds: nothing was taken", h.Name, h.Generation, h.ChangedBy,
|
||||
h.ChangedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// BusAccountValue says value is a bus account as secret names it: the controller's or the vault's JSON, with its user
|
||||
// (or one its name gives) and a password (accountOf).
|
||||
func BusAccountValue(secret, value string) bool {
|
||||
@@ -204,9 +220,8 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
|
||||
}
|
||||
}
|
||||
if opt.neverReplace {
|
||||
return Changed{}, fmt.Errorf("the vault already holds another value for %s (generation %d, changed by %s "+
|
||||
"at %s), and the move never replaces a value the vault holds: nothing was taken", name,
|
||||
before.Generation, before.ChangedBy, before.ChangedAt.Format(time.RFC3339))
|
||||
return Changed{}, &HeldAnother{Name: name, Generation: before.Generation, ChangedBy: before.ChangedBy,
|
||||
ChangedAt: before.ChangedAt, Origin: before.Origin}
|
||||
}
|
||||
}
|
||||
atRest, err := seal.Seal(value, v.Own.PublicKey)
|
||||
|
||||
Reference in New Issue
Block a user