feat: safely control Shell Target
This commit is contained in:
+51
-4
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/shell"
|
||||
"proxyctl/internal/target"
|
||||
"proxyctl/internal/tui"
|
||||
)
|
||||
|
||||
@@ -20,14 +23,58 @@ func main() {
|
||||
}
|
||||
|
||||
func run(args []string, input io.Reader, output io.Writer) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("this build supports no command-line arguments")
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate proxyctl executable: %w", err)
|
||||
}
|
||||
configuration, err := profile.ResolveConfigurationForCurrentProcess()
|
||||
return runWithEnvironment(args, input, output, processEnvironment{}, shell.OSFileSystem{}, executable)
|
||||
}
|
||||
|
||||
type processEnvironment struct{}
|
||||
|
||||
func (processEnvironment) CurrentUser() (profile.UserIdentity, error) {
|
||||
return profile.SystemCurrentUser()
|
||||
}
|
||||
func (processEnvironment) LookupUser(name string) (profile.UserIdentity, error) {
|
||||
return profile.SystemLookupUser(name)
|
||||
}
|
||||
func (processEnvironment) LookupEnv(name string) (string, bool) { return os.LookupEnv(name) }
|
||||
|
||||
func runWithEnvironment(args []string, input io.Reader, output io.Writer, environment profile.IdentityEnvironment, files shell.FileSystem, executable string) error {
|
||||
profileConfiguration, err := profile.ResolveConfiguration(environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
application, err := app.New(profile.NewOwnedFileStoreAt(configuration.Path, configuration.Owner, configuration.TrustedRoot))
|
||||
shellConfiguration, err := shell.ResolveConfiguration(environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shellTarget := shell.New(shellConfiguration, files, environment)
|
||||
|
||||
if len(args) != 0 {
|
||||
if len(args) != 2 || args[0] != "shell" || (args[1] != "install" && args[1] != "uninstall") {
|
||||
return fmt.Errorf("usage: proxyctl [shell install|shell uninstall]")
|
||||
}
|
||||
availability := shellTarget.Availability(context.Background())
|
||||
if !availability.Available {
|
||||
return fmt.Errorf("Shell Target unavailable: %s", availability.Evidence)
|
||||
}
|
||||
if args[1] == "install" {
|
||||
if err := shellTarget.Install(context.Background(), executable); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(output, "Shell integration installed; restart or source your Shell startup file once.")
|
||||
return nil
|
||||
}
|
||||
if err := shellTarget.Uninstall(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(output, "Shell integration uninstalled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
store := profile.NewOwnedFileStoreAt(profileConfiguration.Path, profileConfiguration.Owner, profileConfiguration.TrustedRoot)
|
||||
application, err := app.NewWithTargets(store, []target.Target{shellTarget})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/shell"
|
||||
)
|
||||
|
||||
type commandEnvironment struct {
|
||||
identity profile.UserIdentity
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (e commandEnvironment) CurrentUser() (profile.UserIdentity, error) { return e.identity, nil }
|
||||
func (e commandEnvironment) LookupUser(string) (profile.UserIdentity, error) {
|
||||
return profile.UserIdentity{}, os.ErrNotExist
|
||||
}
|
||||
func (e commandEnvironment) LookupEnv(name string) (string, bool) {
|
||||
value, ok := e.env[name]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func TestShellInstallAndUninstallCommandsUseInjectedUserRoots(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
environment := commandEnvironment{identity: profile.UserIdentity{Username: "tester", HomeDir: home, UID: os.Getuid(), GID: os.Getgid()}, env: map[string]string{"SHELL": "/bin/bash", "XDG_CONFIG_HOME": filepath.Join(home, "config"), "XDG_STATE_HOME": filepath.Join(home, "state")}}
|
||||
var output bytes.Buffer
|
||||
if err := runWithEnvironment([]string{"shell", "install"}, strings.NewReader(""), &output, environment, shell.OSFileSystem{}, "/test/proxyctl"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
startup := filepath.Join(home, ".bashrc")
|
||||
data, err := os.ReadFile(startup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(data), "command '/test/proxyctl'") || !strings.Contains(output.String(), "installed") {
|
||||
t.Fatalf("install output/startup = %q / %q", output.String(), data)
|
||||
}
|
||||
output.Reset()
|
||||
if err := runWithEnvironment([]string{"shell", "uninstall"}, strings.NewReader(""), &output, environment, shell.OSFileSystem{}, "/test/proxyctl"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err = os.ReadFile(startup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), "proxyctl shell integration") || !strings.Contains(output.String(), "uninstalled") {
|
||||
t.Fatalf("uninstall output/startup = %q / %q", output.String(), data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsupportedShellMaintenanceCommandExplainsAvailability(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
environment := commandEnvironment{identity: profile.UserIdentity{Username: "tester", HomeDir: home, UID: os.Getuid(), GID: os.Getgid()}, env: map[string]string{"SHELL": "/bin/fish"}}
|
||||
err := runWithEnvironment([]string{"shell", "install"}, strings.NewReader(""), &bytes.Buffer{}, environment, shell.OSFileSystem{}, "/test/proxyctl")
|
||||
if err == nil || !strings.Contains(err.Error(), "Bash or Zsh") {
|
||||
t.Fatalf("unsupported Shell error = %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user