Keep libraries under groups of their own, and give modules users in them (hq ADR 0328, issue 567)

ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by
its group rather than by ACL entries. The engine now makes each library's group and
each module's user in its libraries' groups; keeps a library's root and requested
sub-paths in its group with group write and setgid on its home, making missing
sub-paths as the root is; waits for the operator's word before touching a root that
holds data, and on the take act walks it by descriptor (no link, same filesystem,
bounded and resumed, files with other names left); holds the containers of a library
whose disk or mount is not there; and puts the tool runner's account in the group.
The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
This commit is contained in:
2026-10-12 22:11:09 +02:00
parent 303d4037e8
commit 2a2be687f3
35 changed files with 2583 additions and 5004 deletions
+150 -23
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"net"
"path/filepath"
"reflect"
"regexp"
"slices"
@@ -456,10 +457,9 @@ const (
// **The distinction 04-ISSUES/036 and 026 turn on.** A `directory` resource is the mesh's own —
// it creates it, sets its owner and mode, and removes it when empty ([ADR 0030](novox/hq)). A
// media library, a download spool is the operator's: it existed before the mesh, several modules
// read and write it at once, and the mesh must not chown, chmod or remove it. The host confirms it
// is there and mounts it, and keeps it usable by the identities that use it through ACL entries it
// adds and nothing else (novox/hq ADR 0325 §5); a missing one is made only below a parent on the
// filesystem the data was placed on (§9).
// read and write it at once, and the mesh must not create, chown, chmod or remove it. The host
// confirms it is there and mounts it; nothing else. Library data is a library's (novox/hq ADR 0328),
// not an access.
type Access struct {
ID string `json:"id"`
Type Type `json:"type"`
@@ -467,15 +467,11 @@ type Access struct {
// Mode is how this module reaches the path: read or read-write. Absent narrows to read.
Mode string `json:"mode,omitempty"`
// Users are the identities that use the folder, resolved by the controller (novox/hq ADR 0325 §3, §4): each
// module identity whose container mounts it, and the tool runner's account standing for every module whose
// tools use it. The engine keeps one ACL entry per user on the folder and as its default (§5); several
// accesses naming one folder are kept as one, each uid at the widest use any of them gives it. Absent is a
// controller older than ADR 0325, or an access nobody but others' bits reach (§2): nothing is kept.
// Users and Taken are what a controller speaking ADR 0325 sends (the identities that use the folder, and
// that the operator's warrant was carried out): accepted, so its declaration is not refused whole, and not
// acted on since ADR 0328 superseded ADR 0325 — no ACL entry is kept.
Users []AccessUser `json:"users,omitempty"`
// Taken is the controller saying the operator's warrant for this folder was carried out (§8): the folder is
// kept although it holds data. The engine also keeps a folder it took itself, on the act, or found empty.
Taken bool `json:"taken,omitempty"`
Taken bool `json:"taken,omitempty"`
// Grant is what a controller older than ADR 0325 still sends (ADR 0312's group grant, superseded): accepted
// so its declaration is not refused whole, and never acted on — no account is put in a group for data (§5).
@@ -1542,19 +1538,129 @@ type Declaration struct {
// not refused for it.
PutBack bool
// Identities is the identity of every module assigned to this machine (novox/hq ADR 0325 §3): an account
// Identities is the identity of every module assigned to this machine (novox/hq ADR 0328 §4): an account
// and a group `mesh-<module>` with the numbers the controller allocated once for the module, the same on
// every machine. The engine makes each before any resource is applied, and removes what it made of one no
// longer declared. Absent is a controller older than ADR 0325.
Identities []Identity
// Groups is the group of every library this machine is reached by (novox/hq ADR 0328 §3): `mesh-<library>`
// with the number the controller allocated once, mesh-wide. The engine makes each, and removes what it made of
// one no longer declared. Absent is a controller older than ADR 0328.
Groups []Group
// Libraries is every library this machine is reached by (ADR 0328 §1, §6, §9): its root here, its group, whether
// this is its home, whether the operator's warrant to take it was carried out, the sub-paths its modules ask for,
// and the tool runner's account to put in its group (§11). Absent is a controller older than ADR 0328.
Libraries []Library
}
// Identity is one module's identity, as the controller allocated it.
// Identity is one module's identity, as the controller allocated it (ADR 0328 §4): its user `mesh-<module>` with
// uid, its own group of the same name and number, its primary group gid (its own, or its first library's group),
// and the groups of its other libraries as added groups.
type Identity struct {
Module string `json:"module"`
Name string `json:"name"`
UID int `json:"uid"`
GID int `json:"gid"`
Groups []int `json:"groups,omitempty"`
}
// Group is one library's group, as the controller allocated it (ADR 0328 §3).
type Group struct {
Name string `json:"name"`
GID int `json:"gid"`
}
// GroupPrefix starts every library group's name, as IdentityPrefix starts every module's.
const GroupPrefix = "mesh-"
// LibraryAccessPrefix starts the access a library's wait and its take act name: `library:<name>`.
const LibraryAccessPrefix = "library:"
// Library is one library as this machine sees it (ADR 0328).
type Library struct {
Library string `json:"library"`
// Root is the library's root on this machine: on its home the data itself, elsewhere the mount point the mounts
// module mounts it at.
Root string `json:"root"`
Group string `json:"group"`
GID int `json:"gid"`
// Home is that the library's data lives on this machine: only there does the engine keep it.
Home bool `json:"home"`
// Taken is the controller saying the operator's warrant to take it was carried out (§7).
Taken bool `json:"taken,omitempty"`
// Paths are the sub-paths its modules ask for, relative to the root.
Paths []string `json:"paths,omitempty"`
// Runner is the tool runner's account, put in the library's group (§11). Absent when no module's tools use it.
Runner string `json:"runner,omitempty"`
}
// checkLibraries judges the groups and libraries on arrival: every library's group declared, named for it, never
// root's number; paths that stay below the root; a runner that is an account's name; and no two roots that nest
// (§13).
func checkLibraries(groups []Group, libs []Library) []string {
var problems []string
byName := map[string]int{}
numbers := map[int]string{}
for _, g := range groups {
where := fmt.Sprintf("group %q", g.Name)
if !strings.HasPrefix(g.Name, GroupPrefix) || !accountName.MatchString(g.Name) {
problems = append(problems, fmt.Sprintf("%s: a library's group is named %s<library>", where, GroupPrefix))
}
if g.GID <= 0 {
problems = append(problems, fmt.Sprintf("%s: gid %d, and a library's group is never root, nor a number "+
"below it (novox/hq ADR 0328 §15); nothing of this declaration was made", where, g.GID))
}
if _, twice := byName[g.Name]; twice {
problems = append(problems, where+": declared twice")
}
if other, used := numbers[g.GID]; used {
problems = append(problems, fmt.Sprintf("%s: gid %d is %s's too", where, g.GID, other))
}
byName[g.Name], numbers[g.GID] = g.GID, g.Name
}
seen := map[string]bool{}
for _, l := range libs {
where := fmt.Sprintf("library %q", l.Library)
switch {
case l.Library == "" || strings.ContainsAny(l.Library, "/ \t\n"):
problems = append(problems, where+": a library is named by one word")
case seen[l.Library]:
problems = append(problems, where+": declared twice")
}
seen[l.Library] = true
if !filepath.IsAbs(l.Root) || filepath.Clean(l.Root) != l.Root || l.Root == "/" {
problems = append(problems, fmt.Sprintf("%s: its root %q is not an absolute, clean path below /", where,
l.Root))
}
if gid, ok := byName[l.Group]; !ok || gid != l.GID {
problems = append(problems, fmt.Sprintf("%s: its group %s (gid %d) is not one this declaration makes",
where, l.Group, l.GID))
}
if l.Group != GroupPrefix+l.Library {
problems = append(problems, fmt.Sprintf("%s: its group is %s%s, and %s is not", where, GroupPrefix,
l.Library, l.Group))
}
for _, p := range l.Paths {
if p == "" || filepath.IsAbs(p) || filepath.Clean(p) != p || p == ".." || strings.HasPrefix(p, "../") {
problems = append(problems, fmt.Sprintf("%s: the path %q is not a clean path below its root", where, p))
}
}
if l.Runner != "" && !accountName.MatchString(l.Runner) {
problems = append(problems, fmt.Sprintf("%s: its runner %q is not an account's name", where, l.Runner))
}
}
for i, a := range libs {
for _, b := range libs[i+1:] {
if a.Root != "" && b.Root != "" && (a.Root == b.Root || strings.HasPrefix(a.Root, b.Root+"/") ||
strings.HasPrefix(b.Root, a.Root+"/")) {
problems = append(problems, fmt.Sprintf("library %q: its root and library %q's nest, and libraries "+
"never overlap (novox/hq ADR 0328 §13)", a.Library, b.Library))
}
}
}
return problems
}
// IdentityPrefix starts every module identity's name.
@@ -1562,9 +1668,13 @@ const IdentityPrefix = "mesh-"
// checkIdentities judges the identities on arrival: each named for its module, never root, and no name or number
// twice — two modules with one account would write as each other.
func checkIdentities(ids []Identity) []string {
func checkIdentities(ids []Identity, groups []Group) []string {
var problems []string
names, uids, gids := map[string]bool{}, map[int]string{}, map[int]string{}
names, uids := map[string]bool{}, map[int]string{}
libraryGroups := map[int]bool{}
for _, g := range groups {
libraryGroups[g.GID] = true
}
for _, id := range ids {
where := fmt.Sprintf("identity %q", id.Name)
if id.Module == "" || id.Name != IdentityPrefix+id.Module || !accountName.MatchString(id.Name) {
@@ -1573,7 +1683,7 @@ func checkIdentities(ids []Identity) []string {
}
if id.UID <= 0 || id.GID <= 0 {
problems = append(problems, fmt.Sprintf("%s: uid %d, gid %d, and a module's identity is never root, "+
"nor a number below it (novox/hq ADR 0325 §11); nothing of this declaration was made", where,
"nor a number below it (novox/hq ADR 0328 §15); nothing of this declaration was made", where,
id.UID, id.GID))
}
if names[id.Name] {
@@ -1582,10 +1692,22 @@ func checkIdentities(ids []Identity) []string {
if other, used := uids[id.UID]; used {
problems = append(problems, fmt.Sprintf("%s: uid %d is %s's too", where, id.UID, other))
}
if other, used := gids[id.GID]; used {
problems = append(problems, fmt.Sprintf("%s: gid %d is %s's too", where, id.GID, other))
// Its primary group is its own (gid == uid) or a library's this declaration makes (ADR 0328 §4); so is
// every added group. A module's own number is never a library group's.
if id.GID != id.UID && !libraryGroups[id.GID] {
problems = append(problems, fmt.Sprintf("%s: its primary group gid %d is neither its own (%d) nor a "+
"library's this declaration makes", where, id.GID, id.UID))
}
names[id.Name], uids[id.UID], gids[id.GID] = true, id.Name, id.Name
if libraryGroups[id.UID] {
problems = append(problems, fmt.Sprintf("%s: uid %d is a library group's number too", where, id.UID))
}
for _, g := range id.Groups {
if !libraryGroups[g] {
problems = append(problems, fmt.Sprintf("%s: its added group gid %d is not a library's this "+
"declaration makes", where, g))
}
}
names[id.Name], uids[id.UID] = true, id.Name
}
return problems
}
@@ -1757,9 +1879,13 @@ type envelope struct {
// sends them only to a host whose reports say `reads_generation`.
Generation int64 `json:"generation,omitempty"`
PutBack bool `json:"put_back,omitempty"`
// Identities is optional on the wire too (novox/hq ADR 0325 §3). **An older host refuses this key**; a
// Identities is optional on the wire too (novox/hq ADR 0325 §3, ADR 0328 §4). **An older host refuses this key**; a
// controller sends it only to a host whose health says link.IdentityContract.
Identities []Identity `json:"identities,omitempty"`
// Groups and Libraries are optional too (novox/hq ADR 0328). **An older host refuses these keys**; a controller
// sends them only to a host whose health says link.LibraryContract.
Groups []Group `json:"groups,omitempty"`
Libraries []Library `json:"libraries,omitempty"`
}
func parse(raw []byte, allowActions bool) (*Declaration, error) {
@@ -1778,8 +1904,9 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence,
Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack,
Identities: env.Identities}
problems := checkIdentities(env.Identities)
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries}
problems := checkIdentities(env.Identities, env.Groups)
problems = append(problems, checkLibraries(env.Groups, env.Libraries)...)
if env.Generation < 0 {
// Below zero would read as "none claimed" and pass every refusal of an older generation.
problems = append(problems, fmt.Sprintf("an assignment generation below zero (%d) is not one the "+