mesh-grants: grants_accept_hashes releases the clause 7 hold once, for exactly the held users of the held list, with why, said as hashes-accepted (issue 774, ADR 0561 §7, §9)
This commit is contained in:
@@ -42,7 +42,7 @@ const (
|
||||
)
|
||||
|
||||
// Emits is every event the part says.
|
||||
var Emits = []string{EventAsserted, EventMembership, EventUserList}
|
||||
var Emits = []string{EventAsserted, EventHashesAccepted, EventMembership, EventUserList}
|
||||
|
||||
// Store is the part's own state, by local name: the only thing the part writes.
|
||||
type Store interface {
|
||||
@@ -173,6 +173,8 @@ type Part struct {
|
||||
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
|
||||
differSaid string
|
||||
differKnown bool
|
||||
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
|
||||
hashHold []string
|
||||
}
|
||||
|
||||
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
||||
|
||||
@@ -2,10 +2,12 @@ package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
@@ -91,11 +93,12 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
why = "no node runs the bus's module, so the list is sealed to nobody"
|
||||
case key == "":
|
||||
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
||||
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost) != "":
|
||||
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost, digest) != "":
|
||||
// After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user
|
||||
// of it for want of a hash: that user's module would be locked out of the bus (review of the switch).
|
||||
why = p.holdsBack(in, leftOut, bus, lost)
|
||||
why = p.holdsBack(in, leftOut, bus, lost, digest)
|
||||
held = why
|
||||
p.keepHashHold(res, digest)
|
||||
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||
object, err := delivery.SealUserList(list, key)
|
||||
if err == nil {
|
||||
@@ -134,6 +137,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
}
|
||||
if next.Digest == digest && cur.Digest != digest {
|
||||
p.keepWrittenUsers(res, list, in.Hashes)
|
||||
p.acceptedWritten(res, digest)
|
||||
}
|
||||
// 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 {
|
||||
@@ -351,7 +355,8 @@ func listUsers(list []byte) []string {
|
||||
// loaded list not read holds it too, since that cannot be ruled out). The list in force is the one the part wrote, by the users
|
||||
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
|
||||
// they could not be read). Empty when it may.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string) string {
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string, digest string) string {
|
||||
p.hashHold = nil
|
||||
if f := in.ReadFailures(); len(f) > 0 {
|
||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||
}
|
||||
@@ -387,9 +392,121 @@ func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string) string {
|
||||
if err != nil {
|
||||
return "held: the credentials of the list in force cannot be read: " + err.Error()
|
||||
}
|
||||
if len(changed) > 1 {
|
||||
if len(changed) > 1 && !p.accepted(digest, changed) {
|
||||
p.hashHold = changed
|
||||
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 ""
|
||||
}
|
||||
|
||||
// **A person accepts a held list's hash changes** (ADR 0561 §7, §9; tools-are-how-a-module-is-spoken-to): the hold of
|
||||
// several loaded users' hashes changing at once is released only by grants_accept_hashes, naming exactly the users held,
|
||||
// with why, bound to the held list's digest. The acceptance releases this guard alone, once: the other holds (a failed
|
||||
// read, a user dropped for want of a hash, a lost right) stand whatever it says; and a list composed otherwise since is
|
||||
// held again, for a new acceptance.
|
||||
|
||||
// KeyHashHold names the list held for its hash changes (its digest and the users), and KeyHashAccept a person's acceptance.
|
||||
const (
|
||||
KeyHashHold = "hash-hold"
|
||||
KeyHashAccept = "hash-accept"
|
||||
)
|
||||
|
||||
// EventHashesAccepted is said when a list held for its hash changes is written on a person's acceptance: a hand act.
|
||||
const EventHashesAccepted = "hashes-accepted"
|
||||
|
||||
// HashHold is the value of KeyHashHold, and HashAccept of KeyHashAccept.
|
||||
type HashHold struct {
|
||||
Digest string `json:"digest"`
|
||||
Users []string `json:"users"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
type HashAccept struct {
|
||||
Digest string `json:"digest"`
|
||||
Users []string `json:"users"`
|
||||
By string `json:"by"`
|
||||
Why string `json:"why"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// keepHashHold writes which list is held for its hash changes, when that is the hold.
|
||||
func (p *Part) keepHashHold(res *Result, digest string) {
|
||||
if p.hashHold == nil {
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(HashHold{Digest: digest, Users: p.hashHold, At: p.now()})
|
||||
if raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold); err == nil && found {
|
||||
var was HashHold
|
||||
if json.Unmarshal(raw, &was) == nil && was.Digest == digest && slices.Equal(was.Users, p.hashHold) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := p.Store.Put(StateUserList, KeyHashHold, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyHashHold, err))
|
||||
}
|
||||
}
|
||||
|
||||
// accepted says a person accepted exactly these users' hash changes of the list of this digest.
|
||||
func (p *Part) accepted(digest string, users []string) bool {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
||||
if err != nil || !found {
|
||||
return false
|
||||
}
|
||||
var a HashAccept
|
||||
return json.Unmarshal(raw, &a) == nil && a.Digest == digest && slices.Equal(a.Users, users)
|
||||
}
|
||||
|
||||
// acceptedWritten says, once, that the list a person accepted is written, and forgets the acceptance and the hold.
|
||||
func (p *Part) acceptedWritten(res *Result, digest string) {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
||||
if err != nil || !found {
|
||||
return
|
||||
}
|
||||
var a HashAccept
|
||||
if json.Unmarshal(raw, &a) != nil || a.Digest != digest {
|
||||
return
|
||||
}
|
||||
p.say(res, EventHashesAccepted, map[string]any{"digest": digest, "users": a.Users, "by": a.By, "why": a.Why})
|
||||
for _, k := range []string{KeyHashAccept, KeyHashHold} {
|
||||
if err := p.Store.Delete(StateUserList, k); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be taken away: %v", StateUserList, k, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptHashes is the tool accept-hashes: a person accepts the hash changes of the list held for them, naming exactly its
|
||||
// users, with why. Refused when no list is held for its hashes, or for another set of users. Bound to the held list's
|
||||
// digest: a list composed otherwise since is held again.
|
||||
func (p *Part) AcceptHashes(caller string, users []string, why string) (any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("accept-hashes says why (`why`)")
|
||||
}
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the held list cannot be read: %w", err)
|
||||
}
|
||||
var h HashHold
|
||||
if !found || json.Unmarshal(raw, &h) != nil || h.Digest == "" {
|
||||
return nil, errors.New("no list is held for its users' hashes changing: there is nothing to accept")
|
||||
}
|
||||
named := slices.Sorted(slices.Values(users))
|
||||
if !slices.Equal(named, h.Users) {
|
||||
return nil, fmt.Errorf("the list held (%s) changes the hashes of %s: an acceptance names exactly those, and %s is not that",
|
||||
short(h.Digest), strings.Join(h.Users, ", "), strings.Join(named, ", "))
|
||||
}
|
||||
data, _ := json.Marshal(HashAccept{Digest: h.Digest, Users: h.Users, By: someoneOr(caller), Why: strings.TrimSpace(why), At: p.now()})
|
||||
rev, err := p.Store.Put(StateUserList, KeyHashAccept, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the acceptance could not be written: %w", err)
|
||||
}
|
||||
return map[string]any{"accepted": rev, "digest": h.Digest, "users": h.Users, "says": "the part's process writes the held " +
|
||||
"list at its next pass, once, unless another hold stands or the list composed then is another; it says hashes-accepted"}, nil
|
||||
}
|
||||
|
||||
func someoneOr(caller string) string {
|
||||
if caller == "" {
|
||||
return "a caller the runtime did not prove"
|
||||
}
|
||||
return caller
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ const (
|
||||
ToolHandBack = "grants_hand_back"
|
||||
ToolAccounts = "grants_accounts"
|
||||
ToolConsumerMatch = "grants_consumer_match"
|
||||
ToolAcceptHashes = "grants_accept_hashes"
|
||||
)
|
||||
|
||||
// Tools is every tool, in the manifest's order.
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
||||
ToolAccounts, ToolConsumerMatch}
|
||||
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes}
|
||||
|
||||
// WritesNoGrant is what grants says of the part's own grants in this milestone.
|
||||
const WritesNoGrant = "none: the grants part writes no grants.<module>.<fp> before ADR 0547's third milestone (clause 5); the controller's are beside"
|
||||
|
||||
Reference in New Issue
Block a user