Merges fix/1945-adopt-hashes 1a190090. A new follower (one per link) waits for the grants part's key to catch up before
its first write, so a restart after take-over never writes the controller's list first. A refused or ended watch is
tried again on link.Backoff, the have says why it is not watched, and the key last heard, kept beside the node's state,
holds meanwhile. While the grants part's list is followed, the bus module's composition is applied with the list on the
machine in place of the controller's. The grants list is never handed to the vault. Pins mesh-sdk c2a410bb.
205 lines
7.2 KiB
Go
205 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
"github.com/novox/mesh-host/internal/userlist"
|
|
)
|
|
|
|
// The bridge of novox/hq ADR 0547 §8, wired: the bus node's engine writes the bus's user list alone from the newest
|
|
// composition of the bus module, under the apply's two locks as a filled file is written (filling.go), and says what
|
|
// it wrote in the bus module's have.
|
|
|
|
// userListFollower is the follower of the bus's user list for one link: the node's composition bucket, the keys the
|
|
// agent trusts, the write alone, and the have.
|
|
func userListFollower(opts options, session *link.Session, composedBucket string, trusted func() []ed25519.PublicKey,
|
|
say link.Announce, wrote func()) *userlist.Follower {
|
|
return userlist.New(userlist.Env{
|
|
Node: session.Membership.Node,
|
|
Composed: bucketWatch{session, composedBucket},
|
|
Trusted: trusted,
|
|
Resolve: func(ctx context.Context, raw json.RawMessage) ([]byte, error) {
|
|
return resolveUserList(session.WithLink(ctx), raw)
|
|
},
|
|
OnDisk: userListOnDisk,
|
|
Write: func(ctx context.Context, module string, raw json.RawMessage, body []byte) (string, bool, error) {
|
|
digest, changed, err := writeUserListAlone(session.WithLink(ctx), opts, raw, body, say)
|
|
if changed && wrote != nil {
|
|
wrote()
|
|
}
|
|
return digest, changed, err
|
|
},
|
|
Report: func(ctx context.Context, module string, w userlist.Written) {
|
|
session.NoteUserList(ctx, module, w)
|
|
},
|
|
// From take-over, the grants part's list (ADR 0547 clauses 7, 8): its one key watched, its object read by name and
|
|
// opened with this machine's sealing key, the key's public half said in the have so the grants part seals to it.
|
|
Grants: bucketWatch{session, delivery.GrantsUserListBucket},
|
|
Fetch: func(ctx context.Context, name string) ([]byte, error) {
|
|
return session.GrantsObject(ctx, name, grantsListLimit)
|
|
},
|
|
Open: func(b seal.Box) ([]byte, error) {
|
|
key, err := identity.LoadSealingKey(identity.SealingKeyPath(opts.state))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return key.OpenVaultBox(b)
|
|
},
|
|
Key: func() string {
|
|
key, err := identity.LoadSealingKey(identity.SealingKeyPath(opts.state))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
pem, err := key.PublicPEM()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return pem
|
|
},
|
|
// The grants part's key as last heard, kept beside the node's state, followed while it cannot be watched; its
|
|
// watch tried again as a passing failure of the bus is (ADR 0547 clause 8).
|
|
Kept: func() ([]byte, uint64) { return keptGrantsKey(opts) },
|
|
Keep: func(value []byte, rev uint64) { keepGrantsKey(opts, value, rev, say) },
|
|
Backoff: link.Backoff,
|
|
// The vault's key from its answer to node-key: the loaded list's hashes are handed sealed to it (ADR 0547
|
|
// clause 9).
|
|
VaultKey: vaultKey.get,
|
|
Say: announceOr(say),
|
|
})
|
|
}
|
|
|
|
// grantsListLimit is the largest sealed list the bus node reads: the live list is about a hundred kilobytes.
|
|
const grantsListLimit = 16 << 20
|
|
|
|
// userListFile is a user-list resource as the file it declares.
|
|
func userListFile(raw []byte) (*declaration.File, error) {
|
|
r, err := declaration.ParseResource(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the user list is not a resource: %w", err)
|
|
}
|
|
f, ok := r.(*declaration.File)
|
|
if !ok {
|
|
return nil, fmt.Errorf("the user list is a %s, not a file", r.Kind())
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// resolveUserList is the list a user-list resource holds: its content, or the object its digest names in this node's
|
|
// file bucket, fetched and verified (apply.ResolveFileReferences, ADR 0486 §1).
|
|
func resolveUserList(ctx context.Context, raw []byte) ([]byte, error) {
|
|
f, err := userListFile(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d := &declaration.Declaration{Version: declaration.Version, Resources: []declaration.Resource{f}}
|
|
if err := apply.ResolveFileReferences(ctx, d, link.FilesFrom(ctx)); err != nil {
|
|
return nil, fmt.Errorf("the user list could not be had by its digest: %w", err)
|
|
}
|
|
return []byte(d.Resources[0].(*declaration.File).Content), nil
|
|
}
|
|
|
|
// userListOnDisk is what the user list's file holds on this machine now; nil when there is none.
|
|
func userListOnDisk(raw json.RawMessage) ([]byte, error) {
|
|
f, err := userListFile(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := os.ReadFile(f.Path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return body, err
|
|
}
|
|
|
|
// writeUserListAlone writes one user-list resource alone (apply.WriteAlone) with body as its content, the node's state
|
|
// read and saved under the apply's locks, and answers the digest of the file on the machine after.
|
|
func writeUserListAlone(ctx context.Context, opts options, raw []byte, body []byte, say link.Announce) (string, bool, error) {
|
|
declared, err := userListFile(raw)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
file := *declared
|
|
file.Content, file.Digest, file.Size = string(body), "", 0
|
|
|
|
applying.Lock()
|
|
defer applying.Unlock()
|
|
unlock, err := store.Lock(opts.state, nil)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
defer unlock()
|
|
known, err := store.Load(opts.state)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
built, err := system.For(builtFor)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
out, updated, writeErr := apply.WriteAlone(ctx, built, &file, known, apply.ExecRunner, announceOr(say))
|
|
// Saved whichever way it went, as an apply's: what was written is on the machine either way.
|
|
if err := store.Save(opts.state, updated); err != nil {
|
|
writeErr = errors.Join(writeErr, errors.New("written, and the node's state could not be saved: "+err.Error()))
|
|
}
|
|
digest := ""
|
|
if body, err := os.ReadFile(file.Path); err == nil {
|
|
digest = userlist.Digest(body)
|
|
}
|
|
return digest, writeErr == nil && out.Action != "unchanged", writeErr
|
|
}
|
|
|
|
// grantsKeyPath is where the grants part's user-list key last heard is kept, beside the node's state.
|
|
func grantsKeyPath(opts options) string {
|
|
return filepath.Join(filepath.Dir(opts.state), "grants-user-list.json")
|
|
}
|
|
|
|
type keptKey struct {
|
|
Revision uint64 `json:"revision"`
|
|
Value json.RawMessage `json:"value"`
|
|
}
|
|
|
|
func keptGrantsKey(opts options) ([]byte, uint64) {
|
|
raw, err := os.ReadFile(grantsKeyPath(opts))
|
|
if err != nil {
|
|
return nil, 0
|
|
}
|
|
var k keptKey
|
|
if json.Unmarshal(raw, &k) != nil || len(k.Value) == 0 {
|
|
return nil, 0
|
|
}
|
|
return k.Value, k.Revision
|
|
}
|
|
|
|
func keepGrantsKey(opts options, value []byte, rev uint64, say link.Announce) {
|
|
path := grantsKeyPath(opts)
|
|
if value == nil {
|
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
announceOr(say)("the grants part's user-list key kept on this machine could not be taken away: " + err.Error())
|
|
}
|
|
return
|
|
}
|
|
raw, _ := json.Marshal(keptKey{Revision: rev, Value: value})
|
|
tmp := path + ".new"
|
|
err := os.WriteFile(tmp, raw, 0o600)
|
|
if err == nil {
|
|
err = os.Rename(tmp, path)
|
|
}
|
|
if err != nil {
|
|
announceOr(say)("the grants part's user-list key could not be kept on this machine: " + err.Error())
|
|
}
|
|
}
|