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") } }