package apply import ( "context" "errors" "sort" "strings" "testing" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0252 and issue 247: a module puts the operator's account in a group by declaring // the account with that group alone; the account's other groups are never touched; a new login is said; // and the mesh gives back only a group it put the account in, and only when nobody declared still asks. // groupDB is a fake user and group database: the account's groups, the groups the machine has, and every // command it was asked. type groupDB struct { account string in map[string]bool exists map[string]bool asked []string } func newGroupDB(in []string, exists ...string) *groupDB { g := &groupDB{account: "operator", in: map[string]bool{}, exists: map[string]bool{}} for _, x := range in { g.in[x], g.exists[x] = true, true } for _, x := range exists { g.exists[x] = true } return g } func (g *groupDB) run(_ context.Context, name string, args ...string) (string, error) { g.asked = append(g.asked, name+" "+strings.Join(args, " ")) switch { case name == "getent" && args[0] == "passwd": if args[1] == g.account { return g.account + ":x:1500:1500::/home/" + g.account + ":/bin/bash\n", nil } return "", errors.New("getent exited 2: ") case name == "getent" && args[0] == "group": if g.exists[args[1]] { return args[1] + ":x:900:\n", nil } return "", errors.New("getent exited 2: ") case name == "id": var out []string for x := range g.in { out = append(out, x) } sort.Strings(out) return strings.Join(out, " ") + "\n", nil case name == "usermod" && args[0] == "--append": if !g.exists[args[2]] { return "", errors.New("usermod exited 6: group '" + args[2] + "' does not exist") } g.in[args[2]] = true case name == "gpasswd" && args[0] == "--delete": delete(g.in, args[2]) } return "", nil } func (g *groupDB) groups() string { var out []string for x := range g.in { out = append(out, x) } sort.Strings(out) return strings.Join(out, " ") } func applyGroupsOf(t *testing.T, g *groupDB, known store.State, resources ...string) (Report, store.State, error) { t.Helper() if len(resources) == 0 { resources = []string{`{"id":"other.dir","type":"directory","path":"` + t.TempDir() + `/other"}`} } return Apply(context.Background(), archHost(t), parse(t, `{"declaration":1,"resources":[`+ strings.Join(resources, ",")+`]}`), known, store.OriginDeclared, g.run, nil, nil) } const ( razer = `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer"]}` docker = `{"id":"docker.account","type":"user","name":"operator","groups":["docker"]}` shell = `{"id":"zsh.login","type":"user","name":"operator","groups":[]}` ) func TestAModulePutsTheAccountInAGroupAndSaysANewLoginIsNeeded(t *testing.T) { g := newGroupDB([]string{"wheel", "plugdev"}, "openrazer") report, state, err := applyGroupsOf(t, g, store.State{}, shell, razer) if err != nil { t.Fatal(err) } if got := g.groups(); got != "openrazer plugdev wheel" { t.Fatalf("the account's groups are %q", got) } o := outcomeOf(report, "openrazer.account") if o.Action != "updated" || !strings.Contains(o.Detail, "put in openrazer") || !strings.Contains(o.Detail, "new login") { t.Errorf("the outcome did not say the group and the new login: %+v", o) } a, _ := state.Find("openrazer.account") if strings.Join(a.Groups, " ") != "openrazer" { t.Errorf("the record holds %v, want the one group the mesh added", a.Groups) } for _, asked := range g.asked { if strings.HasPrefix(asked, "usermod") && !strings.HasPrefix(asked, "usermod --append --groups openrazer ") { t.Errorf("usermod was asked something other than appending the one group: %q", asked) } } // Applied again: nothing to do, and the record still says the mesh added it. report, state, err = applyGroupsOf(t, g, state, shell, razer) if err != nil { t.Fatal(err) } if o := outcomeOf(report, "openrazer.account"); o.Action != "unchanged" { t.Errorf("a second apply changed something: %+v", o) } if a, _ := state.Find("openrazer.account"); strings.Join(a.Groups, " ") != "openrazer" { t.Errorf("a second apply lost what the mesh added: %v", a.Groups) } } func TestAGroupTheAccountWasAlreadyInIsNeverTakenBack(t *testing.T) { g := newGroupDB([]string{"wheel", "openrazer"}) _, state, err := applyGroupsOf(t, g, store.State{}, razer) if err != nil { t.Fatal(err) } if a, _ := state.Find("openrazer.account"); len(a.Groups) != 0 { t.Fatalf("a group found was recorded as the mesh's: %v", a.Groups) } if _, _, err := applyGroupsOf(t, g, state); err != nil { t.Fatal(err) } if got := g.groups(); got != "openrazer wheel" { t.Errorf("undeclaring took a found group: %q", got) } } func TestTheGroupTheMeshAddedIsGivenBackWhenItsModuleGoes(t *testing.T) { g := newGroupDB([]string{"wheel"}, "openrazer") _, state, err := applyGroupsOf(t, g, store.State{}, razer) if err != nil { t.Fatal(err) } report, state, err := applyGroupsOf(t, g, state) if err != nil { t.Fatal(err) } if got := g.groups(); got != "wheel" { t.Errorf("the account's groups after its module went: %q, want wheel alone", got) } o := outcomeOf(report, "openrazer.account") if o.Action != "restored" || !strings.Contains(o.Detail, "taken out of openrazer") { t.Errorf("the removal did not say the group was given back: %+v", o) } if _, still := state.Find("openrazer.account"); still { t.Error("the record stayed") } } func TestAGroupAnotherResourceStillAsksForStays(t *testing.T) { both := `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer","video"]}` video := `{"id":"media.account","type":"user","name":"operator","groups":["video"]}` g := newGroupDB(nil, "openrazer", "video") _, state, err := applyGroupsOf(t, g, store.State{}, both, video) if err != nil { t.Fatal(err) } report, _, err := applyGroupsOf(t, g, state, video) if err != nil { t.Fatal(err) } if got := g.groups(); got != "video" { t.Errorf("the account's groups: %q, want video kept for media and openrazer given back", got) } if o := outcomeOf(report, "openrazer.account"); !strings.Contains(o.Detail, "media.account still asks for") { t.Errorf("the removal did not say why video stayed: %+v", o) } } func TestAGroupNoLongerDeclaredIsGivenBackWhileTheAccountStaysDeclared(t *testing.T) { g := newGroupDB(nil, "openrazer", "input") two := `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer","input"]}` _, state, err := applyGroupsOf(t, g, store.State{}, two) if err != nil { t.Fatal(err) } _, state, err = applyGroupsOf(t, g, state, razer) if err != nil { t.Fatal(err) } if got := g.groups(); got != "openrazer" { t.Errorf("the account's groups: %q, want input given back", got) } if a, _ := state.Find("openrazer.account"); strings.Join(a.Groups, " ") != "openrazer" { t.Errorf("the record holds %v", a.Groups) } } func TestAGroupThatDoesNotExistYetFailsTheResourceSayingSo(t *testing.T) { g := newGroupDB([]string{"wheel"}) _, _, err := applyGroupsOf(t, g, store.State{}, razer) if err == nil || !strings.Contains(err.Error(), "no such group yet") { t.Fatalf("a missing group was not said: %v", err) } for _, asked := range g.asked { if strings.HasPrefix(asked, "usermod") { t.Errorf("usermod was run for a group the machine does not have: %q", asked) } } } func TestAGroupAPersonTookTheAccountOutOfSinceIsPutBackWhileDeclared(t *testing.T) { g := newGroupDB(nil, "openrazer") _, state, err := applyGroupsOf(t, g, store.State{}, razer) if err != nil { t.Fatal(err) } delete(g.in, "openrazer") if _, _, err := applyGroupsOf(t, g, state, razer); err != nil { t.Fatal(err) } if got := g.groups(); got != "openrazer" { t.Errorf("a declared group was not put back: %q", got) } }