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.
129 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|