package main // switch_cli.go — the local switch (localdeny.go) from a terminal. // // privatae-bastion switch on|off // privatae-bastion switch --list // // The console page has had this switch since localdeny.go was written; a // machine with no browser and no person at it (a server the bastion guards // for a Host on the same box) had no way to reach it. The command writes the // same deny list through the same localDenySet, so the invariant there holds // unchanged: "off" removes reach, "on" clears a refusal and grants nothing. // // MACHINE AUTHORITY, for both directions. On a machine that serves a Host it // does not belong to the person using that Host, the switch is part of how // the machine was set up, and the one who stands behind that is whoever // administers the box. --list only reads, and needs no authority of its own; // beside a system service on macOS or Windows an ordinary account is refused // it (serviceGuard, sysservice.go), because it would read that account's own // state rather than the machine's. import ( "errors" "fmt" "io" "os" "sort" "strings" ) const switchUsage = "usage: sudo -H privatae-bastion switch on|off\n" + " privatae-bastion switch --list\n" + " sudo -H privatae-bastion switch --reset (only when the list cannot be read)" // cmdSwitch parses the arguments and runs one switch, the listing, or the // reset of an unreadable list. func cmdSwitch(args []string) error { if len(args) == 1 && args[0] == "--list" { return switchList(os.Stdout) } if len(args) == 1 && args[0] == "--reset" { // The same authority as switching anything back on: a reset leaves // nothing switched off. if err := requireMachineAuthority("switch"); err != nil { return err } kept, err := localDenyReset() if err != nil { return err } fmt.Printf("The unreadable list is kept at %s. Nothing is switched off now; "+ "switch off again whatever you had off: switch off\n", kept) return nil } if len(args) != 3 { return errors.New(switchUsage) } var off bool switch strings.ToLower(args[2]) { case "off": off = true case "on": off = false default: return fmt.Errorf("the last word is on or off, not %q\n%s", args[2], switchUsage) } if err := requireMachineAuthority("switch"); err != nil { return err } host, err := switchResolveHost(args[0]) if err != nil { return err } family := strings.TrimSpace(args[1]) if err := localDenySet(host, family, off); err != nil { return err } if off { fmt.Printf("%s %s off: this machine now refuses it, and tells the host why. Its grant is untouched.\n", host, family) } else { fmt.Printf("%s %s on: the refusal is cleared, up to whatever its signed grant already allowed.\n", host, family) } return nil } // switchResolveHost accepts a paired host's full name or a unique prefix of // one, the way --host does everywhere else. A name no pairing matches is // passed through unchanged so that localDenySet gives its own refusal, which // names the problem in the same words the console page uses. func switchResolveHost(key string) (string, error) { key = strings.TrimSpace(key) if key == "" { return "", errors.New("no host named\n" + switchUsage) } cfgs, err := loadPairings() if err != nil { return key, nil } c, err := findPairing(cfgs, key) if err != nil { return "", err } if c == nil { return key, nil } return c.HostName, nil } // switchList prints one line per paired host and family: // // on|off [since