mesh-vault: bound the record of hand-outs and record only a well-formed ask (review 255)
mesh/merge-gate head 6ab38066c910 merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6ab38066c910 merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 6ab38066c910 merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6ab38066c910 merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,8 @@ func build() *Env {
|
||||
say("%v", err)
|
||||
}
|
||||
if env.Ledger != nil && env.Receives != "" {
|
||||
env.Custody = &Custody{Ledger: env.Ledger, Receives: env.Receives, Emit: emit, Now: time.Now, Log: say}
|
||||
env.Custody = &Custody{Ledger: env.Ledger, Receives: env.Receives, Resource: "secret", Node: os.Getenv("MESH_NODE"),
|
||||
Emit: emit, Now: time.Now, Log: say}
|
||||
}
|
||||
store, err := OpenStore(os.Getenv("MESH_VAULT_STORE"))
|
||||
if err != nil {
|
||||
@@ -61,7 +62,9 @@ func adoptOperatorKey(env *Env) {
|
||||
if err == nil {
|
||||
_, err = env.Vault.SetOperatorKey(key, "the mesh's export")
|
||||
}
|
||||
env.refuseKey("")
|
||||
if err != nil {
|
||||
env.refuseKey(err.Error())
|
||||
say("the operator key in the mesh's export is not taken, or not every copy is sealed to it yet: %v", err)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
@@ -52,8 +53,20 @@ type Store struct {
|
||||
dir string
|
||||
// failWrite, when a test sets it, fails the write of a record: how a failure partway is proven.
|
||||
failWrite func(Record) error
|
||||
// HandedMax is the size at which the record of hand-outs is rolled (0: HandedMaxDefault).
|
||||
HandedMax int64
|
||||
|
||||
handedMu sync.Mutex
|
||||
rolled int
|
||||
}
|
||||
|
||||
// The record of hand-outs is bounded: rolled at HandedMaxDefault into handed.jsonl.1 … .HandedKept, the
|
||||
// oldest dropped, so a flood of asks costs at most (HandedKept+1) × HandedMaxDefault on the vault's disk.
|
||||
const (
|
||||
HandedMaxDefault = 1 << 20
|
||||
HandedKept = 3
|
||||
)
|
||||
|
||||
// OpenStore opens (and makes) the store at dir.
|
||||
func OpenStore(dir string) (*Store, error) {
|
||||
if dir == "" {
|
||||
@@ -132,17 +145,54 @@ type Handing struct {
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// RecordHanding appends one handing to the store's record, synced before it returns.
|
||||
func (s *Store) RecordHanding(h Handing) error {
|
||||
// RecordHanding appends one handing to the store's record, synced before it returns. It says whether the
|
||||
// record was rolled to make room.
|
||||
func (s *Store) RecordHanding(h Handing) (rolled bool, err error) {
|
||||
raw, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
s.handedMu.Lock()
|
||||
defer s.handedMu.Unlock()
|
||||
path := filepath.Join(s.dir, "handed.jsonl")
|
||||
max := s.HandedBound()
|
||||
if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && info.Size()+int64(len(raw))+1 > max {
|
||||
if err := s.roll(path); err != nil {
|
||||
return false, err
|
||||
}
|
||||
rolled = true
|
||||
}
|
||||
return rolled, appendSynced(path, append(raw, '\n'))
|
||||
}
|
||||
|
||||
// roll moves handed.jsonl to .1, .1 to .2 and so on, dropping what falls past HandedKept.
|
||||
func (s *Store) roll(path string) error {
|
||||
_ = os.Remove(fmt.Sprintf("%s.%d", path, HandedKept))
|
||||
for i := HandedKept - 1; i >= 1; i-- {
|
||||
if err := os.Rename(fmt.Sprintf("%s.%d", path, i), fmt.Sprintf("%s.%d", path, i+1)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Rename(path, path+".1"); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(filepath.Join(s.dir, "handed.jsonl"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
s.rolled++
|
||||
return syncDir(s.dir)
|
||||
}
|
||||
|
||||
// Rolled is how many times the record of hand-outs was rolled since the vault started.
|
||||
func (s *Store) Rolled() int {
|
||||
s.handedMu.Lock()
|
||||
defer s.handedMu.Unlock()
|
||||
return s.rolled
|
||||
}
|
||||
|
||||
func appendSynced(path string, raw []byte) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(append(raw, '\n')); err != nil {
|
||||
if _, err := f.Write(raw); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
@@ -153,26 +203,43 @@ func (s *Store) RecordHanding(h Handing) error {
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// Handed is the latest handings, newest first, at most limit.
|
||||
// Handed is the latest handings, newest first, at most limit: the current record, then the rolled ones.
|
||||
func (s *Store) Handed(limit int) ([]Handing, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(s.dir, "handed.jsonl"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return []Handing{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
|
||||
s.handedMu.Lock()
|
||||
defer s.handedMu.Unlock()
|
||||
path := filepath.Join(s.dir, "handed.jsonl")
|
||||
out := []Handing{}
|
||||
for i := len(lines) - 1; i >= 0 && len(out) < limit; i-- {
|
||||
var h Handing
|
||||
if json.Unmarshal([]byte(lines[i]), &h) == nil {
|
||||
out = append(out, h)
|
||||
for i := 0; i <= HandedKept && len(out) < limit; i++ {
|
||||
file := path
|
||||
if i > 0 {
|
||||
file = fmt.Sprintf("%s.%d", path, i)
|
||||
}
|
||||
raw, err := os.ReadFile(file)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
|
||||
for j := len(lines) - 1; j >= 0 && len(out) < limit; j-- {
|
||||
var h Handing
|
||||
if json.Unmarshal([]byte(lines[j]), &h) == nil {
|
||||
out = append(out, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HandedBound is the size at which the record of hand-outs is rolled.
|
||||
func (s *Store) HandedBound() int64 {
|
||||
if s.HandedMax > 0 {
|
||||
return s.HandedMax
|
||||
}
|
||||
return HandedMaxDefault
|
||||
}
|
||||
|
||||
// List is every record, by name.
|
||||
func (s *Store) List() ([]Record, error) {
|
||||
entries, err := os.ReadDir(filepath.Join(s.dir, "secrets"))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -138,16 +139,26 @@ func (v *Vault) announce(c Changed) {
|
||||
}
|
||||
}
|
||||
|
||||
// recordHanding keeps who asked for what, and what they got, in the vault's own store.
|
||||
// recordHanding keeps who asked for what, and what they got, in the vault's own store, and says loudly
|
||||
// when the record had to be rolled to make room.
|
||||
func (v *Vault) recordHanding(h Handing) error {
|
||||
h.At = v.Now().UTC()
|
||||
err := v.Store.RecordHanding(h)
|
||||
rolled, err := v.Store.RecordHanding(h)
|
||||
if err != nil {
|
||||
v.log("the record of %s asking for %s could not be written: %v", h.To, h.Name, err)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
if rolled {
|
||||
v.log("THE RECORD OF HAND-OUTS WAS ROLLED (%d times since the vault started): it reached its bound of %d bytes; "+
|
||||
"the oldest of %d kept files is dropped. Many asks in little time is worth a look: secret_handed and vault_status",
|
||||
v.Store.Rolled(), v.Store.HandedBound(), HandedKept)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// busUser is the shape of a caller the runtime proves (the SDK's): the one shape a refusal is recorded for.
|
||||
var busUser = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`)
|
||||
|
||||
// Get answers caller with the secret name sealed to publicKey, if caller may ask for it. The caller is the
|
||||
// bus user the runtime proved, never an argument; an unproven call is refused.
|
||||
func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
|
||||
@@ -158,9 +169,16 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
|
||||
if askers == nil {
|
||||
askers = NoAsker{}
|
||||
}
|
||||
if !busUser.MatchString(caller) || len(caller) > 200 {
|
||||
return Answer{}, errors.New("the vault answers only a caller the runtime proved")
|
||||
}
|
||||
// A name is judged before anything is recorded, so an arbitrary argument fills no record.
|
||||
if err := validName(name); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
if ok, why := askers.May(caller, name); !ok {
|
||||
v.log("refused %s to %s: %s", name, caller, why)
|
||||
v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why})
|
||||
_ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why})
|
||||
return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why)
|
||||
}
|
||||
key, err := askerKey(publicKey)
|
||||
|
||||
@@ -116,7 +116,8 @@
|
||||
"provisioner_retirement",
|
||||
"provisioner_retire_approve",
|
||||
"provisioner_retire_reject",
|
||||
"provisioner_delete"
|
||||
"provisioner_delete",
|
||||
"vault_status"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user