mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/module-groups delivering: 0 of 2 delivered
mesh/delivery delivered
A module puts the operator's account in a group by declaring the account with that group alone. The node-engine now records each group it added, takes back only those when nothing declared still asks for them, refuses a group the machine lacks before usermod runs, and states each such account as its module's resource of kind account: relogin needed while the running session lacks the group.
232 lines
7.7 KiB
Go
232 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|