The journal verb ran journalctl as the operator account, which outside the journal's group sees only its own entries: every system service read '-- No entries --', and a person reached for a shell. The read now escalates with sudo -n like the acts; ported to Go with every test. hq issue 255.
295 lines
11 KiB
Go
295 lines
11 KiB
Go
package main
|
|
|
|
// The service manager's verbs over a fake runner (novox/hq ADR 0177, to-be 41 WP4), ported with the
|
|
// TypeScript module's tests: which manager a call reaches and how, acts on the system manager escalated,
|
|
// failures named rather than read as empty answers, whether the mesh declares a unit — and the system
|
|
// journal read escalated (novox/hq issue 255).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type call struct {
|
|
cmd string
|
|
args []string
|
|
env []string
|
|
}
|
|
|
|
func fake(answer func(c call) Ran, calls *[]call) Runner {
|
|
return func(cmd string, args []string, env []string) Ran {
|
|
c := call{cmd, args, env}
|
|
if calls != nil {
|
|
*calls = append(*calls, c)
|
|
}
|
|
return answer(c)
|
|
}
|
|
}
|
|
|
|
const (
|
|
list = "sshd.service loaded active running OpenSSH Daemon\nbroken.service loaded failed failed A broken thing\n"
|
|
showMesh = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=42\nExecMainStatus=0\nDescription=showcase, a mesh daemon\nFragmentPath=/etc/systemd/system/showcase.service\n"
|
|
showPackage = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=7\nExecMainStatus=0\nDescription=OpenSSH Daemon\nFragmentPath=/usr/lib/systemd/system/sshd.service\n"
|
|
)
|
|
|
|
var files = map[string]string{
|
|
"/etc/systemd/system/showcase.service": MeshUnitHeader + " Do not edit — this file is replaced whenever the\n[Unit]\n",
|
|
"/usr/lib/systemd/system/sshd.service": "[Unit]\nDescription=OpenSSH Daemon\n",
|
|
}
|
|
|
|
func read(p string) (string, error) {
|
|
if s, ok := files[p]; ok {
|
|
return s, nil
|
|
}
|
|
return "", errors.New("ENOENT")
|
|
}
|
|
|
|
func manager(run Runner, uid int, name string) *Manager {
|
|
return &Manager{Account: "operator", UID: uid, User: name, Run: run, Read: read, Env: []string{"HOME=/h"}}
|
|
}
|
|
|
|
func operator(run Runner) *Manager { return manager(run, 1000, "operator") }
|
|
|
|
func TestAnActAndASystemJournalReadGoThroughSudoUnlessRoot(t *testing.T) {
|
|
for _, verb := range []string{"start", "stop", "restart", "enable", "disable"} {
|
|
if p, _ := escalated("systemctl", []string{verb, "x.service"}, System, 1000); p != "sudo" {
|
|
t.Errorf("%s was not escalated", verb)
|
|
}
|
|
}
|
|
if p, a := escalated("systemctl", []string{"restart", "sshd.service"}, System, 1000); p != "sudo" || !reflect.DeepEqual(a, []string{"-n", "systemctl", "restart", "sshd.service"}) {
|
|
t.Errorf("%s %v", p, a)
|
|
}
|
|
if p, _ := escalated("systemctl", []string{"restart", "sshd.service"}, System, 0); p != "systemctl" {
|
|
t.Error("root escalated")
|
|
}
|
|
for _, args := range [][]string{{"show", "sshd.service"}, {"list-units", "restart"}} {
|
|
if p, _ := escalated("systemctl", args, System, 1000); p != "systemctl" {
|
|
t.Errorf("a read was escalated: %v", args)
|
|
}
|
|
}
|
|
if p, _ := escalated("systemctl", []string{"--user", "restart", "x.service"}, User, 1000); p != "systemctl" {
|
|
t.Error("the user scope was escalated")
|
|
}
|
|
// The system journal is read as root: unescalated, an account outside the journal's group sees only
|
|
// its own entries and every service's journal reads empty (issue 255).
|
|
if p, a := escalated("journalctl", []string{"-u", "x"}, System, 1000); p != "sudo" || a[1] != "journalctl" {
|
|
t.Errorf("the system journal read was not escalated: %s %v", p, a)
|
|
}
|
|
if p, _ := escalated("journalctl", []string{"--user", "-u", "x"}, User, 1000); p != "journalctl" {
|
|
t.Error("the account's own journal was escalated")
|
|
}
|
|
}
|
|
|
|
func TestTheUserScopeIsPlainUserWithTheAccountsRuntimeDirectoryAndBus(t *testing.T) {
|
|
var calls []call
|
|
m := manager(fake(func(call) Ran { return Ran{Stdout: list} }, &calls), 1234, "operator")
|
|
if _, err := m.Units(User, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls[0].cmd != "systemctl" || calls[0].args[0] != "--user" {
|
|
t.Fatalf("%+v", calls[0])
|
|
}
|
|
env := strings.Join(calls[0].env, "\n")
|
|
if !strings.Contains(env, "XDG_RUNTIME_DIR=/run/user/1234") || !strings.Contains(env, "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1234/bus") || !strings.Contains(env, "HOME=/h") {
|
|
t.Fatalf("%v", calls[0].env)
|
|
}
|
|
if _, err := m.Journal(User, "watcher.service", 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls[1].cmd != "journalctl" || calls[1].args[0] != "--user" {
|
|
t.Fatalf("%+v", calls[1])
|
|
}
|
|
}
|
|
|
|
func TestTheSystemScopeIsGivenNoSessionWords(t *testing.T) {
|
|
var calls []call
|
|
if _, err := operator(fake(func(call) Ran { return Ran{Stdout: list} }, &calls)).Units(System, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls[0].env != nil {
|
|
t.Fatalf("%v", calls[0].env)
|
|
}
|
|
for _, a := range calls[0].args {
|
|
if a == "--user" {
|
|
t.Fatal("--user in a system call")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheUserScopeFromAnotherAccountIsRefused(t *testing.T) {
|
|
m := manager(fake(func(call) Ran { return Ran{Stdout: list} }, nil), 0, "root")
|
|
if _, err := m.Units(User, ""); err == nil || !strings.Contains(err.Error(), "operator's service manager, and this runs as root") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestASystemActEscalatesAndAnswersTheStateAfter(t *testing.T) {
|
|
var calls []call
|
|
m := operator(fake(func(c call) Ran {
|
|
if contains(c.args, "show") {
|
|
return Ran{Stdout: showPackage}
|
|
}
|
|
return Ran{}
|
|
}, &calls))
|
|
r, err := m.Act(System, "restart", "sshd.service")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := append([]string{calls[0].cmd}, calls[0].args...); !reflect.DeepEqual(got, []string{"sudo", "-n", "systemctl", "restart", "sshd.service"}) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
if r["ok"] != true || r["active"] != "active" || r["mesh_declared"] != false || r["note"] != nil {
|
|
t.Fatalf("%v", r)
|
|
}
|
|
}
|
|
|
|
func TestTheRestoreNoteIsOnlyOnAUnitTheMeshDeclares(t *testing.T) {
|
|
m := operator(fake(func(c call) Ran {
|
|
if contains(c.args, "show") {
|
|
return Ran{Stdout: showMesh}
|
|
}
|
|
return Ran{}
|
|
}, nil))
|
|
r, _ := m.Act(System, "stop", "showcase.service")
|
|
if r["mesh_declared"] != true || !strings.Contains(r["note"].(string), "host restores its declared state") {
|
|
t.Fatalf("%v", r)
|
|
}
|
|
}
|
|
|
|
func TestStatusSaysWhetherTheMeshDeclaresTheUnit(t *testing.T) {
|
|
answer := func(s string) Runner { return fake(func(call) Ran { return Ran{Stdout: s} }, nil) }
|
|
mesh, _ := operator(answer(showMesh)).Status(System, "showcase.service")
|
|
pkg, _ := operator(answer(showPackage)).Status(System, "sshd.service")
|
|
none, _ := operator(answer("LoadState=not-found\nFragmentPath=\n")).Status(System, "nope.service")
|
|
if mesh["mesh_declared"] != true || mesh["MainPID"] != "42" || pkg["mesh_declared"] != false || none["mesh_declared"] != false {
|
|
t.Fatalf("%v %v %v", mesh, pkg, none)
|
|
}
|
|
if MeshUnitHeader != "# Generated by the mesh." {
|
|
t.Fatal("the header is not the one the host writes")
|
|
}
|
|
}
|
|
|
|
func TestRefusalsAreNamed(t *testing.T) {
|
|
refused := operator(fake(func(call) Ran { return Ran{Status: 1, Stderr: "sudo: a password is required\n"} }, nil))
|
|
if _, err := refused.Act(System, "start", "x.service"); err == nil || !strings.Contains(err.Error(), "may not run it without a prompt: sudo: a password is required") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
missing := operator(fake(func(call) Ran { return Ran{Status: 127, Error: "ENOENT"} }, nil))
|
|
if _, err := missing.Act(System, "start", "x.service"); err == nil || !strings.Contains(err.Error(), "sudo is not installed here") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
polkitRefused := manager(fake(func(call) Ran {
|
|
return Ran{Status: 1, Stderr: "Failed to stop x.service: Access denied as the requested operation requires interactive authentication.\n"}
|
|
}, nil), 0, "root")
|
|
if _, err := polkitRefused.Act(System, "stop", "x.service"); err == nil || !strings.Contains(err.Error(), "the service manager refused the runtime's account") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
failing := operator(fake(func(call) Ran { return Ran{Status: 1, Stderr: "Failed to list units: Connection timed out\n"} }, nil))
|
|
if _, err := failing.Units(System, ""); err == nil || !strings.Contains(err.Error(), "systemctl failed (1): Failed to list units: Connection timed out") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnUnreachableUserManagerIsSaidEvenWhenSystemctlExitsZero(t *testing.T) {
|
|
said := "Failed to connect to user scope bus via local transport: No such file or directory\n"
|
|
m := operator(fake(func(call) Ran { return Ran{Stderr: said} }, nil))
|
|
if _, err := m.Units(User, ""); err == nil || !strings.Contains(err.Error(), "operator's own service manager does not answer at /run/user/1000") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
nonzero := operator(fake(func(call) Ran { return Ran{Status: 1, Stderr: said} }, nil))
|
|
if _, err := nonzero.Status(User, "x.service"); err == nil || !strings.Contains(err.Error(), "does not answer") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestFailedReportsEachManagerAndOneThatDoesNotAnswerByItsError(t *testing.T) {
|
|
m := operator(fake(func(c call) Ran {
|
|
if c.args[0] == "--user" {
|
|
return Ran{Status: 1, Stderr: "Failed to connect to user scope bus via local transport: No such file or directory\n"}
|
|
}
|
|
return Ran{Stdout: list}
|
|
}, nil))
|
|
r := m.Failed()
|
|
system, _ := json.Marshal(r["system"])
|
|
if string(system) != `[{"unit":"broken.service","load":"loaded","active":"failed","sub":"failed","description":"A broken thing"}]` {
|
|
t.Fatalf("%s", system)
|
|
}
|
|
if e, ok := r["user"].(map[string]string); !ok || !strings.Contains(e["error"], "does not answer") {
|
|
t.Fatalf("%v", r["user"])
|
|
}
|
|
}
|
|
|
|
func TestAUnitsNameIsNeverAnOption(t *testing.T) {
|
|
for _, bad := range []string{"--host=elsewhere", "a b", ""} {
|
|
if unitArg(bad) == nil {
|
|
t.Errorf("%q accepted", bad)
|
|
}
|
|
}
|
|
var calls []call
|
|
m := operator(fake(func(call) Ran { return Ran{Stdout: list} }, &calls))
|
|
if _, err := m.Act(System, "stop", "-H"); err == nil || len(calls) != 0 {
|
|
t.Fatalf("an option ran as a unit: %v %d", err, len(calls))
|
|
}
|
|
if _, err := m.Units(System, "-x*"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a := calls[0].args; a[len(a)-2] != "--" || a[len(a)-1] != "-x*" {
|
|
t.Fatalf("%v", a)
|
|
}
|
|
}
|
|
|
|
// The manifest owns the systemd package, claims the seat's eight verbs, and lists exactly the tools served.
|
|
func TestTheManifestOwnsThePackageAndListsWhatIsServed(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Capabilities []string `json:"capabilities"`
|
|
Tools []string `json:"tools"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Resources []struct {
|
|
Type string `json:"type"`
|
|
Package string `json:"package"`
|
|
} `json:"resources"`
|
|
}
|
|
_ = json.Unmarshal(raw, &m)
|
|
owns := false
|
|
for _, r := range m.Resources {
|
|
owns = owns || (r.Type == "package" && r.Package == "systemd")
|
|
}
|
|
if !owns || !contains(m.Capabilities, "package-manager") {
|
|
t.Fatal("the manifest does not own the systemd package")
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range tools(operator(nil)) {
|
|
served[tool.Name] = true
|
|
}
|
|
want := map[string]bool{}
|
|
for _, v := range m.Claims[0].Serves {
|
|
want[seat+"."+v] = true
|
|
}
|
|
for _, n := range m.Tools {
|
|
want[n] = true
|
|
}
|
|
if !reflect.DeepEqual(served, want) {
|
|
t.Fatalf("served %v, the manifest says %v", served, want)
|
|
}
|
|
}
|
|
|
|
func contains(list []string, s string) bool {
|
|
for _, x := range list {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|