184 lines
6.3 KiB
Go
184 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"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 grants part's user list alone into the file
|
|
// the bus module's composition declares, 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, running func() []engcomp.Composed,
|
|
say link.Announce, wrote func()) *userlist.Follower {
|
|
return userlist.New(userlist.Env{
|
|
Node: session.Membership.Node,
|
|
Running: running,
|
|
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, grantsstate.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,
|
|
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
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|