4a619f7adc
First Windows parity milestone (DESIGN-WINDOWS.md §13 build order item 1).
- Add a PlatformOps abstraction so command dispatch is OS-neutral:
- linuxPlatformOps keeps today's systemctl/journalctl/bash behavior (deliberately
untagged so shared dispatch tests run on Windows CI)
- windowsPlatformOps maps reboot/shutdown to shutdown.exe, service control to
sc.exe (stop+start for restart), fetch_logs to Get-WinEvent, arbitrary_bash to
powershell -EncodedCommand (byte-exact under arbitrary quoting), and declines
configure_ldap (Windows logon goes through OpenCredential)
- Run theta-agent as a Windows service (x/sys/windows/svc): SYSTEM auto-start,
SCM stop/shutdown handling; CLI install-service/remove-service via svc/mgr
- Add theta-agent-helper (session-0 companion): lock/display_off/logout via
user32/wtsapi32, and staged self-update (wait for service stop, swap the
locked exe, sc start)
- Self-update becomes platform-aware: Linux renames over the running binary;
Windows stages .new and hands the swap to the helper (running exe is locked)
- Platform paths: agent.yml and tray.sock under %ProgramData%\Theta42 (the
service runs as SYSTEM while the tray runs as the user, so the per-user temp
dir no longer works for tray IPC); LDAP byte-pump falls back to TCP loopback
- config: service_name, desktop_helper, public_ip_detect (air-gap: skips
external public-IP lookups in telemetry + home monitor), wireguard block
- cli: platform-aware config path + self-update artifact name + service restart
- tests: dispatch tests pin linuxPlatformOps; 0600 mode assertions gated to
POSIX so the suite is green on Windows
Rebuilds all tracked dist binaries (v2.1.0).
159 lines
5.6 KiB
Go
159 lines
5.6 KiB
Go
package main
|
|
|
|
// linuxPlatformOps implements PlatformOps for Linux (and any POSIX host).
|
|
// The commands mirror what websocket.go executed before the PlatformOps split;
|
|
// behaviour is unchanged. Deliberately NOT build-tagged: it compiles on every
|
|
// OS (it only shells out to systemctl/journalctl/bash), so the shared command
|
|
// dispatch tests can pin it and run identically on Windows CI.
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type linuxPlatformOps struct {
|
|
exec Executor
|
|
}
|
|
|
|
func (p *linuxPlatformOps) Reboot() ([]byte, error) {
|
|
return p.exec.Execute("reboot")
|
|
}
|
|
|
|
func (p *linuxPlatformOps) Shutdown() ([]byte, error) {
|
|
out, err := p.exec.Execute("shutdown", "-h", "now")
|
|
if err != nil {
|
|
return p.exec.Execute("poweroff")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (p *linuxPlatformOps) FetchLogs(service string, lines int) ([]byte, error) {
|
|
return p.exec.Execute("journalctl", "-u", service, "-n", fmt.Sprintf("%d", lines), "--no-pager")
|
|
}
|
|
|
|
func (p *linuxPlatformOps) ServiceControl(service, action string) ([]byte, error) {
|
|
return p.exec.Execute("systemctl", action, service)
|
|
}
|
|
|
|
func (p *linuxPlatformOps) RunScript(script string) ([]byte, error) {
|
|
return p.exec.Execute("bash", "-c", script)
|
|
}
|
|
|
|
func (p *linuxPlatformOps) DesktopControl(subAction, targetUser string) ([]byte, error) {
|
|
switch subAction {
|
|
case "lock_session", "lock":
|
|
out, err := p.exec.Execute("loginctl", "lock-sessions")
|
|
if err != nil {
|
|
return p.exec.Execute("sh", "-c", "DISPLAY=:0 xdg-screensaver lock || DISPLAY=:0 xset dpms force off")
|
|
}
|
|
return out, nil
|
|
case "logout_user", "logout":
|
|
if targetUser != "" {
|
|
out, err := p.exec.Execute("loginctl", "terminate-user", targetUser)
|
|
if err != nil {
|
|
return p.exec.Execute("pkill", "-KILL", "-u", targetUser)
|
|
}
|
|
return out, nil
|
|
}
|
|
out, err := p.exec.Execute("loginctl", "terminate-session")
|
|
if err != nil {
|
|
return p.exec.Execute("pkill", "-9", "-f", "session-child")
|
|
}
|
|
return out, nil
|
|
case "display_off":
|
|
return p.exec.Execute("sh", "-c", "DISPLAY=:0 xset dpms force off || loginctl lock-sessions")
|
|
case "sleep_host", "sleep":
|
|
return p.exec.Execute("systemctl", "suspend")
|
|
default:
|
|
return nil, fmt.Errorf("unknown desktop action '%s'", subAction)
|
|
}
|
|
}
|
|
|
|
// ConfigureLDAP reproduces the original SSSD/nsswitch/PAM configuration flow.
|
|
func (p *linuxPlatformOps) ConfigureLDAP(configData string) error {
|
|
log.Println("Pushing updated SSSD configuration...")
|
|
_ = os.MkdirAll("/etc/sssd", 0755)
|
|
if err := p.exec.WriteFile("/etc/sssd/sssd.conf", []byte(configData), 0600); err != nil {
|
|
return fmt.Errorf("failed to write SSSD config: %w", err)
|
|
}
|
|
|
|
// Ensure /etc/nsswitch.conf enables sss for passwd, group, shadow, sudoers
|
|
if nssBytes, err := os.ReadFile("/etc/nsswitch.conf"); err == nil {
|
|
nssContent := string(nssBytes)
|
|
updatedNss := false
|
|
lines := strings.Split(nssContent, "\n")
|
|
for i, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if (strings.HasPrefix(trimmed, "passwd:") || strings.HasPrefix(trimmed, "group:") || strings.HasPrefix(trimmed, "shadow:") || strings.HasPrefix(trimmed, "sudoers:")) && !strings.Contains(trimmed, "sss") {
|
|
lines[i] = line + " sss"
|
|
updatedNss = true
|
|
}
|
|
}
|
|
if updatedNss {
|
|
_ = os.WriteFile("/etc/nsswitch.conf", []byte(strings.Join(lines, "\n")), 0644)
|
|
}
|
|
}
|
|
|
|
log.Println("Restarting SSSD service...")
|
|
if _, err := p.exec.Execute("systemctl", "restart", "sssd"); err != nil {
|
|
log.Printf("SSSD restart failed (%v), attempting auto-install of missing packages...", err)
|
|
if _, err2 := p.exec.Execute("sh", "-c", "DEBIAN_FRONTEND=noninteractive apt-get update -y -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sssd sssd-ldap libnss-sss libpam-sss libsss-sudo libpam-runtime || dnf install -y sssd sssd-ldap sssd-tools || yum install -y sssd sssd-ldap sssd-tools"); err2 == nil {
|
|
_, _ = p.exec.Execute("sh", "-c", "pam-auth-update --package --enable mkhomedir sss || true")
|
|
if _, err3 := p.exec.Execute("systemctl", "restart", "sssd"); err3 == nil {
|
|
writeSSSDSshConf(p.exec)
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("failed to restart sssd")
|
|
}
|
|
|
|
writeSSSDSshConf(p.exec)
|
|
return nil
|
|
}
|
|
|
|
// writeSSSDSshConf installs the AuthorizedKeysCommand config sshd needs to look
|
|
// up LDAP-backed SSH keys.
|
|
func writeSSSDSshConf(exec Executor) {
|
|
_ = os.MkdirAll("/etc/ssh/sshd_config.d", 0755)
|
|
sshConfPath := "/etc/ssh/sshd_config.d/theta-sssd.conf"
|
|
sshConfContent := "AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u\nAuthorizedKeysCommandUser nobody\n"
|
|
if err := os.WriteFile(sshConfPath, []byte(sshConfContent), 0644); err == nil {
|
|
_, _ = exec.Execute("systemctl", "reload", "sshd")
|
|
}
|
|
if sshdBytes, err2 := os.ReadFile("/etc/ssh/sshd_config"); err2 == nil {
|
|
sshdStr := string(sshdBytes)
|
|
if !strings.Contains(sshdStr, "sss_ssh_authorizedkeys") {
|
|
sshdStr += "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u\nAuthorizedKeysCommandUser nobody\n"
|
|
_ = os.WriteFile("/etc/ssh/sshd_config", []byte(sshdStr), 0644)
|
|
_, _ = exec.Execute("systemctl", "reload", "sshd")
|
|
}
|
|
}
|
|
_, _ = exec.Execute("sh", "-c", "pam-auth-update --package --enable mkhomedir sss || true")
|
|
}
|
|
|
|
func (p *linuxPlatformOps) ApplyUpdate(downloadURL, checksum string) error {
|
|
tmpPath, err := downloadBinary(downloadURL, checksum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
selfPath, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve current binary path: %w", err)
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(selfPath); err == nil {
|
|
selfPath = resolved
|
|
}
|
|
if err := os.Rename(tmpPath, selfPath); err != nil {
|
|
return fmt.Errorf("failed to replace binary: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *linuxPlatformOps) SelfRestart() {
|
|
os.Exit(0)
|
|
}
|