Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
180 lines
9.5 KiB
Go
180 lines
9.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// A node's files, read and managed (novox/hq ADR 0394, issue 900; superseding ADR 0310; hq to-be 41 WP9).
|
|
//
|
|
// **One seat for every file on a node.** The holder runs as the tool runner's account, the node's operator
|
|
// account (ADR 0394 §2), and reaches further through `sudo -n` where that account's rights do not. It refuses by
|
|
// name the places a secret is kept (reads only, ADR 0394 §7) and the paths a module owns, unless a write names a reason (`override`).
|
|
// node-reads stays beside it until mesh-files is assigned on every node (ADR 0394 §9); it goes in the next
|
|
// delivery.
|
|
|
|
// FilesSeat is the role that reads and manages a node's files and accounts (novox/hq ADR 0394).
|
|
const FilesSeat = "node-files"
|
|
|
|
// FilesOwnedPaths is the word the holder of node-files is given beside the secret places: a JSON object from
|
|
// each absolute path a module owns on its node to "module (tool to use)" (novox/hq ADR 0394 §5).
|
|
const FilesOwnedPaths = "MESH_OWNED_PATHS"
|
|
|
|
const (
|
|
filesAsAccount = "as the node's operator account, through sudo -n where its rights do not reach; a place a " +
|
|
"secret is kept, or a known credential file, is refused by name"
|
|
overrideWord = "a reason, to change a path a module owns anyway (optional)"
|
|
)
|
|
|
|
// filesVerbs is the contract every holder of node-files serves (novox/hq ADR 0394 §1): the reads of node-reads,
|
|
// with their descriptions restated for the operator's account, and the writes.
|
|
func filesVerbs() []Verb {
|
|
var verbs []Verb
|
|
for _, v := range readsVerbs() {
|
|
v.Description = strings.Replace(v.Description,
|
|
"Read as the holder's own account, which holds no secret: a component it may not reach says so.",
|
|
"", 1)
|
|
v.Description = strings.TrimSpace(v.Description) + " Read " + filesAsAccount + "."
|
|
verbs = append(verbs, v)
|
|
}
|
|
write := func(name, description string, in map[string]string, required []string, optional []string, replaces ...string) Verb {
|
|
in["reason"] = "why the file is changed, for the audit trail"
|
|
in["override"] = overrideWord
|
|
return Verb{Name: name, Description: description + " Refuses a path a module owns " +
|
|
"unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.",
|
|
Input: schema(in, append(required, "reason"), append(optional, "override")...), Replaces: replaces}
|
|
}
|
|
return append(verbs,
|
|
write("write", "Write one text file whole, replacing what it holds; the old content goes to the node's trash.",
|
|
map[string]string{"path": "an absolute path", "content": "the text to write",
|
|
"mode": "the file's mode, such as 0644 (optional)"},
|
|
[]string{"path", "content"}, []string{"mode"}, "tee", ">"),
|
|
write("copy", "Copy a file or directory to a path on this node. A target on another node (<node>:/path) is "+
|
|
"refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one "+
|
|
"kept in the node's trash for 7 days.",
|
|
map[string]string{"from": "an absolute path", "to": "an absolute path on this node"},
|
|
[]string{"from", "to"}, nil, "cp", "cp -a", "ssh <node> cp"),
|
|
write("move", "Move or rename a file or directory. A target on another node is refused: copying between nodes "+
|
|
"comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days.",
|
|
map[string]string{"from": "an absolute path", "to": "an absolute path"},
|
|
[]string{"from", "to"}, nil, "mv"),
|
|
write("delete", "Delete a file or directory into the node's trash, where restore can bring it back.",
|
|
map[string]string{"path": "an absolute path", "recursive": "\"true\": delete a directory with what it holds"},
|
|
[]string{"path"}, []string{"recursive"}, "rm", "rm -r"),
|
|
write("mkdir", "Make a directory.",
|
|
map[string]string{"path": "an absolute path", "parents": "\"true\": make the missing parents too"},
|
|
[]string{"path"}, []string{"parents"}, "mkdir", "mkdir -p"),
|
|
write("chmod", "Change the mode of a file or directory.",
|
|
map[string]string{"path": "an absolute path", "mode": "the mode, such as 0644 or u+x",
|
|
"recursive": "\"true\": also everything under a directory"},
|
|
[]string{"path", "mode"}, []string{"recursive"}, "chmod"),
|
|
write("chown", "Change the owner and group of a file or directory.",
|
|
map[string]string{"path": "an absolute path", "owner": "an account's name or number",
|
|
"group": "a group's name or number (optional)", "recursive": "\"true\": also everything under a directory"},
|
|
[]string{"path", "owner"}, []string{"group", "recursive"}, "chown", "chgrp"),
|
|
Verb{Name: "upload", Description: "Send one file of this node into a bucket of the object store, over a short signed link " +
|
|
"and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. " +
|
|
"At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, " +
|
|
"before anything is sent. Read " + filesAsAccount + ".",
|
|
Input: schema(map[string]string{"path": "an absolute path to a file", "bucket": "the bucket the file goes into",
|
|
"key": "the object's key (optional; the file's SHA-256 when not given)"}, []string{"path", "bucket"})},
|
|
// download is a plain Verb, not through write(): write() types its optional arguments as true/false
|
|
// switches, which a hash and a reason are not (issue 1719).
|
|
Verb{Name: "download", Description: "Write one object of a bucket to a file of this node, over a short signed link and " +
|
|
"never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to " +
|
|
"the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns " +
|
|
"unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.",
|
|
Input: schema(map[string]string{"bucket": "the bucket", "key": "the object's key", "path": "an absolute path",
|
|
"sha256": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)",
|
|
"reason": "why the file is changed, for the audit trail",
|
|
"override": overrideWord}, []string{"bucket", "key", "path", "reason"})},
|
|
Verb{Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, " +
|
|
"original path and when it went.",
|
|
Input: schema(map[string]string{}, nil), Replaces: []string{"ls ~/.local/share/Trash"}},
|
|
write("restore", "Put an item of the node's trash back where it was; refuses when something is there now.",
|
|
map[string]string{"id": "an id from trash"}, []string{"id"}, nil, "trash-restore"),
|
|
)
|
|
}
|
|
|
|
// FilesHolderProblems is what a holder of node-files is held to (novox/hq ADR 0394 §2): it runs as the tool
|
|
// runner's account, the node's operator account, so it declares no runs-as.
|
|
func FilesHolderProblems(m Manifest) []string {
|
|
if !m.ClaimsSeat(FilesSeat) || m.RunsAs == "" {
|
|
return nil
|
|
}
|
|
return []string{fmt.Sprintf("%s claims %s and declares runs-as %q: its holder runs as the tool runner's account, "+
|
|
"the node's operator account (novox/hq ADR 0394 §2)", m.Module, FilesSeat, m.RunsAs)}
|
|
}
|
|
|
|
// ownedPaths is every path a module owns on this node, as the holder is told (novox/hq ADR 0394 §5): the
|
|
// paths every seat declares (ADR 0287) with the module holding the seat here, and the paths and files each
|
|
// module's manifest places (declaredPaths). A seat's path wins where both name one.
|
|
func (r Resolution) ownedPaths() map[string]string {
|
|
owned := map[string]string{}
|
|
for _, m := range r.Modules {
|
|
for _, p := range sortedKeys(declaredPaths(m)) {
|
|
owned[p] = m.Module + " (its own tools)"
|
|
}
|
|
}
|
|
for _, s := range Seats() {
|
|
for _, sp := range s.Paths {
|
|
for _, m := range r.Modules {
|
|
if m.ClaimsSeat(s.Name) && declaresPath(m, path.Clean(sp.Path)) && !r.stepsBackHere(m, sp) {
|
|
owned[sp.Path] = m.Module + " (" + s.Name + ")"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return owned
|
|
}
|
|
|
|
// stepsBackHere says m's seat path steps back on this machine (novox/hq ADR 0287): it names a seat whose holder here
|
|
// declares the path too, so that holder owns it, as stepsBack leaves m's own rendering of it out. Without it the owner
|
|
// told was the last seat in the order of the seat table, which the controller reads sorted by name and a node in the
|
|
// order compiled (issue 1126, C3: /etc/resolv.conf, node-uplink against node-resolver).
|
|
func (r Resolution) stepsBackHere(m Manifest, sp SeatPath) bool {
|
|
if sp.StepsBackFor == "" {
|
|
return false
|
|
}
|
|
for _, o := range r.Modules {
|
|
if o.Module != m.Module && holdsSeat(o, sp.StepsBackFor) && declaresPath(o, path.Clean(sp.Path)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// filesWords is what the holder of node-files is given beside its own words: the node's secret places and the
|
|
// paths modules own here.
|
|
func (r Resolution) filesWords(m Manifest, words map[string]string, with Rendering) (map[string]string, error) {
|
|
if !m.ClaimsSeat(FilesSeat) {
|
|
return words, nil
|
|
}
|
|
places, err := r.secretPlaces(with)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
places2, err := json.Marshal(places)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
owned, err := json.Marshal(r.ownedPaths())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if words == nil {
|
|
words = map[string]string{}
|
|
}
|
|
for _, w := range []string{ReadsSecretPlaces, FilesOwnedPaths} {
|
|
if _, has := words[w]; has {
|
|
return nil, fmt.Errorf("%s gives its own %s; the node's secret places and owned paths are the mesh's to "+
|
|
"say (novox/hq ADR 0394 §5)", m.Module, w)
|
|
}
|
|
}
|
|
words[ReadsSecretPlaces] = string(places2)
|
|
words[FilesOwnedPaths] = string(owned)
|
|
return words, nil
|
|
}
|