Files
Privatae 2043fa5e9e
ci / fuzz (push) Successful in 3m27s
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 3m39s
bastion 0.62.0
2026-11-02 21:31:58 +00:00

249 lines
10 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
const (
testOwnerSID = "S-1-5-21-1111111111-2222222222-3333333333-1001"
testOtherSID = "S-1-5-21-1111111111-2222222222-3333333333-1002"
)
// writeSIDOwner records sid as the machine's owner, as `approvals-owner --sid`
// writes it on Windows.
func writeSIDOwner(t *testing.T, dir, sid string) {
t.Helper()
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(approvalsOwnerRecord{SID: sid, SetAt: time.Now().UTC().Format(time.RFC3339)})
if err := os.WriteFile(filepath.Join(dir, approvalsOwnerFile), raw, 0o600); err != nil {
t.Fatal(err)
}
}
// The Windows ask pipe answers the owner's reads, acts and console for the
// account whose SID the installer recorded, and refuses every other one, in
// Windows' own commands: no sudo anywhere.
//
// MUTATION: compare p.UID in laneOwnerMatch for a Windows peer. RESULT: red
// (the owner's SID is refused).
func TestTheAskPipeServesTheOwnersSIDOnly(t *testing.T) {
dir := isolateConfigDir(t)
owner := askPeer{SID: testOwnerSID, Name: `DESKTOP\owner (` + testOwnerSID + `)`}
other := askPeer{SID: testOtherSID, Name: `DESKTOP\other (` + testOtherSID + `)`}
why := readLaneRefusalFor(owner)
if !strings.Contains(why, "approvals-owner --sid "+testOwnerSID) || !strings.Contains(why, "Administrator terminal") {
t.Errorf("with no owner recorded, the refusal does not give Windows' command: %q", why)
}
writeSIDOwner(t, dir, testOwnerSID)
if why := readLaneRefusalFor(owner); why != "" {
t.Errorf("the owner's SID was refused a read: %s", why)
}
if why := consoleLaneRefusalFor(owner); why != "" {
t.Errorf("the owner's SID was refused the console: %s", why)
}
// Case differs between a SID read from a token and one typed.
if why := readLaneRefusalFor(askPeer{SID: strings.ToLower(testOwnerSID), Name: "owner"}); why != "" {
t.Errorf("the owner's SID in other case was refused: %s", why)
}
for _, why := range []string{readLaneRefusalFor(other), consoleLaneRefusalFor(other)} {
if why == "" || !strings.Contains(why, testOwnerSID) || !strings.Contains(why, other.Name) {
t.Errorf("another account was not refused, naming both: %q", why)
}
if strings.Contains(why, "sudo") {
t.Errorf("a Windows refusal names sudo: %q", why)
}
}
if r := serveActFor(other, agentAskRequest{Act: "deny", ID: "abcd1234"}); r.OK || r.User != other.Name {
t.Errorf("another account's act: %+v", r)
}
// A record of the other platform's kind is no owner here.
uid := uint32(1000)
raw, _ := json.Marshal(approvalsOwnerRecord{UID: &uid, Account: "someone"})
if err := os.WriteFile(filepath.Join(dir, approvalsOwnerFile), raw, 0o600); err != nil {
t.Fatal(err)
}
if why := readLaneRefusalFor(owner); why == "" {
t.Error("a uid record let a Windows SID through")
}
}
// One dispatcher answers both lanes, so the pipe answers what the socket does.
func TestAskAnswerIsTheSocketsAnswer(t *testing.T) {
isolateConfigDir(t)
peer := askPeer{SID: testOwnerSID, Name: "owner"}
var extended []time.Duration
extend := func(d time.Duration) { extended = append(extended, d) }
if r, ok := askAnswer(peer, agentAskRequest{Op: "read", Read: "approvals"}, extend, func() int { return 0 }).(agentReadReply); !ok || r.OK {
t.Errorf("a read with no owner recorded: %+v", r)
}
if r, ok := askAnswer(peer, agentAskRequest{Op: "console"}, extend, func() int { return 0 }).(agentAskReply); !ok || r.OK || r.Why == "" {
t.Errorf("the console with no owner recorded: %+v", r)
}
pid := 0
if r, ok := askAnswer(peer, agentAskRequest{Op: "stop", PID: 4242}, extend, func() int { pid = 7; return 7 }).(agentAskReply); !ok || r.OK {
t.Errorf("a stop with no code set up: %+v", r)
}
if pid != 7 {
t.Error("a stop was filed without asking which process asked")
}
if len(extended) != 1 || extended[0] != readDeadline {
t.Errorf("deadlines given: %v", extended)
}
}
func TestAdminCommandsAreEachPlatformsOwn(t *testing.T) {
if got := adminCommandFor(true, "ui"); strings.Contains(got, "sudo") || !strings.Contains(got, "Administrator terminal") {
t.Errorf("windows: %q", got)
}
if got := adminCommandFor(false, "ui"); got != "sudo -H privatae-bastion ui" {
t.Errorf("unix: %q", got)
}
if !strings.Contains(askPipeSDDL, "0x12019b;;;AU") || strings.Contains(askPipeSDDL, "GW;;;AU") || strings.Contains(askPipeSDDL, "GA;;;AU") {
t.Errorf("the ask pipe's ACL gives every account more than read and write: %s", askPipeSDDL)
}
}
// A page with no session names the command that opens it on this machine.
// The machine's console is not opened by "privatae-bastion ui" from just any
// account, and telling a person only that sent them back to a 401.
func TestNotSignedInNamesTheCommandThatOpensThisConsole(t *testing.T) {
if got := notSignedIn("ui", "windows", true); !strings.Contains(got, "Administrator terminal") ||
!strings.Contains(got, "owns this machine") || strings.Contains(got, "sudo") {
t.Errorf("the Windows service's console: %q", got)
}
if got := notSignedIn("ui", "linux", true); !strings.Contains(got, "sudo -H privatae-bastion ui") ||
!strings.Contains(got, "owns this machine") {
t.Errorf("the root bastion's console: %q", got)
}
if got := notSignedIn("ui", "windows", false); !strings.Contains(got, "`privatae-bastion ui`") || strings.Contains(got, "Administrator") {
t.Errorf("an account's own bastion: %q", got)
}
err := uiKeyRefused("127.0.0.1:7880", "401 Unauthorized", "windows", true, false)
if s := err.Error(); !strings.Contains(s, winServiceName) || strings.Contains(s, "sudo") || strings.Contains(s, "restart it") {
t.Errorf("a 401 beside the Windows service: %q", s)
}
}
// A second start says what is running and how to reach it, and never that it
// should be ended: ending it was what the old words sent a person to do.
//
// MUTATION: drop the PID from secondRunWords. RESULT: red.
func TestASecondStartNamesTheRunningBastion(t *testing.T) {
task := runHolder{PID: 4242, Program: `C:\Users\someone\AppData\Local\Programs\privatae-bastion\privatae-bastion.exe`,
Started: "2026-11-02T18:02:00Z"}
console, dialog := secondRunWords(task, true)
for _, want := range []string{"4242", task.Program, "privatae-bastion ui", "Stop-Process -Id 4242"} {
if !strings.Contains(console, want) || !strings.Contains(dialog.Body, want) {
t.Errorf("the refusal does not say %q:\n%s\n%s", want, console, dialog.Body)
}
}
svc, _ := secondRunWords(runHolder{PID: 9, Service: true}, true)
if !strings.Contains(svc, "as the "+winServiceName+" service") || !strings.Contains(svc, "Restart-Service "+winServiceName) {
t.Errorf("the service: %s", svc)
}
old, _ := secondRunWords(runHolder{}, false)
if !strings.Contains(old, "Get-Process privatae-bastion") {
t.Errorf("an older bastion holding the lock: %s", old)
}
for _, s := range []string{console, svc, old, dialog.Body} {
if strings.Contains(s, "Task Manager") || strings.Contains(s, "believe the running one is dead") {
t.Errorf("the refusal sends the person to end the running bastion: %s", s)
}
}
dir := t.TempDir()
writeRunHolder(dir, true)
if h, ok := readRunHolder(dir); !ok || h.PID != os.Getpid() || !h.Console {
t.Errorf("the holder record: %+v, %v", h, ok)
}
}
// A bastion relaunched by its predecessor's update waits for it to exit and
// then takes the lock, where it used to refuse as a second copy and leave the
// machine with none.
func TestARelaunchedBastionWaitsForThePredecessorsLock(t *testing.T) {
clock := time.Unix(0, 0)
now := func() time.Time { return clock }
sleep := func(d time.Duration) { clock = clock.Add(d) }
held := 3
try := func() error {
if held > 0 {
held--
return os.ErrExist
}
return nil
}
waited := 0
waitExit := func(pid int, d time.Duration) { waited = pid }
if err := takeRunLock(try, 4242, waitExit, 30*time.Second, now, sleep); err != nil {
t.Fatalf("the lock was not taken once the predecessor let go: %v", err)
}
if waited != 4242 {
t.Errorf("waited on process %d", waited)
}
held = 1 << 30
if err := takeRunLock(try, 4242, waitExit, 30*time.Second, now, sleep); err == nil {
t.Fatal("a lock held for ever was taken")
}
held, waited = 1, 0
if err := takeRunLock(try, 0, waitExit, 30*time.Second, now, sleep); err == nil || waited != 0 {
t.Errorf("a start that replaces nothing waited, or retried: %v, %d", err, waited)
}
t.Setenv(relaunchOfEnv, "4242")
if got := relaunchOf(); got != 4242 {
t.Errorf("relaunchOf = %d", got)
}
if _, set := os.LookupEnv(relaunchOfEnv); set {
t.Error("the predecessor's pid would pass on to what this bastion starts")
}
}
// The Windows files wire it together: they are not built on Linux, so this
// reads them, as TestServiceModeOpensNothingOnTheDesktop does.
func TestTheWindowsFilesWireTheLaneTheCarryAndTheRelaunchWait(t *testing.T) {
for file, wants := range map[string][]string{
"relaunch_windows.go": {`relaunchOfEnv+"="+strconv.Itoa(os.Getpid())`},
"runguard_windows.go": {"takeRunLock(", "relaunchOf()", "writeRunHolder(d,", "secondRunWords("},
"sysservice_windows.go": {"carryStateInto(from, to)", "serviceCarrySource(serviceCarryFromSID)"},
"asklane_windows.go": {"askAnswer(peer, req", "pipeServerIsService(c.h)", "askPipeSDDL"},
"agentask.go": {"startAskPipe(done)"},
"../deploy/install.ps1": {"service install --from-account $owner"},
} {
src, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for _, w := range wants {
if !strings.Contains(string(src), w) {
t.Errorf("%s no longer has %s", file, w)
}
}
}
// The carry runs before the service's first start, not after it.
src, _ := os.ReadFile("sysservice_windows.go")
s := string(src)
if c, st := strings.Index(s, "carryStateInto(from, to)"), strings.Index(s, "s.Start()"); c < 0 || st < 0 || c > st {
t.Error("the carry is not before the service starts, so the service would start with no pairing")
}
}
// A process that started after the connection was made is not the one that
// made it: its id was freed and reused, and answering it would answer a
// stranger as whoever holds that id now.
func TestAPipeClientStartedAfterItsConnectionIsNotTheCaller(t *testing.T) {
connected := time.Unix(1000, 0)
if !pipeClientNewer(connected.Add(time.Millisecond), connected) {
t.Error("a process created after the connection was taken for the caller")
}
if pipeClientNewer(connected.Add(-time.Hour), connected) {
t.Error("the process that connected was refused")
}
}