Files
mesh-catalog/modules/sudo/cmd/sudo-tools/escalation.go
T

90 lines
3.0 KiB
Go

package main
// Who can become root here without a person (novox/hq ADR 0259 §8): the controller's self-check asks it of
// every machine where the router or a channel proving its sender runs, for the accounts agents run as and
// the account the machine's runtime runs commands as.
//
// Root without a person is any of: a sudo rule letting the account run every command as root with no
// password; membership of a group that is root by another name (docker, which mounts the host; disk, which
// reads it raw; wheel or sudo only where a rule grants them without a password, which the listing shows); or
// the account being root itself.
import (
"fmt"
"regexp"
"strings"
)
// Escalation is one account's answer.
type Escalation struct {
Account string `json:"account"`
Root bool `json:"root_without_a_person"`
Why string `json:"why"`
}
// rootGroups are groups that are root by another name.
var rootGroups = map[string]string{"docker": "the docker group, which can mount the machine's root file system",
"disk": "the disk group, which reads and writes the drives raw", "root": "the root group"}
var accountShape = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`)
// Escalations answers, for each account, whether it can become root here without a person.
func (m *Machine) Escalations(accounts []string) ([]Escalation, error) {
if len(accounts) == 0 {
return nil, fmt.Errorf("name the accounts to judge")
}
var out []Escalation
for _, a := range accounts {
if !accountShape.MatchString(a) {
return nil, fmt.Errorf("%q is not an account name", a)
}
out = append(out, m.escalation(a))
}
return out, nil
}
func (m *Machine) escalation(account string) Escalation {
e := Escalation{Account: account}
if account == "root" {
e.Root, e.Why = true, "it is root"
return e
}
id := m.Run(bg(), "id", "-nG", account)
if id.Status != 0 || id.Err != "" {
e.Why = "no such account here"
return e
}
for _, g := range strings.Fields(id.Stdout) {
if why, ok := rootGroups[g]; ok {
e.Root, e.Why = true, "a member of "+why
return e
}
}
// sudo's own word, for that account: listing another account's rules needs root, which the runtime's
// account escalates to as the mesh's acting tools do.
r := m.Run(bg(), "sudo", "-n", "-l", "-U", account)
if r.Status != 0 || r.Err != "" {
if strings.Contains(r.Stdout+r.Stderr, "not allowed to run sudo") {
e.Why = "no sudo rule names it"
return e
}
e.Why = "sudo could not be asked: " + strings.TrimSpace(firstLine(r.Stderr+r.Err))
return e
}
if strings.Contains(r.Stdout, "is not allowed to run sudo") {
e.Why = "no sudo rule names it"
return e
}
rules := ParseList(r.Stdout, account)
if rules.PasswordlessAll {
for _, rule := range rules.Rules {
if hasTag(rule.Tags, "NOPASSWD") && contains(rule.Commands, "ALL") && runsAsRoot(rule.RunAs) {
e.Root, e.Why = true, "sudo lets it run every command as root without a password ("+rule.Line+")"
return e
}
}
}
e.Why = "sudo asks it for a password, or lets it run named commands only"
return e
}