From 4313b7f1d6648e0f5c45b063ff5b91b5ad6d2716 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Fri, 22 Nov 2024 14:30:13 -0500 Subject: [PATCH] linux key escrow progress windows (#24069) --- orbit/pkg/execuser/execuser.go | 13 ----- orbit/pkg/execuser/execuser_darwin.go | 7 +-- orbit/pkg/execuser/execuser_linux.go | 38 +++----------- orbit/pkg/execuser/execuser_windows.go | 5 -- orbit/pkg/luks/luks_linux.go | 70 ++++++++++++++++++++------ orbit/pkg/zenity/zenity.go | 33 +++++++++--- orbit/pkg/zenity/zenity_test.go | 21 +++++--- 7 files changed, 103 insertions(+), 84 deletions(-) diff --git a/orbit/pkg/execuser/execuser.go b/orbit/pkg/execuser/execuser.go index 5d4aaa353fef..e598bdc2aaaa 100644 --- a/orbit/pkg/execuser/execuser.go +++ b/orbit/pkg/execuser/execuser.go @@ -2,8 +2,6 @@ // SYSTEM service on Windows) as the current login user. package execuser -import "context" - type eopts struct { env [][2]string args [][2]string @@ -51,14 +49,3 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er } return runWithOutput(path, o) } - -// RunWithWait runs an application as the current login user and waits for it to finish -// or to be canceled by the context. Canceling the context will not return an error. -// It assumes the caller is running with high privileges (root on UNIX). -func RunWithWait(ctx context.Context, path string, opts ...Option) error { - var o eopts - for _, fn := range opts { - fn(&o) - } - return runWithWait(ctx, path, o) -} diff --git a/orbit/pkg/execuser/execuser_darwin.go b/orbit/pkg/execuser/execuser_darwin.go index ca92601ba980..6641b40604f7 100644 --- a/orbit/pkg/execuser/execuser_darwin.go +++ b/orbit/pkg/execuser/execuser_darwin.go @@ -1,7 +1,6 @@ package execuser import ( - "context" "errors" "fmt" "io" @@ -10,6 +9,8 @@ import ( ) // run uses macOS open command to start application as the current login user. +// Note that the child process spawns a new process in user space and thus it is not +// effective to add a context to this function to cancel the child process. func run(path string, opts eopts) (lastLogs string, err error) { info, err := os.Stat(path) if err != nil { @@ -53,7 +54,3 @@ func run(path string, opts eopts) (lastLogs string, err error) { func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) { return nil, 0, errors.New("not implemented") } - -func runWithWait(ctx context.Context, path string, opts eopts) error { - return errors.New("not implemented") -} diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index 1e9614d01be8..5ce487c23c6a 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -3,7 +3,6 @@ package execuser import ( "bufio" "bytes" - "context" "errors" "fmt" "io" @@ -33,6 +32,12 @@ func run(path string, opts eopts) (lastLogs string, err error) { path, ) + if len(opts.args) > 0 { + for _, arg := range opts.args { + args = append(args, arg[0], arg[1]) + } + } + cmd := exec.Command("sudo", args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -74,37 +79,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er return output, exitCode, nil } -func runWithWait(ctx context.Context, path string, opts eopts) error { - args, err := getUserAndDisplayArgs(path, opts) - if err != nil { - return fmt.Errorf("get args: %w", err) - } - - args = append(args, path) - - if len(opts.args) > 0 { - for _, arg := range opts.args { - args = append(args, arg[0], arg[1]) - } - } - - cmd := exec.CommandContext(ctx, "sudo", args...) - log.Printf("cmd=%s", cmd.String()) - - if err := cmd.Start(); err != nil { - return fmt.Errorf("cmd start %q: %w", path, err) - } - - if err := cmd.Wait(); err != nil { - if errors.Is(ctx.Err(), context.Canceled) { - return nil - } - return fmt.Errorf("cmd wait %q: %w", path, err) - } - - return nil -} - func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) { user, err := getLoginUID() if err != nil { diff --git a/orbit/pkg/execuser/execuser_windows.go b/orbit/pkg/execuser/execuser_windows.go index f3bd58038db8..9cf7e9d33855 100644 --- a/orbit/pkg/execuser/execuser_windows.go +++ b/orbit/pkg/execuser/execuser_windows.go @@ -6,7 +6,6 @@ package execuser // To view what was modified/added, you can use the execuser_windows_diff.sh script. import ( - "context" "errors" "fmt" "os" @@ -122,10 +121,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er return nil, 0, errors.New("not implemented") } -func runWithWait(ctx context.Context, path string, opts eopts) error { - return errors.New("not implemented") -} - // getCurrentUserSessionId will attempt to resolve // the session ID of the user currently active on // the system. diff --git a/orbit/pkg/luks/luks_linux.go b/orbit/pkg/luks/luks_linux.go index 9c32307a500e..c45cb74ebffd 100644 --- a/orbit/pkg/luks/luks_linux.go +++ b/orbit/pkg/luks/luks_linux.go @@ -25,10 +25,10 @@ const ( entryDialogTitle = "Enter disk encryption passphrase" entryDialogText = "Passphrase:" retryEntryDialogText = "Passphrase incorrect. Please try again." - infoFailedTitle = "Encryption key escrow" + infoTitle = "Disk encryption" infoFailedText = "Failed to escrow key. Please try again later." - infoSuccessTitle = "Encryption key escrow" - infoSuccessText = "Key escrowed successfully." + infoSuccessText = "Success! Now, return to your browser window and follow the instructions to verify disk encryption." + timeoutMessage = "Please visit Fleet Desktop > My device and click Create key" maxKeySlots = 8 userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase ) @@ -53,6 +53,11 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { response.Err = err.Error() } + if len(key) == 0 && err == nil { + // dialog was canceled or timed out + return nil + } + response.Passphrase = string(key) response.KeySlot = keyslot @@ -76,7 +81,7 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { } // Show error in dialog - if err := lr.infoPrompt(ctx, infoFailedTitle, infoFailedText); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, infoFailedText); err != nil { log.Info().Err(err).Msg("failed to show failed escrow key dialog") } @@ -84,14 +89,14 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { } if response.Err != "" { - if err := lr.infoPrompt(ctx, infoFailedTitle, response.Err); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, response.Err); err != nil { log.Info().Err(err).Msg("failed to show response error dialog") } return fmt.Errorf("error getting linux escrow key: %s", response.Err) } // Show success dialog - if err := lr.infoPrompt(ctx, infoSuccessTitle, infoSuccessText); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, infoSuccessText); err != nil { log.Info().Err(err).Msg("failed to show success escrow key dialog") } @@ -108,6 +113,19 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err) } + if len(passphrase) == 0 { + log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") + return nil, nil, nil + } + + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Validating passphrase...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog") + } + // Validate the passphrase for { valid, err := lr.passphraseIsValid(ctx, device, devicePath, passphrase, userKeySlot) @@ -123,11 +141,27 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by if err != nil { return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err) } + + if len(passphrase) == 0 { + log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") + return nil, nil, nil + } + + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Validating passphrase...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog after retry") + } } - if len(passphrase) == 0 { - log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") - return nil, nil, nil + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Key escrow in progress...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog") } escrowPassphrase, err := generateRandomPassphrase() @@ -216,14 +250,18 @@ func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]by TimeOut: 1 * time.Minute, }) if err != nil { - switch err { - case dialog.ErrCanceled: + switch { + case errors.Is(err, dialog.ErrCanceled): log.Debug().Msg("end user canceled key escrow dialog") return nil, nil - case dialog.ErrTimeout: + case errors.Is(err, dialog.ErrTimeout): log.Debug().Msg("key escrow dialog timed out") + err := lr.infoPrompt(ctx, infoTitle, timeoutMessage) + if err != nil { + log.Info().Err(err).Msg("failed to show timeout dialog") + } return nil, nil - case dialog.ErrUnknown: + case errors.Is(err, dialog.ErrUnknown): return nil, err default: return nil, err @@ -237,11 +275,11 @@ func (lr *LuksRunner) infoPrompt(ctx context.Context, title, text string) error err := lr.notifier.ShowInfo(ctx, dialog.InfoOptions{ Title: title, Text: text, - TimeOut: 30 * time.Second, + TimeOut: 1 * time.Minute, }) if err != nil { - switch err { - case dialog.ErrTimeout: + switch { + case errors.Is(err, dialog.ErrTimeout): log.Debug().Msg("successPrompt timed out") return nil default: diff --git a/orbit/pkg/zenity/zenity.go b/orbit/pkg/zenity/zenity.go index bd51d214f82b..2d2989f9d804 100644 --- a/orbit/pkg/zenity/zenity.go +++ b/orbit/pkg/zenity/zenity.go @@ -7,28 +7,37 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" "github.com/fleetdm/fleet/v4/orbit/pkg/execuser" + "github.com/fleetdm/fleet/v4/orbit/pkg/platform" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/rs/zerolog/log" ) +const zenityProcessName = "zenity" + type Zenity struct { // cmdWithOutput can be set in tests to mock execution of the dialog. cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error) // cmdWithWait can be set in tests to mock execution of the dialog. cmdWithWait func(ctx context.Context, args ...string) error + // killZenityFunc can be set in tests to mock killing the zenity process. + killZenityFunc func() } // New creates a new Zenity dialog instance for zenity v4 on Linux. // Zenity implements the Dialog interface. func New() *Zenity { return &Zenity{ - cmdWithOutput: execCmdWithOutput, - cmdWithWait: execCmdWithWait, + cmdWithOutput: execCmdWithOutput, + cmdWithWait: execCmdWithWait, + killZenityFunc: killZenityProcesses, } } // ShowEntry displays an dialog that accepts end user input. It returns the entered // text or errors ErrCanceled, ErrTimeout, or ErrUnknown. func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) { + z.killZenityFunc() + args := []string{"--entry"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -47,9 +56,9 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt if err != nil { switch statusCode { case 1: - return nil, ctxerr.Wrap(ctx, dialog.ErrCanceled) + return nil, dialog.ErrCanceled case 5: - return nil, ctxerr.Wrap(ctx, dialog.ErrTimeout) + return nil, dialog.ErrTimeout default: return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error()) } @@ -60,6 +69,8 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt // ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown. func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error { + z.killZenityFunc() + args := []string{"--info"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -94,6 +105,8 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error { // Use this function for cases where a progress dialog is needed to run // alongside other operations, with explicit cancellation or termination. func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error { + z.killZenityFunc() + args := []string{"--progress"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -122,7 +135,7 @@ func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args } - output, exitCode, err := execuser.RunWithOutput("zenity", opts...) + output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...) // Trim the newline from zenity output output = bytes.TrimSuffix(output, []byte("\n")) @@ -136,5 +149,13 @@ func execCmdWithWait(ctx context.Context, args ...string) error { opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args } - return execuser.RunWithWait(ctx, "zenity", opts...) + _, err := execuser.Run(zenityProcessName, opts...) + return err +} + +func killZenityProcesses() { + _, err := platform.KillAllProcessByName(zenityProcessName) + if err != nil { + log.Warn().Err(err).Msg("failed to kill zenity process") + } } diff --git a/orbit/pkg/zenity/zenity_test.go b/orbit/pkg/zenity/zenity_test.go index 5d57f52d91ff..f7b2337f8cc2 100644 --- a/orbit/pkg/zenity/zenity_test.go +++ b/orbit/pkg/zenity/zenity_test.go @@ -76,7 +76,8 @@ func TestShowEntryArgs(t *testing.T) { output: []byte("some output"), } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, tt.opts) assert.NoError(t, err) @@ -117,7 +118,8 @@ func TestShowEntryError(t *testing.T) { exitCode: tt.exitCode, } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) require.ErrorIs(t, err, tt.expectedErr) @@ -133,7 +135,8 @@ func TestShowEntrySuccess(t *testing.T) { output: []byte("some output"), } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) assert.NoError(t, err) @@ -168,7 +171,8 @@ func TestShowInfoArgs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockExecCmd{} z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } err := z.ShowInfo(ctx, tt.opts) assert.NoError(t, err) @@ -203,7 +207,8 @@ func TestShowInfoError(t *testing.T) { exitCode: tt.exitCode, } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } err := z.ShowInfo(ctx, dialog.InfoOptions{}) require.ErrorIs(t, err, tt.expectedErr) @@ -233,7 +238,8 @@ func TestProgressArgs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockExecCmd{} z := &Zenity{ - cmdWithWait: mock.runWithWait, + cmdWithWait: mock.runWithWait, + killZenityFunc: func() {}, } err := z.ShowProgress(ctx, tt.opts) assert.NoError(t, err) @@ -249,7 +255,8 @@ func TestProgressKillOnCancel(t *testing.T) { waitDuration: 5 * time.Second, } z := &Zenity{ - cmdWithWait: mock.runWithWait, + cmdWithWait: mock.runWithWait, + killZenityFunc: func() {}, } done := make(chan struct{})