Files
mesh-host/internal/apply/contract_test.go
T
jschoubben aad33dfe5c 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.
2026-10-12 19:51:09 +02:00

129 lines
4.8 KiB
Go

//go:build linux
package apply
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/store"
)
// The contract the controller needs (novox/hq ADR 0325, issue 567): what it cannot know is read here, and what it
// names is matched as it names it.
// accountsHere stands in for the machine's user database: name → uid, gid.
func accountsHere(t *testing.T, known map[string][2]int) {
t.Helper()
was := AccountIDs
AccountIDs = func(_ context.Context, name string) (int, int, bool, error) {
ids, ok := known[name]
return ids[0], ids[1], ok, nil
}
t.Cleanup(func() { AccountIDs = was })
}
// A tool runner's account comes with its name alone: its uid is read here, and its entry kept with it.
func TestAToolRunnersAccountIsReadByName(t *testing.T) {
dir := aclFolder(t)
keeping(t, nil, nil)
accountsHere(t, map[string][2]int{"nox": {runner, 1000}})
lib := filepath.Join(dir, "books")
if err := os.Mkdir(lib, 0o755); err != nil {
t.Fatal(err)
}
a := `{"id":"importer.books","type":"access","path":"` + lib + `","mode":"read-write","users":[` +
`{"identity":"nox","use":"read-write","from":"tool-runner"}]}`
if _, _, err := applyAccesses(t, store.State{}, a); err != nil {
t.Fatal(err)
}
if got := effectiveOn(t, lib, runner); got != 7 {
t.Fatalf("the tool runner's account read by name may %s", permWords(got))
}
if rec, _ := keptRecord(lib); len(rec.Want) != 1 || rec.Want[0].UID != runner {
t.Errorf("the uid read is not what is kept: %+v", rec.Want)
}
}
// One that is not here, or is root's, refuses its access, naming why, and gives nothing an entry.
func TestAToolRunnersAccountThatIsNotHereOrIsRootsIsRefused(t *testing.T) {
was := RootGroupIDs
RootGroupIDs = func() map[int]string { return map[int]string{10: "wheel"} }
defer func() { RootGroupIDs = was }()
for name, says := range map[string]string{"ghost": "not on this machine", "root": "never root",
"admin": "root group"} {
dir := aclFolder(t)
keeping(t, nil, nil)
accountsHere(t, map[string][2]int{"root": {0, 0}, "admin": {1001, 10}})
lib := filepath.Join(dir, "books")
if err := os.Mkdir(lib, 0o755); err != nil {
t.Fatal(err)
}
a := `{"id":"importer.books","type":"access","path":"` + lib + `","mode":"read-write","users":[` +
`{"identity":"` + name + `","use":"read-write","from":"tool-runner"}]}`
_, _, err := applyAccesses(t, store.State{}, a)
if err == nil || !strings.Contains(err.Error(), says) {
t.Errorf("%s: not refused saying %q: %v", name, says, err)
}
if n := len(readACLsOrNil(t, lib)); n > 3 {
t.Errorf("%s: an entry was kept", name)
}
}
// And only a tool runner's account may come without a number.
if _, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"m.a","type":"access","path":"/srv/x",` +
`"mode":"read-write","users":[{"identity":"mesh-m","use":"read","from":"container"}]}]}`)); err == nil {
t.Error("a container's identity without a uid was accepted")
}
}
func readACLsOrNil(t *testing.T, path string) acl {
a, _, _ := readACLs(t, path)
return a
}
// The take act names its access by the declared resource id exactly, `/` and all; anything not declared is refused.
func TestTheTakeActMatchesTheDeclaredAccessId(t *testing.T) {
dir := aclFolder(t)
keeping(t, nil, nil)
lib := filepath.Join(dir, "movies")
if err := os.MkdirAll(filepath.Join(lib, "a film"), 0o755); err != nil {
t.Fatal(err)
}
id := "radarr.access-services/media/movies"
a := `{"id":"` + id + `","type":"access","path":"` + lib + `","mode":"read-write","users":[` +
`{"identity":"mesh-radarr","uid":61001,"use":"read-write","from":"container"}]}`
toUser(t)
if _, _, err := TakeFolder(context.Background(), declare(t, a), "radarr.access-services/media", lib, "w", "op",
noServices); err == nil {
t.Fatal("an access not declared was taken")
}
toUser(t)
done, _, err := TakeFolder(context.Background(), declare(t, a), id, lib, "w", "op", noServices)
if err != nil {
t.Fatalf("the declared id with a slash was refused: %v", err)
}
<-done
}
// The words a keep wait says of ACLs: only a ZFS dataset is one the take act lets carry them.
func TestAKeepWaitSaysWhetherTheTakeActCanAllowACLs(t *testing.T) {
if aclWord("tank/media", "zfs") != ACLNeedsEnabling || aclWord("/dev/sdb1", "ext4") != ACLUnsupported ||
aclWord("", "zfs") != ACLUnsupported {
t.Fatal("the ACL words are not what the take act can do")
}
dir := aclFolder(t) // carries ACLs: the wait says nothing of them
keeping(t, nil, nil)
lib := filepath.Join(dir, "music")
if err := os.MkdirAll(filepath.Join(lib, "album"), 0o755); err != nil {
t.Fatal(err)
}
toUser(t)
if v := AccessVerdict(context.Background(), lib, false, true); v.Kind != VerdictWaitKeep || v.ACL != "" {
t.Fatalf("a folder whose filesystem allows ACLs is said %+v", v)
}
}