sudo: declare the operator account's passwordless escalation as a module
Three modules' tools act through `sudo -n` and nothing declared that the account may; each machine said so in a hand-set line in /etc/sudoers. The module owns the package and /etc/sudoers.d/10-mesh-operator (0440), checked by visudo in its manifest test, and serves sudo_rules, sudo_check and sudo_drop_ins from a Go bundle. lab stops declaring the sudo package, which would collide with this module on the node that runs both (hq ADR 0207, to-be 42 Phase 1).
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user