package resolution import ( "encoding/json" "fmt" "path" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) 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" const 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() []mcat.Verb { var verbs []mcat.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) mcat.Verb { in["reason"] = "why the file is changed, for the audit trail" in["override"] = overrideWord return mcat.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 (:/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 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"), mcat.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"})}, mcat. // 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"})}, mcat.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"), ) } // 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 resolutionOwnedPaths(r mcat.Resolution) 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 mcat.Seats() { for _, sp := range s.Paths { for _, m := range r.Modules { if m.ClaimsSeat(s.Name) && declaresPath(m, path.Clean(sp.Path)) && !resolutionStepsBackHere(r, 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 resolutionStepsBackHere(r mcat.Resolution, m mcat.Manifest, sp mcat.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 resolutionFilesWords(r mcat.Resolution, m mcat.Manifest, words map[string]string, with mcat.Rendering) (map[string]string, error) { if !m.ClaimsSeat(mcat.FilesSeat) { return words, nil } places, err := resolutionSecretPlaces(r, with) if err != nil { return nil, err } places2, err := json.Marshal(places) if err != nil { return nil, err } owned, err := json.Marshal(resolutionOwnedPaths(r)) if err != nil { return nil, err } if words == nil { words = map[string]string{} } for _, w := range []string{mcat.ReadsSecretPlaces, mcat.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[mcat.ReadsSecretPlaces] = string(places2) words[mcat.FilesOwnedPaths] = string(owned) return words, nil }