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 §4). 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 (:/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"), 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"}}, Verb{Name: "restore", Description: "Put an item of the node's trash back where it was; refuses when " + "something is there now.", Input: schema(map[string]string{"id": "an id from trash"}, []string{"id"}), Replaces: []string{"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 §4): 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)) { owned[sp.Path] = m.Module + " (" + s.Name + ")" } } } } return owned } // 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 §4)", m.Module, w) } } words[ReadsSecretPlaces] = string(places2) words[FilesOwnedPaths] = string(owned) return words, nil }