Give the controller the contract it can fill (hq ADR 0325, issue 567)

The controller cannot know the operator's account's uid, names an access by its
declared id (which holds a slash), and should not parse words. A tool runner's
account may now come by name alone and is read here (refused when absent or root's,
its uid said in the access's listing); the take act matches the declared id exactly;
an access wait says its reason (keep or disk) and, for a filesystem that refuses
ACLs, whether the take act can enable them.
This commit is contained in:
2026-10-12 19:51:09 +02:00
parent ef26dba275
commit aad33dfe5c
12 changed files with 354 additions and 21 deletions
+64 -3
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/novox/mesh-host/internal/accounts"
"os"
"path/filepath"
"regexp"
@@ -476,6 +477,10 @@ func RootWords(why string) string {
// Nothing is changed on the folder but the entries the mesh adds and the mask that shows them.
func applyAccess(ctx context.Context, sys system.System, r *declaration.Access, run Runner, previous store.Applied,
in inputs) (Outcome, error) {
// A tool runner's account that is not here, or is root's, refuses its access (§4, §11).
if err := in.runners[r.ID]; err != nil {
return begin(r), err
}
// What ADR 0312's grant put an account in is taken back first: no account is in a group for data (§5).
gave, left, said := takeBackGranted(ctx, sys, previous, run, in.groups, in.handed)
out, err := applyAccessFolder(ctx, r, in)
@@ -832,10 +837,18 @@ const (
VerdictNobody = "nobody"
)
// What a keep wait says of its folder's filesystem and ACLs: the same words as link's ACLNeedsEnabling and
// ACLUnsupported, which the engine says them as.
const (
ACLNeedsEnabling = "needs-enabling"
ACLUnsupported = "unsupported"
)
// AccessJudgement is a look's judgement of an access's folder, and why, in words; Placed names the filesystem at
// it for a wait, with no path (PlacedID).
// it for a wait, with no path (PlacedID); ACL is, on a keep wait, ACLNeedsEnabling or ACLUnsupported
// when its filesystem refuses access entries now.
type AccessJudgement struct {
Kind, Why, Placed string
Kind, Why, Placed, ACL string
}
// AccessVerdict judges an access's folder for a look, read only (ADR 0325 §7, §9, §12): taken is whether the
@@ -858,7 +871,7 @@ func AccessVerdict(ctx context.Context, path string, taken, used bool) AccessJud
case v.kind == folderMake:
return AccessJudgement{Kind: VerdictMake}
case v.kind == folderWaitKeep, v.kind == folderKeep && !rec.Kept && !taken:
return AccessJudgement{Kind: VerdictWaitKeep, Placed: PlacedID(v.mount)}
return AccessJudgement{Kind: VerdictWaitKeep, Placed: PlacedID(v.mount), ACL: aclProbe(path, v.mount)}
}
return AccessJudgement{Kind: VerdictKeep}
}
@@ -951,3 +964,51 @@ func legacyAccess(r *declaration.Access) (Outcome, error) {
out.Detail = "operator-owned; present, " + mode + ", nothing managed"
return out, nil
}
// AccountIDs reads an account's uid and primary gid by name from the machine's user database (getent, through the
// account judge's reader). A variable so a test can stand in for accounts it cannot make.
var AccountIDs = func(ctx context.Context, name string) (uid, gid int, found bool, err error) {
return accounts.Exec{Run: accounts.Runner(ExecRunner)}.IDs(ctx, name)
}
// ResolveRunners gives every tool runner's account that came with no uid its number, read by name on this machine
// (ADR 0325 §4): the controller cannot know the operator's account's. One that is not here, or is root's — uid 0 or
// a root group (§11) — is taken out of the access's users, and its access is refused, naming why, by id. Changes
// the declaration in place; a user that already has its number is left as it is.
func ResolveRunners(ctx context.Context, d *declaration.Declaration) map[string]error {
refused := map[string]error{}
if d == nil {
return refused
}
for _, r := range d.Resources {
a, ok := r.(*declaration.Access)
if !ok {
continue
}
kept := a.Users[:0]
for _, u := range a.Users {
if u.From != declaration.FromToolRunner || u.UID != 0 {
kept = append(kept, u)
continue
}
uid, gid, found, err := AccountIDs(ctx, u.Identity)
switch {
case err != nil:
refused[a.ID] = fmt.Errorf("the tool runner's account %s could not be read: %w", u.Identity, err)
continue
case !found:
refused[a.ID] = fmt.Errorf("the tool runner's account %s is not on this machine, so no entry can "+
"be kept for it (novox/hq ADR 0325 §4)", u.Identity)
continue
}
if err := rootIdentity(uid, gid); err != nil {
refused[a.ID] = fmt.Errorf("the tool runner's account %s is never given an entry: %w", u.Identity, err)
continue
}
u.UID = uid
kept = append(kept, u)
}
a.Users = kept
}
return refused
}