mesh/delivery merged: source.main moved to novox/mesh-delivery@1d416827157d
mesh/check being carried to ce600df98afc on 1d416827157d
332 lines
13 KiB
Go
332 lines
13 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **The list the bus node loads from take-over** (novox/hq ADR 0547 clause 7, issue 1945). Each pass composes the bus's
|
|
// user list with the vault's hashes (the accounts block, buscompose.ComposeAccounts, as the bus module's file holds it),
|
|
// seals it to the bus node's key (the key the node says in its have of the bus module) and puts it as one object of the
|
|
// part's object bucket, named by its digest; then writes the key `user-list.sealed` naming it, with the revision, the user
|
|
// count, the users left out for want of a hash, and which list the bus node follows (the switch, clause 8). The object
|
|
// and the key are written in shadow too: the bus node reads the key and follows the controller's list until the key says
|
|
// FollowGrants, so a take-over has a list to load at once. Only the process writes them (Objects); a tool asks it.
|
|
|
|
// KeySealed is the key of the state user-list the bus node reads (delivery.GrantsUserListKey).
|
|
const KeySealed = delivery.GrantsUserListKey
|
|
|
|
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
|
|
type Objects interface {
|
|
PutObject(name string, data []byte) error
|
|
DeleteObject(name string) error
|
|
}
|
|
|
|
// ComposeList is the bus's user list as the file holds it, with each user's hash from the vault, and the users left out
|
|
// for want of one. Pure.
|
|
func ComposeList(r buscompose.Records, hashes map[string]HeldHash) ([]byte, int, []string, error) {
|
|
principals, err := buscompose.Users(r)
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
held := map[string]string{}
|
|
for user, h := range hashes {
|
|
held[user] = h.Hash
|
|
}
|
|
filled, missing := buscompose.WithPasswords(principals, held)
|
|
list, err := buscompose.ComposeAccounts(filled)
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
slices.Sort(missing)
|
|
return []byte(list), len(filled), missing, nil
|
|
}
|
|
|
|
// Sealed is the part's key `user-list.sealed` as written, and whether there is one.
|
|
func (p *Part) Sealed() (delivery.GrantsUserList, uint64, bool) {
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeySealed)
|
|
if err != nil || !found {
|
|
return delivery.GrantsUserList{}, 0, false
|
|
}
|
|
g, err := delivery.DecodeGrantsUserList(raw)
|
|
if err != nil {
|
|
return delivery.GrantsUserList{}, 0, false
|
|
}
|
|
return g, rev, true
|
|
}
|
|
|
|
// followOf is which list the switch says the bus node follows.
|
|
func followOf(state string) string {
|
|
if state == TookOver {
|
|
return delivery.FollowGrants
|
|
}
|
|
return delivery.FollowController
|
|
}
|
|
|
|
// writeSealed puts the list sealed to the bus node and writes the key, each only when what it says changed. It says in
|
|
// res what it wrote, or why it wrote nothing.
|
|
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string, in Inputs, bus []string) {
|
|
cur, _, had := p.Sealed()
|
|
next := cur
|
|
next.Follow = followOf(p.SwitchState())
|
|
key := ""
|
|
if in.Loaded != nil {
|
|
key = in.Loaded.Key
|
|
}
|
|
digest := delivery.UserListDigest(list)
|
|
why, held := "", ""
|
|
switch {
|
|
case p.Objects == nil:
|
|
// The tools: the process writes; the follow alone may change here, from a tool's view of the switch, and is not.
|
|
res.Sealed = sealedSays(cur, had, "")
|
|
return
|
|
case in.BusNode == "":
|
|
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) != "":
|
|
// 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)
|
|
held = why
|
|
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
|
object, err := delivery.SealUserList(list, key)
|
|
if err == nil {
|
|
err = p.Objects.PutObject(delivery.UserListObjectName(digest), object)
|
|
}
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, "the sealed user list could not be put: "+err.Error())
|
|
res.Sealed = sealedSays(cur, had, "")
|
|
return
|
|
}
|
|
if cur.Digest != digest {
|
|
next.Revision++
|
|
}
|
|
next.Digest, next.Object, next.SealedTo, next.Node = digest, delivery.UserListObjectName(digest), delivery.SealingKeyID(key), in.BusNode
|
|
}
|
|
p.sayHeld(held)
|
|
if next.Digest == digest {
|
|
next.Users, next.LeftOut = users, leftOut
|
|
}
|
|
if next.Digest == "" {
|
|
next.Users, next.LeftOut = 0, nil
|
|
}
|
|
if had && sameSealed(cur, next) {
|
|
res.Sealed = sealedSays(cur, had, why)
|
|
return
|
|
}
|
|
next.At = p.now()
|
|
data, err := delivery.EncodeGrantsUserList(next)
|
|
if err == nil {
|
|
_, err = p.Store.Put(StateUserList, KeySealed, data)
|
|
}
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeySealed, err))
|
|
res.Sealed = sealedSays(cur, had, "")
|
|
return
|
|
}
|
|
if next.Digest == digest && cur.Digest != digest {
|
|
p.keepWrittenUsers(res, list)
|
|
}
|
|
// 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 {
|
|
if err := p.Objects.DeleteObject(cur.Object); err != nil {
|
|
res.Errors = append(res.Errors, "the sealed list before could not be taken away: "+err.Error())
|
|
}
|
|
}
|
|
res.Sealed = sealedSays(next, true, why)
|
|
p.say(res, EventUserList, map[string]any{"digest": next.Digest, "revision": next.Revision, "users": next.Users,
|
|
"left_out": len(next.LeftOut), "follow": next.Follow, "sealed_to": next.Node})
|
|
}
|
|
|
|
// sayHeld raises the warning condition mesh-grants.user-list-held while the list in force is kept (why, which words the pass's
|
|
// `held: ...`), and clears it on the first pass that holds nothing back: the list advanced, or the reason went. The refusal
|
|
// condition's way (sayRefused): said once per reason, again when the reason changes, and tried again when a say failed.
|
|
func (p *Part) sayHeld(why string) {
|
|
if p.Say == nil {
|
|
return
|
|
}
|
|
p.holdMu.Lock()
|
|
defer p.holdMu.Unlock()
|
|
if p.heldKnown && why == p.heldSaid {
|
|
return
|
|
}
|
|
c := asks.Condition{Key: HeldCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
|
if why == "" {
|
|
c.State = asks.StateCleared
|
|
} else {
|
|
reason := strings.TrimPrefix(why, "held: ")
|
|
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
|
c.Summary, c.Headline, c.Needs = "the user list is held: "+reason, "The user list in force is held back", "a look at why"
|
|
c.Explanation = asks.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason
|
|
c.Resolved = "a pass finds nothing to hold back and the list advances"
|
|
}
|
|
if p.Say(c) == nil {
|
|
p.heldSaid, p.heldKnown = why, true
|
|
}
|
|
}
|
|
|
|
// HeldCondition is the key of the warning raised while a list in force is held back after take-over.
|
|
const HeldCondition = "mesh-grants.user-list-held"
|
|
|
|
func sameSealed(a, b delivery.GrantsUserList) bool {
|
|
a.At, b.At = b.At, a.At
|
|
x, _ := json.Marshal(a)
|
|
y, _ := json.Marshal(b)
|
|
return string(x) == string(y)
|
|
}
|
|
|
|
// SealedState is what a pass says of the sealed list: the key as written, and why nothing was written when nothing was.
|
|
type SealedState struct {
|
|
Written *delivery.GrantsUserList `json:"written,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
func sealedSays(g delivery.GrantsUserList, had bool, why string) *SealedState {
|
|
s := &SealedState{Why: why}
|
|
if had {
|
|
s.Written = &g
|
|
}
|
|
return s
|
|
}
|
|
|
|
// syncFollow writes the switch into the key at once, so the bus node turns on the operator's yes, not at the next pass.
|
|
func (p *Part) syncFollow(state string) error {
|
|
cur, _, had := p.Sealed()
|
|
if had && cur.Follow == followOf(state) {
|
|
return nil
|
|
}
|
|
if !had {
|
|
cur = delivery.GrantsUserList{}
|
|
}
|
|
cur.Follow, cur.At = followOf(state), p.now()
|
|
data, err := delivery.EncodeGrantsUserList(cur)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = p.Store.Put(StateUserList, KeySealed, data)
|
|
return err
|
|
}
|
|
|
|
// Effect is where the switch stands on the bus node: "switched" only once the bus node's loaded list is the grants part's
|
|
// written one (its source the part's key, its digest the written digest); otherwise what it waits for.
|
|
func (p *Part) Effect(state string, loaded *delivery.UserListWritten) string {
|
|
g, _, had := p.Sealed()
|
|
fromGrants := loaded != nil && loaded.Source == delivery.GrantsUserListSource
|
|
switch {
|
|
case state == TookOver && had && fromGrants && g.Digest != "" && loaded.Digest == g.Digest && loaded.Error == "":
|
|
return "switched: the bus node loaded the grants part's list " + short(g.Digest) + " (revision " + fmt.Sprint(g.Revision) + ")"
|
|
case state == TookOver && !had || state == TookOver && g.Digest == "":
|
|
return "recorded, not in effect: the grants part has written no sealed list for the bus node yet"
|
|
case state == TookOver && loaded == nil:
|
|
return "recorded, not in effect: the bus module's have names no loaded list"
|
|
case state == TookOver && loaded.Unwatched != "" && !fromGrants:
|
|
return "recorded, not in effect: the bus node does not watch the grants part's key: " + loaded.Unwatched
|
|
case state == TookOver && loaded.Error != "":
|
|
return "recorded, not in effect: the bus node could not load the grants part's list: " + loaded.Error
|
|
case state == TookOver:
|
|
return "recorded, not in effect yet: the bus node loaded " + short(loaded.Digest) + " from " + loaded.Source + ", the grants part wrote " + short(g.Digest)
|
|
case fromGrants:
|
|
return "handed back, not in effect yet: the bus node still follows the grants part's list"
|
|
}
|
|
return "shadow: the bus node follows the controller's list"
|
|
}
|
|
|
|
func short(d string) string {
|
|
d = strings.TrimPrefix(d, "sha256:")
|
|
if len(d) > 12 {
|
|
d = d[:12]
|
|
}
|
|
return d
|
|
}
|
|
|
|
// followReasons is what a take-over waits for on the sealed list itself: a list written for the bus node, of the list
|
|
// composed now, and the loaded list granting the bus node its read (the controller's composer carries ADR 0547 clause 7's
|
|
// grants from the operator's second exception; until its delivery the bus node could not read the first list).
|
|
func (p *Part) followReasons(res Result, in Inputs) []string {
|
|
var out []string
|
|
g, _, had := p.Sealed()
|
|
switch {
|
|
case !had || g.Digest == "":
|
|
why := "no list is written"
|
|
if res.Sealed != nil && res.Sealed.Why != "" {
|
|
why = res.Sealed.Why
|
|
}
|
|
out = append(out, "the grants part has written no sealed list for the bus node: "+why)
|
|
case in.Loaded == nil || in.Loaded.Key == "" || g.SealedTo != delivery.SealingKeyID(in.Loaded.Key):
|
|
out = append(out, "the sealed list is not sealed to the key the bus node says now")
|
|
}
|
|
if in.BusNode != "" {
|
|
node := "node." + in.BusNode
|
|
for _, d := range res.UserList.Compare.Differ {
|
|
if d.User == node && slices.ContainsFunc(d.PublishAdded, func(s string) bool { return strings.Contains(s, delivery.GrantsUserListBucket) }) {
|
|
out = append(out, "the loaded list does not grant "+node+" reading the grants part's sealed list (ADR 0547 clause 7): "+
|
|
"the controller's composer carries it from its delivery of the operator's second exception")
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// KeyWrittenUsers is the key of the state user-list naming the users of the list the key `sealed` names: what a list
|
|
// composed after take-over must keep.
|
|
const KeyWrittenUsers = "sealed-users"
|
|
|
|
func (p *Part) keepWrittenUsers(res *Result, list []byte) {
|
|
data, _ := json.Marshal(listUsers(list))
|
|
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))
|
|
}
|
|
}
|
|
|
|
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
|
|
|
// listUsers names the users of a list as the composer writes it.
|
|
func listUsers(list []byte) []string {
|
|
out := []string{}
|
|
for _, m := range listUser.FindAllSubmatch(list, -1) {
|
|
out = append(out, string(m[1]))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// holdsBack says why a list composed now may not replace the one in force after take-over: a read failed this pass, or
|
|
// a user of the list in force is left out for want of a hash. 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 []string) string {
|
|
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, "; ") + ")"
|
|
}
|
|
raw, _, found, err := p.Store.Get(StateUserList, KeyWrittenUsers)
|
|
if err != nil {
|
|
return "held: the users of the list in force cannot be read: " + err.Error()
|
|
}
|
|
var had []string
|
|
switch {
|
|
case found:
|
|
_ = json.Unmarshal(raw, &had)
|
|
case bus == nil:
|
|
return "held: the users of the list in force are not known: the part kept none, and the loaded list could not be read"
|
|
default:
|
|
had = bus
|
|
}
|
|
var lost []string
|
|
for _, u := range leftOut {
|
|
if slices.Contains(had, u) {
|
|
lost = append(lost, u)
|
|
}
|
|
}
|
|
if len(lost) > 0 {
|
|
return "held: the list in force is kept, since " + strings.Join(lost, ", ") + " would be left out for want of a hash in the vault"
|
|
}
|
|
return ""
|
|
}
|