Files
mesh-controller/internal/catalogue/grants.go
T
jschoubben 30599c1858 Pin the node-engine at its reviewed grant, and hold the mount match to the path itself (hq ADR 0312, issue 537 review)
A path beginning with the same letters is another path, and a read-only mount
of a read-write access is still granted: the grant follows what mounts it.
2026-10-12 13:47:17 +02:00

135 lines
5.1 KiB
Go

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
}