230 lines
8.5 KiB
Go
230 lines
8.5 KiB
Go
package main
|
|
|
|
// uninstall.go — `privatae-bastion uninstall [--delete-data]`.
|
|
//
|
|
// Removes what the installers put in place: the service that starts the
|
|
// bastion (the systemd unit, the launchd daemon, the Windows service, or the
|
|
// account's own user unit, LaunchAgent or logon task) and the program. The
|
|
// bastion's state stays unless --delete-data is given: the pairings and keys,
|
|
// and the audit log, which is the record of what every Host and agent did on
|
|
// this machine and is the one thing a person may need after the bastion is
|
|
// gone. --delete-data takes a typed confirmation at a terminal.
|
|
//
|
|
// Installed for the whole machine, uninstalling needs the machine's
|
|
// administrator, as stopping the service does. The steps are decided in
|
|
// uninstallSteps, untagged, so the suite checks every platform's on any.
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type uninstallStep struct {
|
|
Run []string // a command to run; its failure is reported and the rest go on
|
|
Remove string // a file to remove; one already gone is fine
|
|
Call string // "system-service": uninstallSystemService (macOS, Windows)
|
|
Say string // printed as it is done
|
|
}
|
|
|
|
// uninstallSteps is what uninstalling removes on goos. whole: the bastion is
|
|
// installed for the whole machine.
|
|
func uninstallSteps(goos string, whole bool, home string, uid int) []uninstallStep {
|
|
var s []uninstallStep
|
|
switch goos {
|
|
case "linux":
|
|
if whole {
|
|
s = append(s,
|
|
uninstallStep{Run: []string{"systemctl", "disable", "--now", "privatae-bastion"}, Say: "stopped the system service privatae-bastion"},
|
|
uninstallStep{Remove: linuxSystemUnitPath},
|
|
uninstallStep{Run: []string{"systemctl", "daemon-reload"}},
|
|
uninstallStep{Remove: "/usr/local/bin/privatae-bastion", Say: "removed /usr/local/bin/privatae-bastion"})
|
|
} else {
|
|
unit := filepath.Join(home, ".config", "systemd", "user", "privatae-bastion.service")
|
|
if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
|
|
unit = filepath.Join(x, "systemd", "user", "privatae-bastion.service")
|
|
}
|
|
s = append(s,
|
|
uninstallStep{Run: []string{"systemctl", "--user", "disable", "--now", "privatae-bastion"}, Say: "stopped your own service privatae-bastion"},
|
|
uninstallStep{Remove: unit},
|
|
uninstallStep{Run: []string{"systemctl", "--user", "daemon-reload"}},
|
|
uninstallStep{Remove: filepath.Join(home, ".local", "bin", "privatae-bastion")})
|
|
}
|
|
case "darwin":
|
|
if whole {
|
|
s = append(s,
|
|
uninstallStep{Call: "system-service"},
|
|
uninstallStep{Remove: "/Library/Application Support/privatae/bin/privatae-bastion", Say: "removed the program"},
|
|
uninstallStep{Remove: "/usr/local/bin/privatae-bastion"})
|
|
} else {
|
|
s = append(s,
|
|
uninstallStep{Run: []string{"/bin/launchctl", "bootout", "gui/" + strconv.Itoa(uid) + "/ai.privatae.bastion.user"}, Say: "stopped your own LaunchAgent"},
|
|
uninstallStep{Remove: filepath.Join(home, "Library", "LaunchAgents", "ai.privatae.bastion.user.plist")},
|
|
uninstallStep{Remove: filepath.Join(home, ".local", "bin", "privatae-bastion")})
|
|
}
|
|
case "windows":
|
|
if whole {
|
|
s = append(s, uninstallStep{Call: "system-service"})
|
|
} else {
|
|
s = append(s,
|
|
uninstallStep{Run: []string{"schtasks", "/End", "/TN", "PrivataeBastion"}},
|
|
uninstallStep{Run: []string{"schtasks", "/Delete", "/TN", "PrivataeBastion", "/F"}, Say: "removed your sign-in task PrivataeBastion"})
|
|
}
|
|
}
|
|
// The program is removed only from the installers' own places, never from
|
|
// wherever else someone keeps a copy. On Windows a running program cannot
|
|
// be removed; cmdUninstall says where it is.
|
|
return s
|
|
}
|
|
|
|
// uninstallWhole reports whether this machine's bastion is installed for the
|
|
// whole machine.
|
|
func uninstallWhole() bool {
|
|
if runtime.GOOS == "linux" {
|
|
return linuxSystemUnitPresent()
|
|
}
|
|
return systemService.installed
|
|
}
|
|
|
|
// uninstallHomeRefusal: on Linux, root's state is found through root's HOME
|
|
// (the unit runs with HOME=/root), and a sudo that keeps the caller's HOME (no
|
|
// -H) or XDG_CONFIG_HOME (sudo -E, env_keep) would name the person's own state
|
|
// as the machine's, and with --delete-data delete it. dataDir is the folder
|
|
// this process would name. "" when fine.
|
|
func uninstallHomeRefusal(goos string, root bool, dataDir, rootHome string) string {
|
|
if goos != "linux" || !root || rootHome == "" ||
|
|
filepath.Clean(dataDir) == filepath.Join(rootHome, ".config", "privatae-bastion") {
|
|
return ""
|
|
}
|
|
return "run it as: sudo -H privatae-bastion uninstall, with no XDG_CONFIG_HOME kept. This sudo kept your HOME or XDG_CONFIG_HOME, " +
|
|
"so the state it would name (" + dataDir + ") is your account's, not the machine's — nothing was removed"
|
|
}
|
|
|
|
// uninstallRootStateRefusal is uninstallHomeRefusal for this process.
|
|
func uninstallRootStateRefusal() string {
|
|
if !machineAuthorityHeld() {
|
|
return ""
|
|
}
|
|
rootHome := ""
|
|
if u, err := user.LookupId("0"); err == nil {
|
|
rootHome = u.HomeDir
|
|
}
|
|
dataDir, _ := configDirPath()
|
|
return uninstallHomeRefusal(runtime.GOOS, true, dataDir, rootHome)
|
|
}
|
|
|
|
func cmdUninstall(deleteData bool) error {
|
|
if why := uninstallRootStateRefusal(); why != "" {
|
|
return errors.New(why)
|
|
}
|
|
// From an account that does not administer the machine, uninstall
|
|
// removes that account's own bastion and leaves the machine's alone.
|
|
machine := uninstallWhole()
|
|
whole := machine && machineAuthorityHeld()
|
|
if machine && !whole && !accountInstallPresent() {
|
|
return requireMachineAuthority("uninstall")
|
|
}
|
|
dataDir, err := configDirPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if deleteData && !uninstallConfirm(dataDir) {
|
|
return errors.New("nothing was removed")
|
|
}
|
|
home, _ := os.UserHomeDir()
|
|
self, _ := resolvedSelf()
|
|
cfgs, _ := loadPairings()
|
|
hadHosts := len(cfgs) > 0
|
|
failed := 0
|
|
for _, st := range uninstallSteps(runtime.GOOS, whole, home, os.Getuid()) {
|
|
switch {
|
|
case st.Call == "system-service":
|
|
if err := uninstallSystemService(); err != nil {
|
|
fmt.Fprintf(os.Stderr, " %v\n", err)
|
|
failed++
|
|
}
|
|
case len(st.Run) > 0:
|
|
cmd := exec.Command(st.Run[0], st.Run[1:]...)
|
|
hideConsole(cmd)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil && st.Say != "" {
|
|
fmt.Fprintf(os.Stderr, " %s: %v %s\n", strings.Join(st.Run, " "), err, strings.TrimSpace(string(out)))
|
|
failed++
|
|
continue
|
|
}
|
|
case st.Remove != "":
|
|
if err := os.Remove(st.Remove); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
fmt.Fprintf(os.Stderr, " could not remove %s: %v\n", st.Remove, err)
|
|
failed++
|
|
continue
|
|
}
|
|
}
|
|
if st.Say != "" {
|
|
fmt.Println(" " + st.Say)
|
|
}
|
|
}
|
|
if deleteData {
|
|
if err := os.RemoveAll(dataDir); err != nil {
|
|
return fmt.Errorf("the bastion is removed, but its state could not be deleted: %w", err)
|
|
}
|
|
fmt.Printf("Deleted %s: its pairings, keys and audit log.\n", dataDir)
|
|
} else {
|
|
fmt.Printf("Kept %s: its pairings, keys and the audit log of what Hosts and agents did here.\n"+
|
|
" Delete it with: privatae-bastion uninstall --delete-data, or remove the folder.\n", dataDir)
|
|
}
|
|
if runtime.GOOS == "windows" && self != "" {
|
|
fmt.Printf("The program is still at %s; Windows cannot remove a running program. Delete it now this has finished.\n", self)
|
|
}
|
|
if failed > 0 {
|
|
return fmt.Errorf("%d step(s) did not finish (above); the rest was removed", failed)
|
|
}
|
|
if machine && !whole {
|
|
fmt.Println("Your account's own bastion is removed. The machine's stays; removing it needs: sudo -H privatae-bastion uninstall")
|
|
return nil
|
|
}
|
|
if hadHosts {
|
|
fmt.Println("The bastion is uninstalled. The Hosts it was connected to see it go quiet.")
|
|
} else {
|
|
fmt.Println("The bastion is uninstalled.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// uninstallConfirm asks for the word "delete" at a terminal before the state
|
|
// goes. Not at a terminal, the answer is no.
|
|
func uninstallConfirm(dir string) bool {
|
|
if !stdinIsConsole() {
|
|
fmt.Fprintln(os.Stderr, "--delete-data deletes the audit log too, so it is confirmed at a terminal; this one's input is a file or a pipe.")
|
|
return false
|
|
}
|
|
fmt.Printf("This deletes %s: the pairings, keys and the audit log of what Hosts and agents did here.\n"+
|
|
"Type delete to go on: ", dir)
|
|
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
return strings.TrimSpace(line) == "delete"
|
|
}
|
|
|
|
// accountInstallPresent: this account has a bastion of its own installed (its
|
|
// user unit, LaunchAgent or program in ~/.local/bin).
|
|
func accountInstallPresent() bool {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, st := range uninstallSteps(runtime.GOOS, false, home, os.Getuid()) {
|
|
if st.Remove != "" {
|
|
if _, err := os.Stat(st.Remove); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|