Keep main's placement guard, and refuse only an archive or a written-into file below an agent's home
Issue 339's guard (#63/#64/#65) now refuses the machine's own trees, another account's directory below a home, and uses a found directory as found; this branch's own copy of those rules goes. What stays is the one rule only an account that never becomes root needs: below its home it owns every parent and could swap a link in between the engine's check and an unpack or a write-into (hq ADR 0266 §7). One reader of the user database serves both.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package apply
|
||||
|
||||
// What is never placed below an agent's home (novox/hq ADR 0266 §7).
|
||||
//
|
||||
// An account that never becomes root owns every directory in its home, so it can swap a link into the path
|
||||
// between the engine's check and the engine's write. The engine writes a whole file there through a
|
||||
// descriptor that followed no link (home_links.go), but it unpacks an archive, and writes into an existing
|
||||
// file, by path. So below the home of an account the declaration says never becomes root, neither is placed.
|
||||
// The catalogue's own check (checks/agenthome) refuses either under `${machine:agent-home}`, so no module
|
||||
// asks; this is the engine refusing it again where it applies. The machine's own trees, other accounts'
|
||||
// homes and a directory's owner below a home are placement_guard.go's (novox/hq issue 339).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// AgentHomeRefusedError is an archive or a written-into file the engine will not place below an agent's home.
|
||||
type AgentHomeRefusedError struct {
|
||||
Path, What, Home string
|
||||
}
|
||||
|
||||
func (e *AgentHomeRefusedError) Error() string {
|
||||
return fmt.Sprintf("%s is not placed: %s below %s, the home of an account that never becomes root, which "+
|
||||
"owns every parent there and could swap a link in between the check and the write (novox/hq ADR 0266); "+
|
||||
"nothing was touched", e.Path, e.What, e.Home)
|
||||
}
|
||||
|
||||
// refuseUnderAgentHome says why a resource is not placed below one of agentHomes; nil when it may be.
|
||||
func refuseUnderAgentHome(r declaration.Resource, agentHomes []string) error {
|
||||
what := ""
|
||||
switch res := r.(type) {
|
||||
case *declaration.Archive:
|
||||
what = "an archive unpacked"
|
||||
case *declaration.File:
|
||||
if res.Into != "" {
|
||||
what = "a file written into (" + res.Into + ")"
|
||||
}
|
||||
}
|
||||
if what == "" {
|
||||
return nil
|
||||
}
|
||||
path := filepath.Clean(r.Target())
|
||||
for _, p := range []string{path, resolved(path)} {
|
||||
for _, home := range agentHomes {
|
||||
if atOrBelow(p, home) {
|
||||
return &AgentHomeRefusedError{Path: path, What: what, Home: home}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rootNeverHomes is the home of every account the declaration says never becomes root, from the user
|
||||
// database; an account not made yet has no home to protect.
|
||||
func rootNeverHomes(d *declaration.Declaration) []string {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
homes := accountsOfHomes()
|
||||
var out []string
|
||||
for _, r := range d.Resources {
|
||||
u, ok := r.(*declaration.User)
|
||||
if !ok || u.Root != declaration.RootNever {
|
||||
continue
|
||||
}
|
||||
for home, a := range homes {
|
||||
if a.Name == u.Name {
|
||||
out = append(out, home)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package apply
|
||||
|
||||
// Defends novox/hq ADR 0266 §7: no archive is unpacked and no file is written into below the home of an account
|
||||
// the declaration says never becomes root; a whole file there still passes.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
func TestNothingIsUnpackedOrWrittenIntoBelowAnAgentsHome(t *testing.T) {
|
||||
was := accountsOfHomes
|
||||
accountsOfHomes = func() map[string]homeAccount {
|
||||
return map[string]homeAccount{"/home/agent": {"agent", 1001}, "/home/operator": {"operator", 1000}}
|
||||
}
|
||||
t.Cleanup(func() { accountsOfHomes = was })
|
||||
agent := []string{"/home/agent"}
|
||||
if err := refuseUnderAgentHome(&declaration.Archive{ID: "a.x", Type: declaration.TypeArchive, Path: "/home/agent/.local/x", Owner: "agent"}, agent); err == nil {
|
||||
t.Error("an archive below an agent's home is refused")
|
||||
}
|
||||
if err := refuseUnderAgentHome(&declaration.File{ID: "a.f", Type: declaration.TypeFile, Path: "/home/agent/.claude.json", Owner: "agent", Into: "json", Content: "{}"}, agent); err == nil {
|
||||
t.Error("a file written into below an agent's home is refused")
|
||||
}
|
||||
if err := refuseUnderAgentHome(&declaration.File{ID: "a.f", Type: declaration.TypeFile, Path: "/home/agent/x", Owner: "agent", Content: "x"}, agent); err != nil {
|
||||
t.Errorf("a whole file there passes: %v", err)
|
||||
}
|
||||
if err := refuseUnderAgentHome(&declaration.Archive{ID: "a.x", Type: declaration.TypeArchive, Path: "/home/operator/x", Owner: "operator"}, agent); err != nil {
|
||||
t.Errorf("an archive in a home whose account may become root is placement_guard.go's to judge, not this: %v", err)
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[{"id":"c.agent","type":"user","name":"agent","root":"never"},`+
|
||||
`{"id":"c.op","type":"user","name":"operator"}]}`)
|
||||
if got := rootNeverHomes(d); len(got) != 1 || got[0] != "/home/agent" {
|
||||
t.Errorf("the agent's home is known by the user database: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -471,6 +471,7 @@ func ApplyMindingWindows(
|
||||
// which declared directories were already here: one this apply makes is the mesh's, one found may not be.
|
||||
refreshRuntimeRoots(ctx)
|
||||
in.refused = refusedPlaces(d.Resources)
|
||||
in.agentHomes = rootNeverHomes(d)
|
||||
in.dirsBefore = map[string]bool{}
|
||||
for _, resource := range d.Resources {
|
||||
in.declares[resource.Identity()] = declaredDigest(resource)
|
||||
@@ -957,8 +958,12 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
|
||||
}
|
||||
}
|
||||
// Nothing below a home is touched through a link an account put there (novox/hq ADR 0266).
|
||||
// And nothing is unpacked or written into below an agent's home (agent_home_guard.go).
|
||||
switch r.(type) {
|
||||
case *declaration.Directory, *declaration.File, *declaration.Archive:
|
||||
if err := refuseUnderAgentHome(r, in.agentHomes); err != nil {
|
||||
return begin(r), err
|
||||
}
|
||||
if err := refuseLinksUnderHome(r.Target()); err != nil {
|
||||
return begin(r), err
|
||||
}
|
||||
@@ -1968,6 +1973,9 @@ type inputs struct {
|
||||
// refused is every directory and access the engine will not place, by path, with why (novox/hq issue 339):
|
||||
// each fails, and a container mounting one fails with it.
|
||||
refused map[string]error
|
||||
// agentHomes is the home of every account the declaration says never becomes root (novox/hq ADR
|
||||
// 0266): nothing is unpacked or written into below one (agent_home_guard.go).
|
||||
agentHomes []string
|
||||
// dirsBefore is every declared directory that was on the machine when this apply began (novox/hq issue
|
||||
// 339): one the apply itself made — a file's parent — is the mesh's, one that was there may not be.
|
||||
dirsBefore map[string]bool
|
||||
|
||||
@@ -26,46 +26,21 @@ package apply
|
||||
// and is left as it was.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// passwdFile is the user database the homes are read from; a test names its own.
|
||||
var passwdFile = "/etc/passwd"
|
||||
|
||||
// homes is every home of a person's or an agent's account on this machine. A variable so a test can name the
|
||||
// homes it made.
|
||||
// homes is every home of a person's or an agent's account on this machine, as placement_guard.go reads them
|
||||
// from the user database. A variable so a test can name the homes it made.
|
||||
var homes = func() []string {
|
||||
f, err := os.Open(passwdFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
var out []string
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
fields := strings.Split(sc.Text(), ":")
|
||||
if len(fields) < 6 {
|
||||
continue
|
||||
}
|
||||
uid, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
home := filepath.Clean(fields[5])
|
||||
if home == "/" || home == "." || home == "" {
|
||||
continue
|
||||
}
|
||||
if (uid >= 1000 && uid != 65534) || strings.HasPrefix(home, "/home/") {
|
||||
out = append(out, home)
|
||||
}
|
||||
for home := range accountsOfHomes() {
|
||||
out = append(out, home)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user