diff --git a/cmd/down.go b/cmd/down.go index 3bd67a47d..fea6503a2 100644 --- a/cmd/down.go +++ b/cmd/down.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" - "github.com/windsorcli/cli/pkg/shell" ) var ( @@ -17,34 +16,18 @@ var ( ) var downCmd = &cobra.Command{ - Use: "down", - Short: "Tear down the Windsor environment", - Long: "Tear down the Windsor environment by executing necessary shell commands.", - SilenceUsage: true, + Use: "down", + Short: "Tear down the Windsor environment", + Long: "Tear down the Windsor environment by executing necessary shell commands.", + SilenceUsage: true, + PersistentPreRunE: checkTrust, RunE: func(cmd *cobra.Command, args []string) error { // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, initialize a base pipeline to set up core dependencies (shell, config, etc.) - _, err := pipelines.WithPipeline(injector, cmd.Context(), "basePipeline") - if err != nil { - return fmt.Errorf("failed to initialize dependencies: %w", err) - } - - // Now check if directory is trusted using the initialized shell - shellInstance := injector.Resolve("shell") - if shellInstance != nil { - if s, ok := shellInstance.(shell.Shell); ok { - if err := s.CheckTrustedDirectory(); err != nil { - return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") - } - } - } - - // Directory is trusted, proceed with normal pipeline execution // First, run the env pipeline in quiet mode to set up environment variables var envPipeline pipelines.Pipeline - envPipeline, err = pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") + envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") if err != nil { return fmt.Errorf("failed to set up env pipeline: %w", err) } diff --git a/cmd/down_test.go b/cmd/down_test.go index 0bda8baf7..d2af55e57 100644 --- a/cmd/down_test.go +++ b/cmd/down_test.go @@ -333,28 +333,4 @@ func TestDownCmd(t *testing.T) { } }) - t.Run("FailsWhenDirectoryNotTrusted", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupDownMocks(t) - - // And shell CheckTrustedDirectory returns an error - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return fmt.Errorf("directory not trusted") - } - - // When executing the down command - cmd := createTestDownCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should occur about untrusted directory - if err == nil { - t.Error("Expected error when directory is not trusted, got nil") - } - if !strings.Contains(err.Error(), "not in a trusted directory") { - t.Errorf("Expected error about untrusted directory, got: %v", err) - } - }) } diff --git a/cmd/env.go b/cmd/env.go index 55833ef1f..9199b66cb 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -10,10 +10,11 @@ import ( ) var envCmd = &cobra.Command{ - Use: "env", - Short: "Output commands to set environment variables", - Long: "Output commands to set environment variables for the application.", - SilenceUsage: true, + Use: "env", + Short: "Output commands to set environment variables", + Long: "Output commands to set environment variables for the application.", + SilenceUsage: true, + PersistentPreRunE: checkTrust, RunE: func(cmd *cobra.Command, args []string) error { // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) diff --git a/cmd/env_test.go b/cmd/env_test.go index 08d899c9f..d42a1c768 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -15,9 +15,13 @@ func TestEnvCmd(t *testing.T) { } t.Run("Success", func(t *testing.T) { - // Given proper output capture + // Given proper output capture and mock setup _, stderr := setup(t) + // Set up mocks with trusted directory + mocks := setupMocks(t) + _ = mocks + rootCmd.SetArgs([]string{"env"}) // When executing the command @@ -35,9 +39,13 @@ func TestEnvCmd(t *testing.T) { }) t.Run("SuccessWithDecrypt", func(t *testing.T) { - // Given proper output capture + // Given proper output capture and mock setup _, stderr := setup(t) + // Set up mocks with trusted directory + mocks := setupMocks(t) + _ = mocks + rootCmd.SetArgs([]string{"env", "--decrypt"}) // When executing the command @@ -75,9 +83,13 @@ func TestEnvCmd(t *testing.T) { }) t.Run("SuccessWithVerbose", func(t *testing.T) { - // Given proper output capture + // Given proper output capture and mock setup _, stderr := setup(t) + // Set up mocks with trusted directory + mocks := setupMocks(t) + _ = mocks + rootCmd.SetArgs([]string{"env", "--verbose"}) // When executing the command diff --git a/cmd/exec.go b/cmd/exec.go index 6c173ac12..d8352fb25 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -7,15 +7,15 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" - "github.com/windsorcli/cli/pkg/shell" ) // execCmd represents the exec command var execCmd = &cobra.Command{ - Use: "exec [command] [args...]", - Short: "Execute a command with environment variables", - Long: "Execute a command with environment variables loaded from configuration and secrets", - Args: cobra.MinimumNArgs(1), + Use: "exec [command] [args...]", + Short: "Execute a command with environment variables", + Long: "Execute a command with environment variables loaded from configuration and secrets", + Args: cobra.MinimumNArgs(1), + PersistentPreRunE: checkTrust, RunE: func(cmd *cobra.Command, args []string) error { // Safety check for arguments if len(args) == 0 { @@ -25,26 +25,6 @@ var execCmd = &cobra.Command{ // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // Initialize base pipeline to set up dependencies - basePipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "basePipeline") - if err != nil { - return fmt.Errorf("failed to set up base pipeline: %w", err) - } - - if err := basePipeline.Execute(cmd.Context()); err != nil { - return fmt.Errorf("failed to initialize base pipeline: %w", err) - } - - // Now check if directory is trusted using the initialized shell - shellInstance := injector.Resolve("shell") - if shellInstance != nil { - if s, ok := shellInstance.(shell.Shell); ok { - if err := s.CheckTrustedDirectory(); err != nil { - return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") - } - } - } - // First, run the env pipeline in quiet mode to set up environment variables envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") if err != nil { diff --git a/cmd/exec_test.go b/cmd/exec_test.go index 741fc3ea0..2e8010760 100644 --- a/cmd/exec_test.go +++ b/cmd/exec_test.go @@ -63,45 +63,6 @@ func TestExecCmd(t *testing.T) { } }) - t.Run("UntrustedDirectory", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell that fails trust check - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { - return fmt.Errorf("directory not trusted") - } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) - cmd.SetContext(ctx) - - args := []string{"go", "version"} - cmd.SetArgs(args) - - err := cmd.Execute() - - if err == nil { - t.Error("Expected error for untrusted directory, got nil") - } - expectedMsg := "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - if fmt.Sprintf("%v", err) != expectedMsg { - t.Errorf("Expected error message '%s', got '%v'", expectedMsg, err) - } - }) - t.Run("NoCommandProvided", func(t *testing.T) { tmpDir := t.TempDir() originalDir, _ := os.Getwd() diff --git a/cmd/install.go b/cmd/install.go index 856e80496..bcacb5ea8 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -12,9 +12,10 @@ import ( var installWaitFlag bool var installCmd = &cobra.Command{ - Use: "install", - Short: "Install the blueprint's cluster-level services", - SilenceUsage: true, + Use: "install", + Short: "Install the blueprint's cluster-level services", + SilenceUsage: true, + PersistentPreRunE: checkTrust, RunE: func(cmd *cobra.Command, args []string) error { // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) diff --git a/cmd/root.go b/cmd/root.go index 641c4b9e1..4043851a4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,10 @@ package cmd import ( "context" + "fmt" + "os" + "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" @@ -50,3 +54,51 @@ func init() { // Define the --verbose flag rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") } + +// checkTrust performs trust validation for Windsor CLI commands requiring a trusted project directory. +// It verifies directory trust status by checking if the current project directory is in the trusted file list. +// For the "init" command, or for the "env" command with the --hook flag set, trust validation is skipped. +// Returns an error if the directory is untrusted. +func checkTrust(cmd *cobra.Command, args []string) error { + if cmd.Name() == "init" { + return nil + } + + if cmd.Name() == "env" { + if hook, _ := cmd.Flags().GetBool("hook"); hook { + return nil + } + } + + // Use shims to allow mocking in tests + currentDir, err := shims.Getwd() + if err != nil { + return fmt.Errorf("Error getting current directory: %w", err) + } + + homeDir, err := shims.UserHomeDir() + if err != nil { + return fmt.Errorf("Error getting user home directory: %w", err) + } + + trustedDirPath := filepath.Join(homeDir, ".config", "windsor") + trustedFilePath := filepath.Join(trustedDirPath, ".trusted") + + data, err := shims.ReadFile(trustedFilePath) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") + } + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") + } + + trustedDirs := strings.Split(strings.TrimSpace(string(data)), "\n") + for _, trustedDir := range trustedDirs { + trimmedDir := strings.TrimSpace(trustedDir) + if trimmedDir != "" && strings.HasPrefix(currentDir, trimmedDir) { + return nil + } + } + + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") +} diff --git a/cmd/root_test.go b/cmd/root_test.go index b470e1468..aba58cbdd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -9,8 +9,12 @@ import ( "bytes" "os" "os/exec" + "path/filepath" + "runtime" + "strings" "testing" + "github.com/spf13/cobra" blueprintpkg "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" @@ -62,9 +66,13 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { UserHomeDir: func() (string, error) { return t.TempDir(), nil }, Stat: func(string) (os.FileInfo, error) { return nil, nil }, RemoveAll: func(string) error { return nil }, - Getwd: func() (string, error) { return t.TempDir(), nil }, + Getwd: func() (string, error) { return "/test/project", nil }, Command: func(string, ...string) *exec.Cmd { return exec.Command("echo") }, Setenv: func(string, string) error { return nil }, + ReadFile: func(filename string) ([]byte, error) { + // Mock trusted file content that includes the current directory + return []byte("/test/project\n"), nil + }, } // Override with provided shims if any @@ -259,3 +267,125 @@ func TestRootCmd_PersistentPreRunE(t *testing.T) { } }) } + +func TestCheckTrust(t *testing.T) { + createMockCmd := func(name string) *cobra.Command { + return &cobra.Command{ + Use: name, + } + } + + t.Run("SkipsTrustCheckForInitCommand", func(t *testing.T) { + // Given an init command + cmd := createMockCmd("init") + + // When checking trust + err := checkTrust(cmd, []string{}) + + // Then no error should occur (trust check is skipped) + if err != nil { + t.Errorf("Expected no error for init command, got: %v", err) + } + }) + + t.Run("SkipsTrustCheckForEnvCommandWithHookFlag", func(t *testing.T) { + // Given an env command with hook flag + cmd := createMockCmd("env") + cmd.Flags().Bool("hook", false, "hook flag") + cmd.Flags().Set("hook", "true") + + // When checking trust + err := checkTrust(cmd, []string{}) + + // Then no error should occur (trust check is skipped for env --hook) + if err != nil { + t.Errorf("Expected no error for env --hook, got: %v", err) + } + }) + + t.Run("ChecksTrustForEnvCommandWithoutHookFlag", func(t *testing.T) { + // Given an env command without hook flag in an untrusted directory + cmd := createMockCmd("env") + cmd.Flags().Bool("hook", false, "hook flag") + + // Set up a temporary directory that's not trusted + tmpDir := t.TempDir() + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer os.Chdir(originalDir) + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // When checking trust + err = checkTrust(cmd, []string{}) + + // Then an error should occur about untrusted directory + if err == nil { + t.Error("Expected error for untrusted directory, got nil") + } + if !strings.Contains(err.Error(), "not in a trusted directory") { + t.Errorf("Expected trust error message, got: %v", err) + } + }) + + t.Run("PassesTrustCheckForTrustedDirectory", func(t *testing.T) { + // Given a command in a trusted directory + cmd := createMockCmd("down") + + // Set up a temporary directory structure with trusted file + tmpDir := t.TempDir() + testDir := filepath.Join(tmpDir, "project") + if err := os.MkdirAll(testDir, 0755); err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Create trusted file + trustedDir := filepath.Join(tmpDir, ".config", "windsor") + if err := os.MkdirAll(trustedDir, 0755); err != nil { + t.Fatalf("Failed to create trusted directory: %v", err) + } + + trustedFile := filepath.Join(trustedDir, ".trusted") + realTestDir, _ := filepath.EvalSymlinks(testDir) + trustedContent := realTestDir + "\n" + if err := os.WriteFile(trustedFile, []byte(trustedContent), 0644); err != nil { + t.Fatalf("Failed to create trusted file: %v", err) + } + + // Change to test directory + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer os.Chdir(originalDir) + + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Mock home directory for cross-platform compatibility + var originalHome string + if runtime.GOOS == "windows" { + originalHome = os.Getenv("USERPROFILE") + defer os.Setenv("USERPROFILE", originalHome) + os.Setenv("USERPROFILE", tmpDir) + } else { + originalHome = os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tmpDir) + } + + // When checking trust + err = checkTrust(cmd, []string{}) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error for trusted directory, got: %v", err) + } + }) + +} diff --git a/cmd/shims.go b/cmd/shims.go index 270948f86..2b4288985 100644 --- a/cmd/shims.go +++ b/cmd/shims.go @@ -25,6 +25,7 @@ type Shims struct { Setenv func(string, string) error Command func(string, ...string) *exec.Cmd Getenv func(string) string + ReadFile func(string) ([]byte, error) } // ============================================================================= @@ -42,6 +43,7 @@ func NewShims() *Shims { Setenv: os.Setenv, Command: exec.Command, Getenv: os.Getenv, + ReadFile: os.ReadFile, } } diff --git a/cmd/up.go b/cmd/up.go index 75928af52..c1b4b4dd6 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" - "github.com/windsorcli/cli/pkg/shell" ) var ( @@ -16,31 +15,15 @@ var ( ) var upCmd = &cobra.Command{ - Use: "up", - Short: "Set up the Windsor environment", - Long: "Set up the Windsor environment by executing necessary shell commands.", - SilenceUsage: true, + Use: "up", + Short: "Set up the Windsor environment", + Long: "Set up the Windsor environment by executing necessary shell commands.", + SilenceUsage: true, + PersistentPreRunE: checkTrust, RunE: func(cmd *cobra.Command, args []string) error { // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, initialize a base pipeline to set up core dependencies (shell, config, etc.) - _, err := pipelines.WithPipeline(injector, cmd.Context(), "basePipeline") - if err != nil { - return fmt.Errorf("failed to initialize dependencies: %w", err) - } - - // Now check if directory is trusted using the initialized shell - shellInstance := injector.Resolve("shell") - if shellInstance != nil { - if s, ok := shellInstance.(shell.Shell); ok { - if err := s.CheckTrustedDirectory(); err != nil { - return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") - } - } - } - - // Directory is trusted, proceed with normal pipeline execution // First, run the env pipeline in quiet mode to set up environment variables envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") if err != nil { diff --git a/cmd/up_test.go b/cmd/up_test.go index 725032513..5782d1566 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -477,28 +477,4 @@ func TestUpCmd(t *testing.T) { } }) - t.Run("FailsWhenDirectoryNotTrusted", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // And shell CheckTrustedDirectory returns an error - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return fmt.Errorf("directory not trusted") - } - - // When executing the up command - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should occur about untrusted directory - if err == nil { - t.Error("Expected error when directory is not trusted, got nil") - } - if !strings.Contains(err.Error(), "not in a trusted directory") { - t.Errorf("Expected error about untrusted directory, got: %v", err) - } - }) }