Give back only the groups the mesh added, and say when a new login is needed (hq ADR 0251, issue 247)
mesh/delivery-group group feat/module-groups checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
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 checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
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
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.
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0251 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user