package main import ( "strings" "testing" ) const listNovox = `Matching Defaults entries for operator on anchor: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/bin User operator may run the following commands on anchor: (ALL) NOPASSWD: ALL (root) SETENV: NOPASSWD: /usr/bin/pacman, /usr/bin/systemctl ` func TestSudoListIsParsedIntoDefaultsAndRules(t *testing.T) { r := ParseList(listNovox, "x") if r.Account != "operator" || r.Host != "anchor" { t.Fatalf("who: %+v", r) } if len(r.Defaults) != 3 || r.Defaults[0] != "env_reset" { t.Fatalf("defaults: %v", r.Defaults) } if len(r.Rules) != 2 || r.Rules[0].RunAs != "ALL" || strings.Join(r.Rules[0].Tags, ",") != "NOPASSWD" || r.Rules[0].Commands[0] != "ALL" { t.Fatalf("first rule: %+v", r.Rules) } if strings.Join(r.Rules[1].Tags, ",") != "SETENV,NOPASSWD" || len(r.Rules[1].Commands) != 2 { t.Fatalf("second rule: %+v", r.Rules[1]) } if !r.PasswordlessAll { t.Fatal("(ALL) NOPASSWD: ALL is passwordless escalation") } only := ParseList("User operator may run the following commands on h:\n (ALL : ALL) ALL\n", "x") if only.PasswordlessAll { t.Fatal("a rule that asks for a password is not passwordless") } } func TestListingThatNeedsAPasswordIsAnError(t *testing.T) { m := machine(fake(func(call) Ran { return Ran{Status: 1, Stderr: "sudo: a password is required\n"} }, nil), 1000) if _, err := m.ListRules(); err == nil || !strings.Contains(err.Error(), "a password is required") { t.Fatalf("got %v", err) } } const mainSudoers = `## sudoers file. root ALL=(ALL:ALL) ALL %wheel ALL=(ALL:ALL) NOPASSWD: ALL #includedir is spelled with @ these days @includedir /etc/sudoers.d operator ALL=(ALL) \ ALL ` func sudoersMachine(uid int, calls *[]call) *Machine { return machine(byLine(map[string]Ran{ "sudo -n true": {}, "id -nG operator": {Stdout: "users wheel docker\n"}, "sudo -n cat /etc/sudoers": {Stdout: mainSudoers}, "sudo -n find /etc/sudoers.d -mindepth 1 -maxdepth 1 -type f -printf %f\n": {Stdout: "10-mesh-operator\nold.pacsave\n"}, "sudo -n cat /etc/sudoers.d/10-mesh-operator": {Stdout: "# The mesh's\noperator ALL=(ALL:ALL) NOPASSWD: ALL\n"}, }, calls), uid) } func TestCheckFindsEveryGrantInReadingOrderAndTheOneThatDecides(t *testing.T) { var calls []call c, err := sudoersMachine(1000, &calls).CheckEscalation() if err != nil { t.Fatal(err) } if !c.Passwordless || c.Refusal != "" { t.Fatalf("escalation: %+v", c) } got := []string{} for _, g := range c.Grants { got = append(got, g.File+":"+g.Who) } want := "/etc/sudoers:%wheel /etc/sudoers.d/10-mesh-operator:operator /etc/sudoers:operator" if strings.Join(got, " ") != want { t.Fatalf("grants in order: %v", got) } if c.DecidedBy == nil || c.DecidedBy.File != "/etc/sudoers" || c.DecidedBy.NoPasswd || c.DecidedBy.Line != 6 { t.Fatalf("the last rule sudo reads decides, joined across its continuation: %+v", c.DecidedBy) } if !c.MeshDropIn.Present || !c.MeshDropIn.Grants { t.Fatalf("the module's drop-in: %+v", c.MeshDropIn) } for _, cl := range calls { if strings.Contains(cl.String(), "old.pacsave") { t.Fatal("a file sudo skips was read as a rule") } } } func TestARefusedEscalationIsSaidAndNothingIsReadAsNoGrant(t *testing.T) { m := machine(fake(func(c call) Ran { if c.String() == "sudo -n true" { return Ran{Status: 1, Stderr: "sudo: a password is required\n"} } if c.name == "id" { return Ran{Stdout: "users\n"} } t.Fatalf("read %s after a refusal", c) return Ran{} }, nil), 1000) c, err := m.CheckEscalation() if err != nil { t.Fatal(err) } if c.Passwordless || c.Refusal != "sudo: a password is required" || !strings.Contains(c.Note, "not read") { t.Fatalf("%+v", c) } } func TestSudoSkipsDottedAndBackupNames(t *testing.T) { for name, want := range map[string]bool{"10-mesh-operator": true, "old.pacsave": false, "rule~": false, "README": true} { if ReadBySudo(name) != want { t.Errorf("%s: %v", name, !want) } } } func TestDropInsAreListedWithWhetherSudoReadsAndParsesEach(t *testing.T) { m := machine(byLine(map[string]Ran{ "sudo -n find /etc/sudoers.d -mindepth 1 -maxdepth 1 -printf %f\t%y\t%u\t%g\t%m\t%s\n": {Stdout: "10-mesh-operator\tf\troot\troot\t440\t120\nbroken\tf\troot\troot\t440\t9\nloose\tf\toperator\troot\t644\t3\nx.bak\tf\troot\troot\t640\t3\n"}, "sudo -n visudo -c -f /etc/sudoers.d/10-mesh-operator": {Stdout: "/etc/sudoers.d/10-mesh-operator: parsed OK\n"}, "sudo -n visudo -c -f /etc/sudoers.d/broken": {Status: 1, Stderr: "/etc/sudoers.d/broken:1:5: syntax error\n"}, "sudo -n visudo -c -f /etc/sudoers.d/loose": {Stdout: "parsed OK\n"}, "sudo -n visudo -c -f /etc/sudoers.d/x.bak": {Stdout: "parsed OK\n"}, "sudo -n visudo -c": {Status: 1, Stdout: "/etc/sudoers: parsed OK\n", Stderr: "/etc/sudoers.d/broken:1:5: syntax error\n"}, }, nil), 1000) d, err := m.ListDropIns() if err != nil { t.Fatal(err) } by := map[string]DropIn{} for _, e := range d.Entries { by[e.Name] = e } if e := by["10-mesh-operator"]; !e.Mesh || !e.ReadBySudo || e.Parses == nil || !*e.Parses || e.Mode != "0440" { t.Fatalf("the mesh's: %+v", e) } if e := by["broken"]; e.Parses == nil || *e.Parses || !strings.Contains(e.Error, "syntax error") { t.Fatalf("broken: %+v", e) } if e := by["loose"]; e.ReadBySudo || !strings.Contains(e.Why, "owned by root") { t.Fatalf("loose: %+v", e) } if e := by["x.bak"]; e.ReadBySudo || !strings.Contains(e.Why, "dot") { t.Fatalf("x.bak: %+v", e) } if d.SudoersParses || len(d.SudoersSaid) != 2 { t.Fatalf("the whole: %+v", d) } }