diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 6a0be40c..60c1effb 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -262,8 +262,8 @@ type Rendering struct { TakenLibraries map[string]bool LibraryPaths map[string][]string // RunnerJoinsOwnGroups says this machine's engine puts the tool runner's account in a joining module's own group - // (novox/hq ADR 0334 §2: its statement's contract is link.OwnGroupContract or later); to an older one the - // identity's runner is not sent. + // and takes a directory's setgid mode (novox/hq ADR 0334 §1, §2: its statement's contract is link.OwnGroupContract + // or later); to an older one neither the identity's runner nor the setgid bit is sent. RunnerJoinsOwnGroups bool } @@ -1062,7 +1062,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string, // And its identity and libraries, where it joins (novox/hq ADR 0328 §4, §5, §10): after the placement's // owner, which a joining module may not set. if joins { - if err := joinedInto(copied, m, joined, libPaths); err != nil { + if err := joinedInto(copied, m, joined, libPaths, with.RunnerJoinsOwnGroups); err != nil { return nil, err } } diff --git a/internal/catalogue/libraries.go b/internal/catalogue/libraries.go index 7c8ea78e..a906718d 100644 --- a/internal/catalogue/libraries.go +++ b/internal/catalogue/libraries.go @@ -424,8 +424,8 @@ func toolsDirectoryProblems(m Manifest) []string { } if n&0o050 != 0o050 { problems = append(problems, fmt.Sprintf("%s's tools name its directory %s, whose mode %s closes it to its "+ - "own group: the tool runner reads it through that group, so it needs group read and enter (0750) "+ - "(novox/hq ADR 0334 §3)", m.Module, id, mode)) + "own group: the tool runner reads it through that group, so it needs group read and enter (0750; "+ + "the mesh adds setgid, 2750) (novox/hq ADR 0334 §3)", m.Module, id, mode)) } } } @@ -619,7 +619,7 @@ func identityFill(s string, j Joined) string { // joinedInto composes one resource of a joining module (§4, §5, §10): ${identity:…} filled everywhere, a container's // ${library:} volume sources filled with the host path and a read entry mounted read-only, and a directory owned by // the module's user and primary group. -func joinedInto(resource map[string]any, m Manifest, j Joined, paths map[string]string) error { +func joinedInto(resource map[string]any, m Manifest, j Joined, paths map[string]string, setgid bool) error { for k, v := range resource { if k == "id" || k == "type" { continue @@ -630,10 +630,18 @@ func joinedInto(resource map[string]any, m Manifest, j Joined, paths map[string] } resource[k] = filled } - // Its own directories belong to its user and its own group (ADR 0334 §1): the library's group is its processes' - // primary, so what they make in a library keeps that group, and its own data stays its own. + // Its own directories belong to its user and its own group, with setgid (ADR 0334 §1, v4): the library's group is + // its processes' primary, so what they make in a library keeps that group — and what they make in their own + // directories keeps mesh- by the setgid bit. The bit only to an engine that takes a mode with it + // (Rendering.RunnerJoinsOwnGroups, link.OwnGroupContract): the one pinned refuses a mode not starting with 0. if fmt.Sprint(resource["type"]) == "directory" { resource["owner"] = fmt.Sprintf("%d:%d", j.UID, j.UID) + if mode, ok := resource["mode"].(string); ok && setgid { + var n uint64 + if _, err := fmt.Sscanf(mode, "%o", &n); err == nil && mode != "" { + resource["mode"] = fmt.Sprintf("%04o", n|0o2000) + } + } } if fmt.Sprint(resource["type"]) != "container" { return nil diff --git a/internal/catalogue/libraries_test.go b/internal/catalogue/libraries_test.go index 2b0ab87b..0b0c9ef4 100644 --- a/internal/catalogue/libraries_test.go +++ b/internal/catalogue/libraries_test.go @@ -357,6 +357,11 @@ func TestAJoiningModulesOwnDirectoriesAreItsOwnGroups(t *testing.T) { if owner := got["radarr.config"]["owner"]; owner != "50020:50020" { t.Errorf("its directory is owned by %v; want its user and its own group", owner) } + // ADR 0334 v4: with setgid, so what is made in it later keeps mesh-radarr though the process's primary group is the + // library's. + if mode := got["radarr.config"]["mode"]; mode != "0750" { + t.Errorf("an engine that refuses setgid modes was sent %v", mode) + } if env := got["radarr.app"]["env"].(map[string]any); env["PGID"] != "50010" { t.Errorf("its process's primary group is %v; want the library's, so what it makes there keeps the group", env["PGID"]) } @@ -366,9 +371,13 @@ func TestAJoiningModulesOwnDirectoriesAreItsOwnGroups(t *testing.T) { } with := librariesRendering() with.RunnerJoinsOwnGroups = true - if got := composeLibraries(t, r, with).Identities[0].Runner; got != "jochen" { + newer := composeLibraries(t, r, with) + if got := newer.Identities[0].Runner; got != "jochen" { t.Errorf("the runner joins %q's own group; want jochen, whose tools it runs", got) } + if mode := byID(newer.Resources)["radarr.config"]["mode"]; mode != "2750" { + t.Errorf("its directory's mode is %v; want 2750, setgid on its own 0750", mode) + } } // novox/hq ADR 0334 §3: a joining module whose tools name a directory whose mode closes it to its group is refused at diff --git a/internal/link/protocol.go b/internal/link/protocol.go index ff2450ea..923c8280 100644 --- a/internal/link/protocol.go +++ b/internal/link/protocol.go @@ -452,8 +452,9 @@ const IdentityContract = 5 const LibraryContract = 6 // OwnGroupContract is the statement of an engine that also puts the tool runner's account in a joining module's own -// group, named by an identity's `runner` (novox/hq ADR 0334 §2). Not stated by any engine yet: the engine must add -// `runner` (an account's name) to its declaration's Identity, and put that account in the group mesh-. +// group, named by an identity's `runner`, and takes a directory's mode with the setgid bit, such as "2750" (novox/hq +// ADR 0334 §1, §2). Not stated by any engine yet: the engine must add `runner` (an account's name) to its declaration's +// Identity and put that account in the group mesh-, and accept a directory mode whose first digit is 2. const OwnGroupContract = 7 // KindLibrary is a library's own verdict on its home, as the engine may state it (novox/hq ADR 0328): healthy once