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:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
@@ -135,6 +136,25 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer"))
|
||||
}},
|
||||
{Name: grants.ToolAcceptHashes, Description: "Release the hold of a user list that changes several loaded users' hashes at " +
|
||||
"once (novox/hq ADR 0561 §7; the condition mesh-grants.user-list-held names them), by a person's act with why: names " +
|
||||
"exactly the users held, bound to the held list's digest, so the part's process writes that list once at its next pass " +
|
||||
"and says hashes-accepted. It never releases another hold (a failed read, a user dropped for want of a hash, a lost " +
|
||||
"right), and a list composed otherwise since is held again for a new acceptance.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"users": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the users held, as the condition names them"},
|
||||
"why": map[string]any{"type": "string", "description": "why their hashes change"}}, "required": []string{"users", "why"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
var users []string
|
||||
if list, ok := a["users"].([]any); ok {
|
||||
for _, u := range list {
|
||||
if s, ok := u.(string); ok {
|
||||
users = append(users, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why"))
|
||||
}},
|
||||
{Name: grants.ToolTakeOverState, Description: "Where the switch stands: the list the bus node follows, the ask open now, the request, and " +
|
||||
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
||||
Input: schema(nil),
|
||||
|
||||
@@ -2096,9 +2096,40 @@ func TestGrants(t *testing.T) {
|
||||
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 hold is its own guard, beside take-over's refusals (ADR 0562): the switch stays taken over.
|
||||
if p.SwitchState() != grants.TookOver {
|
||||
t.Errorf("the hold moved the switch to %s", p.SwitchState())
|
||||
}
|
||||
// A person accepts exactly the named users' changes of the held list, with why: refused for another set, with no
|
||||
// why, or once the list changed since; accepted, the next pass writes it once, said as a hand act.
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x"}, "rotated by hand"); err == nil || !strings.Contains(err.Error(), "a.mod-x, a.nats") {
|
||||
t.Errorf("an acceptance of another set than the held one: %v", err)
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, ""); err == nil {
|
||||
t.Error("an acceptance with no why")
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.nats", "a.mod-x"}, "the vault's store was restored"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The list changed since the acceptance (a.mod-x's hash again): the acceptance is bound to the held list, refused.
|
||||
hash("a.mod-x", "$2a$10$fourth", 4, false)
|
||||
res = p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "held") {
|
||||
t.Fatalf("an acceptance of a list that changed since released it: %+v", res.Sealed)
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, "the vault's store was restored"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.events = nil
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared ||
|
||||
!slices.Contains(b.events, grants.EventHashesAccepted) {
|
||||
t.Fatalf("an accepted hold was not released once and said: %+v, events %v", said, b.events)
|
||||
}
|
||||
inForce, _, _ = p.Sealed()
|
||||
// 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)
|
||||
hash("a.mod-x", "$2a$10$fifth", 5, true)
|
||||
hash("a.nats", "$2a$10$fifth", 3, 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)
|
||||
|
||||
@@ -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 {
|
||||
@@ -166,6 +166,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 {
|
||||
@@ -342,7 +346,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, "; ") + ")"
|
||||
}
|
||||
@@ -378,9 +383,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"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
],
|
||||
"emits": [
|
||||
"asserted",
|
||||
"hashes-accepted",
|
||||
"membership-changed",
|
||||
"user-list-changed"
|
||||
],
|
||||
@@ -44,7 +45,8 @@
|
||||
"grants_take_over_state",
|
||||
"grants_hand_back",
|
||||
"grants_accounts",
|
||||
"grants_consumer_match"
|
||||
"grants_consumer_match",
|
||||
"grants_accept_hashes"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user