mesh-grants: hold a list that changes several loaded users' hashes at once with no rotation by the vault naming them, and raise user-list-held naming them (issue 774, ADR 0561 §7)
This commit is contained in:
@@ -1883,6 +1883,68 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("once matched: %+v, said %+v", res.Consumers, said)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC13 (novox/hq ADR 0561 §7): after take-over a list in which more than one loaded user's hash changes, with no
|
||||
// rotation by the vault naming each, is held: the list in force stays, user-list-held is raised naming those users. One
|
||||
// user's change, or changes the vault's rotation names, are written.
|
||||
t.Run("a list changing several loaded users' hashes is held unless the vault's rotation names each", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
hash := func(user, h string, generation int, rotating bool) {
|
||||
v := map[string]any{"user": user, "hash": h, "generation": generation, "fingerprint": fmt.Sprintf("sha256:%d", generation)}
|
||||
if rotating {
|
||||
v["rotating"] = map[string]any{"login": user + "--b", "generation": generation, "revision": 7}
|
||||
}
|
||||
b.put(grants.ReadAccounts, user, v)
|
||||
}
|
||||
for _, u := range shadowOf(t, b) {
|
||||
hash(u.User, "$2a$10$first", 1, false)
|
||||
}
|
||||
busKey, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
||||
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
||||
p := partOn(b)
|
||||
p.Objects = &fakeObjects{}
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
||||
sw, _ := json.Marshal(grants.Switch{V: 1, State: grants.TookOver, Acts: []grants.Act{}})
|
||||
if _, err := (ownStore{b}).Put(grants.StateTakeover, grants.KeySwitch, sw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Compose()
|
||||
inForce, _, _ := p.Sealed()
|
||||
if inForce.Digest == "" {
|
||||
t.Fatal("no list written after take-over")
|
||||
}
|
||||
// One user's hash changes: written.
|
||||
hash("a.mod-x", "$2a$10$second", 2, false)
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest {
|
||||
t.Fatal("a list changing one loaded user's hash was not written")
|
||||
}
|
||||
inForce, _, _ = p.Sealed()
|
||||
// Two change at once, no rotation names them: held, said naming both.
|
||||
hash("a.mod-x", "$2a$10$third", 3, false)
|
||||
hash("a.nats", "$2a$10$third", 2, false)
|
||||
said = nil
|
||||
res := p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x, a.nats") {
|
||||
t.Fatalf("a list changing two loaded users' hashes: %+v", res.Sealed)
|
||||
}
|
||||
if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "a.mod-x, a.nats") {
|
||||
t.Errorf("the hold was said as %+v", said)
|
||||
}
|
||||
// The vault's rotation names each: written, the warning cleared.
|
||||
hash("a.mod-x", "$2a$10$third", 3, true)
|
||||
hash("a.nats", "$2a$10$third", 2, true)
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared {
|
||||
t.Errorf("changes the vault's rotation names were held: %+v", said)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type fakeAsker struct {
|
||||
|
||||
@@ -139,6 +139,22 @@ type HeldHash struct {
|
||||
Generation int `json:"generation,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// Fingerprint is the vault's fingerprint of the value the hash is of: with the generation, what says the password
|
||||
// changed, never the hash itself (a hash of the same password differs by its salt).
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
// Rotating is the vault's rotation of the account under way: a change of its hash it announces (ADR 0561 §4, §7).
|
||||
Rotating *struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"rotating,omitempty"`
|
||||
}
|
||||
|
||||
// credentialOf is what says which password a held hash is of, never the hash: adopted, or the vault's generation and the
|
||||
// fingerprint of its value.
|
||||
func credentialOf(h HeldHash) string {
|
||||
if h.Adopted {
|
||||
return "adopted"
|
||||
}
|
||||
return fmt.Sprintf("generation %d %s", h.Generation, h.Fingerprint)
|
||||
}
|
||||
|
||||
// readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held.
|
||||
|
||||
@@ -133,7 +133,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
return
|
||||
}
|
||||
if next.Digest == digest && cur.Digest != digest {
|
||||
p.keepWrittenUsers(res, list)
|
||||
p.keepWrittenUsers(res, list, in.Hashes)
|
||||
}
|
||||
// The object the key named before is taken away once the key names the new one: the bucket keeps the list in force.
|
||||
if had && cur.Object != "" && cur.Object != next.Object {
|
||||
@@ -279,11 +279,50 @@ func (p *Part) followReasons(res Result, in Inputs) []string {
|
||||
// composed after take-over must keep.
|
||||
const KeyWrittenUsers = "sealed-users"
|
||||
|
||||
func (p *Part) keepWrittenUsers(res *Result, list []byte) {
|
||||
data, _ := json.Marshal(listUsers(list))
|
||||
// KeyWrittenCredentials is the key of the state user-list naming, per user of the list in force, which password its hash
|
||||
// is of (credentialOf: adopted, or the vault's generation and fingerprint), never the hash: what tells a list that changes
|
||||
// several loaded users' hashes at once (ADR 0561 §7).
|
||||
const KeyWrittenCredentials = "sealed-credentials"
|
||||
|
||||
func (p *Part) keepWrittenUsers(res *Result, list []byte, hashes map[string]HeldHash) {
|
||||
users := listUsers(list)
|
||||
data, _ := json.Marshal(users)
|
||||
if _, err := p.Store.Put(StateUserList, KeyWrittenUsers, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenUsers, err))
|
||||
}
|
||||
creds := map[string]string{}
|
||||
for _, u := range users {
|
||||
if h, ok := hashes[u]; ok {
|
||||
creds[u] = credentialOf(h)
|
||||
}
|
||||
}
|
||||
data, _ = json.Marshal(creds)
|
||||
if _, err := p.Store.Put(StateUserList, KeyWrittenCredentials, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenCredentials, err))
|
||||
}
|
||||
}
|
||||
|
||||
// changedHashes are the users of the list in force whose hash the list composed now would change with no rotation by the
|
||||
// vault naming them, sorted; none when the part kept no credentials of the list in force (a list written before).
|
||||
func (p *Part) changedHashes(hashes map[string]HeldHash) ([]string, error) {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyWrittenCredentials)
|
||||
if err != nil || !found {
|
||||
return nil, err
|
||||
}
|
||||
var had map[string]string
|
||||
if err := json.Unmarshal(raw, &had); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var changed []string
|
||||
for user, was := range had {
|
||||
h, ok := hashes[user]
|
||||
if !ok || credentialOf(h) == was || h.Rotating != nil {
|
||||
continue
|
||||
}
|
||||
changed = append(changed, user)
|
||||
}
|
||||
slices.Sort(changed)
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
||||
@@ -333,5 +372,15 @@ func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string) string {
|
||||
if len(unhashed) > 0 {
|
||||
return "held: the list in force is kept, since " + strings.Join(unhashed, ", ") + " would be left out for want of a hash in the vault"
|
||||
}
|
||||
// More than one loaded user's hash changing at once, with no rotation by the vault naming each (ADR 0561 §7): a new
|
||||
// writer of hashes must not lock several holders out in one list, as nine seats were cut on 2026-10-27 at 09:21.
|
||||
changed, err := p.changedHashes(in.Hashes)
|
||||
if err != nil {
|
||||
return "held: the credentials of the list in force cannot be read: " + err.Error()
|
||||
}
|
||||
if len(changed) > 1 {
|
||||
return "held: the list in force is kept, since the hashes of " + fmt.Sprint(len(changed)) + " loaded users would change at " +
|
||||
"once with no rotation by the vault naming them: " + strings.Join(changed, ", ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user