package catalogue import ( "fmt" "path/filepath" "sort" "strings" ) // A read-write access grants the path's own group to what runs the module's code (novox/hq ADR 0312, issue 537). // // **The controller says who; the node-engine reads which group.** Which group owns the operator's path is a fact // of the machine the controller cannot know, and the path is never changed (§2): so each read-write access // reaches a machine naming the identities its module's code runs as, and the node-engine reads the path's group // at apply and grants it to them. Two kinds of identity run a module's code here: // // - each of its containers that mounts the path — the source of one of its volumes is the path, or below it — // which gains the group as a supplementary group of its process and is recreated with it; // - the account its tools run as, where it has a tools bundle the machine's runtime loads: the operator's // account the runtime runs as, or the module's own account for a module served by a runtime of its own // (ADR 0259 §8). That runtime names the access under its `restart-on`, so it is restarted when the access // moved the account's groups, and its new process holds the group with no new login (§6). // // A read access grants nothing (§3): it is sent as before. A read-write access whose module runs nothing here is // sent an empty grant, so the node-engine still states it unhealthy while its group cannot write (§2). // // **Only to an engine that reads it.** An older node-engine parses strictly and refuses a whole declaration for a // field it does not know; the field and the restarts it brings are composed only where the engine's statement // says link.GrantContract or later (Rendering.GrantsGroups). // GrantField is an access's field naming who its path's group is granted to (novox/hq ADR 0312). const GrantField = "grant" // toolsAccount is the account module m's tools run as on this machine, or empty where they run nowhere as one: // no runtime here, no bundle it loads, a module left out of an adopted machine's declaration, or a runtime // running as root on a machine with no account. func (r Resolution) toolsAccount(m Manifest, with Rendering) string { if !r.runtimeHere() || (with.Adopted && m.Filtering != nil) { return "" } loads := false for _, b := range m.Bundles { if len(b.Loads) > 0 { loads = true } } if !loads { return "" } if m.RunsAs != "" { return m.RunsAs } return r.Account } // grantAccesses writes the grant of every read-write access of module, among the resources composed so far: the // module's containers that mount the access's path, and account when it is not empty. The ids of the accesses // that grant to account, for the runtime that runs as it to be restarted on. func grantAccesses(out []map[string]any, owner map[string]string, module, account string) []string { var containers []map[string]any for _, res := range out { if res["type"] == "container" && owner[fmt.Sprint(res["id"])] == module { containers = append(containers, res) } } var toAccount []string for _, res := range out { if res["type"] != "access" || owner[fmt.Sprint(res["id"])] != module || res["mode"] != AccessReadWrite { continue } path, _ := res["path"].(string) grant := map[string]any{} var mounting []any for _, c := range containers { if mounts(c, path) { mounting = append(mounting, fmt.Sprint(c["id"])) } } if len(mounting) > 0 { grant["containers"] = mounting } if account != "" { grant["account"] = account toAccount = append(toAccount, fmt.Sprint(res["id"])) } res[GrantField] = grant } sort.Strings(toAccount) return toAccount } // mounts says whether a container mounts path, or something below it, from the machine. A path that merely // begins with the same letters — /storage/media2 beside /storage/media — is another path. A read-only mount of a // read-write access still counts: the module declared the access read-write, the engine grants the group to what // mounts it, and a read-only mount only means that container chose not to write through it. func mounts(container map[string]any, path string) bool { volumes, _ := container["volumes"].([]any) path = filepath.Clean(path) for _, v := range volumes { s, ok := v.(string) if !ok { continue } source, _, _ := strings.Cut(s, ":") if !strings.HasPrefix(source, "/") { continue // a named volume: the runtime's, not the operator's path } source = filepath.Clean(source) if source == path || strings.HasPrefix(source, path+"/") { return true } } return false } // restartOnGrants adds to a runtime process's restart-on the accesses that grant to the account it runs as, so it // is restarted to hold a group the account was put in or taken out of (novox/hq ADR 0312 §6). func restartOnGrants(process map[string]any, ids []string) { if process == nil || len(ids) == 0 { return } restarts, _ := process["restart-on"].([]any) seen := map[string]bool{} for _, id := range restarts { seen[fmt.Sprint(id)] = true } for _, id := range ids { if !seen[id] { restarts = append(restarts, id) seen[id] = true } } process["restart-on"] = restarts }