diff --git a/pkg/crc/preflight/preflight_checks_tray_windows.go b/pkg/crc/preflight/preflight_checks_tray_windows.go index 494151752a..8bc481cff9 100644 --- a/pkg/crc/preflight/preflight_checks_tray_windows.go +++ b/pkg/crc/preflight/preflight_checks_tray_windows.go @@ -4,95 +4,145 @@ import ( "fmt" "io/ioutil" goos "os" - "os/exec" - "os/user" "path/filepath" "strings" + "time" "github.com/code-ready/crc/pkg/crc/constants" "github.com/code-ready/crc/pkg/crc/input" "github.com/code-ready/crc/pkg/crc/logging" - "github.com/code-ready/crc/pkg/crc/version" dl "github.com/code-ready/crc/pkg/download" "github.com/code-ready/crc/pkg/embed" "github.com/code-ready/crc/pkg/extract" "github.com/code-ready/crc/pkg/os" "github.com/code-ready/crc/pkg/os/windows/powershell" - "github.com/code-ready/crc/pkg/os/windows/secpol" - "github.com/code-ready/crc/pkg/os/windows/service" ) -var ( - startUpFolder = filepath.Join(constants.GetHomeDir(), "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup") - trayProcessName = constants.TrayBinaryName[:len(constants.TrayBinaryName)-4] -) +func checkIfTrayInstalled() error { + /* We want to force installation whenever setup is ran + * as we want the service config to point to the binary + * with which the setup command was issued. + */ -func checkIfDaemonServiceInstalled() error { - // We want to force installation whenever setup is ran - // as we want the service config to point to the binary - // with which the setup command was issued - return fmt.Errorf("Ignoring check and forcing installation of daemon service") + return fmt.Errorf("Ignoring check and forcing installation of System Tray") } -func fixDaemonServiceInstalled() error { - // only try to remove if a previous version exists - if service.IsInstalled(constants.DaemonServiceName) { - if service.IsRunning(constants.DaemonServiceName) { - _ = service.Stop(constants.DaemonServiceName) - } - _ = service.Delete(constants.DaemonServiceName) +func fixTrayInstalled() error { + /* To avoid asking for elevated privileges again and again + * this takes care of all the steps needed to have a running + * tray by invoking a ps script which does the following: + * a) Add logon as service permission for the user + * b) Create the daemon service and start it + * c) Add tray to startup folder and start it + */ + + tempDir, err := ioutil.TempDir("", "crc") + if err != nil { + logging.Error("Failed creating temporary directory for tray installation") + return err } - // get executables path + defer func() { + _ = goos.RemoveAll(tempDir) + }() + + // prepare the ps script binPath, err := goos.Executable() if err != nil { return fmt.Errorf("Unable to find the current executables location: %v", err) } binPathWithArgs := fmt.Sprintf("%s daemon", strings.TrimSpace(binPath)) - // get the account name - u, err := user.Current() - if err != nil { - return fmt.Errorf("Failed to get username: %v", err) - } - logging.Debug("Got username: ", u.Username) - accountName := strings.TrimSpace(u.Username) - // get the password from user password, err := input.PromptUserForSecret("Enter account login password for service installation", "This is the login password of your current account, needed to install the daemon service") if err != nil { return fmt.Errorf("Unable to get login password: %v", err) } - err = service.Create(constants.DaemonServiceName, binPathWithArgs, accountName, password) + + // sanitize password + password = escapeWindowsPassword(password) + + psScriptContent := genTrayInstallScript( + password, + tempDir, + binPathWithArgs, + constants.TrayBinaryPath, + constants.TrayShortcutName, + constants.DaemonServiceName, + ) + psFilePath := filepath.Join(tempDir, "trayInstallation.ps1") + + // write temporary ps script + if err = writePsScriptContentToFile(psScriptContent, psFilePath); err != nil { + return err + } + + // invoke the ps script + _, _, err = powershell.ExecuteAsAdmin("Installing System Tray for CodeReady Containers", psFilePath) + // wait for the script to finish executing + time.Sleep(2 * time.Second) if err != nil { - return fmt.Errorf("Failed to install CodeReady Containers daemon service: %v", err) + logging.Debug("Failed to execute tray installation script") + return err + } + + // check for 'success' file + if _, err = goos.Stat(filepath.Join(tempDir, "success")); goos.IsNotExist(err) { + return fmt.Errorf("Installation script didn't execute successfully: %v", err) } return nil } -func removeDaemonService() error { - // try to remove service if only it is installed - // this should remove unnecessary UAC prompts during cleanup - // service.IsInstalled doesn't need an admin shell to run - if service.IsInstalled(constants.DaemonServiceName) { - err := service.Stop(constants.DaemonServiceName) - if err != nil { - return fmt.Errorf("Failed to stop the daemon service: %v", err) - } - return service.Delete(constants.DaemonServiceName) +func escapeWindowsPassword(password string) string { + // escape specials characters (|`|$|"|') with '`' if present in password + if strings.Contains(password, "`") { + password = strings.Replace(password, "`", "``", -1) } - return nil + if strings.Contains(password, "$") { + password = strings.Replace(password, "$", "`$", -1) + } + if strings.Contains(password, "\"") { + password = strings.Replace(password, "\"", "`\"", -1) + } + if strings.Contains(password, "'") { + password = strings.Replace(password, "'", "`'", -1) + } + return password } -func checkIfDaemonServiceRunning() error { - if service.IsRunning(constants.DaemonServiceName) { +func removeTray() error { + trayProcessName := constants.TrayBinaryName[:len(constants.TrayBinaryName)-4] + + tempDir, err := ioutil.TempDir("", "crc") + if err != nil { + logging.Debug("Failed to create temporary directory for System Tray removal") + return nil + } + defer func() { + _ = goos.RemoveAll(tempDir) + }() + + psScriptContent := genTrayRemovalScript( + trayProcessName, + constants.TrayShortcutName, + constants.DaemonServiceName, + tempDir, + ) + psFilePath := filepath.Join(tempDir, "trayRemoval.ps1") + + // write script content to temporary file + if err = writePsScriptContentToFile(psScriptContent, psFilePath); err != nil { + logging.Debug(err) return nil } - return fmt.Errorf("CodeReady Containers dameon service is not running") -} -func fixDaemonServiceRunning() error { - return service.Start(constants.DaemonServiceName) + _, _, err = powershell.ExecuteAsAdmin("Uninstalling CodeReady Containers System Tray", psFilePath) + // wait for the script to finish executing + time.Sleep(2 * time.Second) + if err != nil { + logging.Debugf("Unable to execute System Tray uninstall script: %v", err) + } + return nil } func checkTrayBinaryExists() error { @@ -131,123 +181,27 @@ func fixTrayBinaryExists() error { return nil } -func checkTrayBinaryVersion() error { - versionCmd := `(Get-Item %s).VersionInfo.ProductVersion` - stdOut, stdErr, err := powershell.Execute(fmt.Sprintf(versionCmd, constants.TrayBinaryPath)) - if err != nil { - return fmt.Errorf("Failed to get the version of tray: %v: %s", err, stdErr) - } - currentTrayVersion := strings.TrimSpace(stdOut) - if currentTrayVersion != version.GetCRCWindowsTrayVersion() { - return fmt.Errorf("Current tray version doesn't match with expected version") - } - return nil -} - -func fixTrayBinaryVersion() error { - // If a tray is already running kill it - if err := checkTrayRunning(); err == nil { - cmd := `Stop-Process -Name "tray-windows"` - if _, _, err := powershell.Execute(cmd); err != nil { - logging.Debugf("Failed to kill running tray: %v", err) - } - } - return fixTrayBinaryExists() -} - -func checkTrayBinaryAddedToStartupFolder() error { - if os.FileExists(filepath.Join(startUpFolder, constants.TrayShortcutName)) { - return nil - } - return fmt.Errorf("Tray shortcut does not exists in startup folder") -} - -func fixTrayBinaryAddedToStartupFolder() error { - cmd := fmt.Sprintf( - "New-Item -ItemType SymbolicLink -Path \"%s\" -Name \"%s\" -Value \"%s\"", - startUpFolder, - constants.TrayShortcutName, - constants.TrayBinaryPath, - ) - _, _, err := powershell.ExecuteAsAdmin("Adding tray binary to startup applications", cmd) - if err != nil { - return fmt.Errorf("Failed to create shortcut of tray binary in startup folder: %v", err) - } - return nil -} - -func removeTrayBinaryFromStartupFolder() error { - err := goos.Remove(filepath.Join(startUpFolder, constants.TrayShortcutName)) - if err != nil { - logging.Warn("Failed to remove tray from startup folder: ", err) - } - return nil -} - -func checkTrayRunning() error { - cmd := fmt.Sprintf("Get-Process -Name \"%s\"", trayProcessName) - _, stdErr, err := powershell.Execute(cmd) - if err != nil { - return fmt.Errorf("Failed to check if the tray is running: %v", err) - } - if strings.Contains(stdErr, constants.TrayBinaryName) { - return fmt.Errorf("Tray binary is not running") - } - return nil -} - -func fixTrayRunning() error { - // #nosec G204 - err := exec.Command(constants.TrayBinaryPath).Start() +func writePsScriptContentToFile(psScriptContent, psFilePath string) error { + psFile, err := goos.Create(psFilePath) if err != nil { + logging.Debugf("Unable to create file to write scipt content: %v", err) return err } - return nil -} - -func stopTray() error { - if err := checkTrayRunning(); err != nil { - logging.Debug("Failed to check if a tray is running: ", err) - return nil - } - cmd := fmt.Sprintf("Stop-Process -Name %s", trayProcessName) - _, _, err := powershell.Execute(cmd) + defer psFile.Close() + // write the ps script + /* Add UTF-8 BOM at the beginning of the script so that Windows + * correctly detects the file encoding + */ + _, err = psFile.Write([]byte{0xef, 0xbb, 0xbf}) if err != nil { - logging.Warn("Failed to stop running tray: ", err) - } - return nil -} - -func checkUserHasServiceLogonEnabled() error { - username := getCurrentUsername() - enabled, err := secpol.UserAllowedToLogonAsService(username) - if enabled { - return nil + logging.Debugf("Unable to write script content to file: %v", err) + return err } - return fmt.Errorf("Failed to check if user is allowed to log on as service: %v", err) -} - -func fixUserHasServiceLogonEnabled() error { - // get the username - username := getCurrentUsername() - - return secpol.AllowUserToLogonAsService(username) -} - -func getCurrentUsername() string { - user, err := user.Current() + _, err = psFile.WriteString(psScriptContent) if err != nil { - logging.Debugf("Unable to get current user's name: %v", err) - return "" + logging.Debugf("Unable to write script content to file: %v", err) + return err } - return strings.TrimSpace(strings.Split(user.Username, "\\")[1]) -} - -func disableUserServiceLogon() error { - username := getCurrentUsername() - if err := secpol.RemoveLogonAsServiceUserRight(username); err != nil { - logging.Warn("Failed trying to remove log on as a service user right: ", err) - } return nil } diff --git a/pkg/crc/preflight/preflight_checks_tray_windows_test.go b/pkg/crc/preflight/preflight_checks_tray_windows_test.go new file mode 100644 index 0000000000..2f800383a6 --- /dev/null +++ b/pkg/crc/preflight/preflight_checks_tray_windows_test.go @@ -0,0 +1,29 @@ +package preflight + +import ( + "fmt" + "strings" + "testing" + + "github.com/code-ready/crc/pkg/os/windows/powershell" +) + +func TestEscapeWindowsPassword(t *testing.T) { + passwords := []string{ + "tes;t@\"_\\/", + "$kdhhjs;%&*'`", + "``````$''\"", + } + + for _, pass := range passwords { + op := escapeWindowsPassword(pass) + psCmd := fmt.Sprintf("$var=\"%s\"; $var", op) + stdOut, stdErr, err := powershell.Execute(psCmd) + if err != nil { + t.Errorf("Error while executing powershell command: %v: %v", err, stdErr) + } + if strings.TrimSpace(stdOut) != pass { + t.Errorf("Passwords don't match after escaping for powershell. Expected: %s, Actual: %s", pass, stdOut) + } + } +} diff --git a/pkg/crc/preflight/preflight_tray_powershell_windows.go b/pkg/crc/preflight/preflight_tray_powershell_windows.go new file mode 100644 index 0000000000..248b5f6c90 --- /dev/null +++ b/pkg/crc/preflight/preflight_tray_powershell_windows.go @@ -0,0 +1,170 @@ +package preflight + +import ( + "fmt" + "strings" +) + +var ( + trayInstallationScript = []string{ + `$ErrorActionPreference = "Stop"`, + `$password = "%s"`, + `$tempDir = "%s"`, + `$crcBinaryPath = "%s"`, + `$trayBinaryPath = "%s"`, + `$traySymlinkName = "%s"`, + `$serviceName = "%s"`, + `$currentUserSid = (Get-LocalUser -Name "$env:USERNAME").Sid.Value`, + `$startUpFolder = "$Env:USERPROFILE\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"`, + `function AddServiceLogonRightForCurrentUser()`, + `{`, + ` $securityTemplate = @"`, + `[Unicode]`, + `Unicode=yes`, + `[Version]`, + "signature=\"`$CHICAGO$\"", + `Revision=1`, + `[Privilege Rights]`, + `SeServiceLogonRight = {0}`, + `"@`, + ` SecEdit.exe /export /cfg $tempDir\secdef.inf /areas USER_RIGHTS`, + ` if ($LASTEXITCODE -ne 0)`, + ` {`, + ` exit 1`, + ` }`, + ` $userRights = Get-Content -Path $tempDir\secdef.inf`, + ` $serviceLogonUserRight = ($userRights | select-string -Pattern "SeServiceLogonRight\s=\s.*")`, + ` $sidsInServiceLogonRight = ($serviceLogonUserRight -split "=")[1].Trim()`, + ` $sidsArray = $sidsInServiceLogonRight -split ","`, + ` if (!($sidsArray.Contains($env:USERNAME) -or $sidsArray.Contains("*"+$currentUserSid)))`, + ` {`, + ` Write-Output "User doesn't have logon as service right, adding sid of $env:Username"`, + ` $sidsInServiceLogonRight += ",*$currentUserSid"`, + ` $templateContent = $securityTemplate -f "$sidsInServiceLogonRight"`, + ` Set-Content -Path $tempDir\secdef_fin.inf $templateContent`, + ` SecEdit.exe /configure /db $tempDir\tempdb.db /cfg $tempDir\secdef_fin.inf /areas USER_RIGHTS`, + ` if ($LASTEXITCODE -ne 0)`, + ` {`, + ` exit`, + ` }`, + ` }`, + `}`, + + `function CreateDaemonService()`, + `{`, + ` $secPass = ConvertTo-SecureString $password -AsPlainText -Force`, + ` $creds = New-Object pscredential ("$env:USERDOMAIN\$env:USERNAME", $secPass)`, + ` $params = @{`, + ` Name = "$serviceName"`, + ` BinaryPathName = "$crcBinaryPath daemon"`, + ` DisplayName = "$serviceName"`, + ` StartupType = "Automatic"`, + ` Description = "CodeReady Containers Daemon service for System Tray."`, + ` Credential = $creds`, + ` }`, + ` New-Service @params`, + `}`, + + `function StartDaemonService()`, + `{`, + ` Start-Service "CodeReady Containers"`, + `}`, + + `AddServiceLogonRightForCurrentUser`, + `sc.exe stop "$serviceName"`, + `sc.exe delete "$serviceName"`, + `CreateDaemonService`, + `StartDaemonService`, + `$ErrorActionPreference = "Continue"`, + `Stop-Process -Name tray-windows`, + `Remove-Item "$startUpFolder\$traySymlinkName"`, + + `$ErrorActionPreference = "Stop"`, + `New-Item -ItemType SymbolicLink -Path "$startUpFolder" -Name "$traySymlinkName" -Value "$trayBinaryPath"`, + `Start-Process -FilePath "$trayBinaryPath"`, + `New-Item -ItemType File -Path "$tempDir" -Name "success"`, + `Set-Content -Path $tempDir\success "blah blah"`, + } + + trayRemovalScript = []string{ + `$tempDir = "%s"`, + `$trayProcessName = "%s"`, + `$traySymlinkName = "%s"`, + `$serviceName = "%s"`, + `$startUpFolder = "$Env:USERPROFILE\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"`, + + `function RemoveUserFromServiceLogon`, + `{`, + ` $securityTemplate = @"`, + `[Unicode]`, + `Unicode=yes`, + `[Version]`, + "signature=\"`$CHICAGO$\"", + `Revision=1`, + `[Privilege Rights]`, + `SeServiceLogonRight = {0}`, + `"@`, + ` SecEdit.exe /export /cfg $tempDir\secdef.inf /areas USER_RIGHTS`, + ` if ($LASTEXITCODE -ne 0)`, + ` {`, + ` exit 1`, + ` }`, + + ` $userRights = Get-Content -Path $tempDir\secdef.inf`, + ` $serviceLogonUserRight = ($userRights | select-string -Pattern "SeServiceLogonRight\s=\s.*")`, + + ` $sidsInServiceLogonRight = ($serviceLogonUserRight -split "=")[1].Trim()`, + ` $sidsArray = $sidsInServiceLogonRight -split ","`, + ` $newSids = $sidsArray | Where-Object {$_ -ne $env:USERNAME}`, + ` $newSids = $newSids -Join ","`, + ` $templateContent = $securityTemplate -f "$newSids"`, + + ` Set-Content -Path $tempDir\secdef_fin.inf $templateContent`, + ` SecEdit.exe /configure /db $tempDir\tempdb.db /cfg $tempDir\secdef_fin.inf /areas USER_RIGHTS`, + `}`, + + `function DeleteDaemonService()`, + `{`, + ` sc.exe stop "$serviceName"`, + ` sc.exe delete "$serviceName"`, + `}`, + + `function RemoveTrayFromStartUpFolder()`, + `{`, + ` Stop-Process -Name "$trayProcessName"`, + ` Remove-Item "$startUpFolder\$traySymlinkName"`, + `}`, + + `RemoveUserFromServiceLogon`, + `DeleteDaemonService`, + `RemoveTrayFromStartUpFolder`, + } +) + +func getTrayInstallationScriptTemplate() string { + return strings.Join(trayInstallationScript, "\n") +} + +func getTrayRemovalScriptTemplate() string { + return strings.Join(trayRemovalScript, "\n") +} + +func genTrayInstallScript(password, tempDirPath, daemonCmd, trayBinaryPath, traySymlinkName, daemonServiceName string) string { + return fmt.Sprintf(getTrayInstallationScriptTemplate(), + password, + tempDirPath, + daemonCmd, + trayBinaryPath, + traySymlinkName, + daemonServiceName, + ) +} + +func genTrayRemovalScript(trayProcessName, traySymlinkName, daemonServiceName, tempDir string) string { + return fmt.Sprintf(getTrayRemovalScriptTemplate(), + tempDir, + trayProcessName, + traySymlinkName, + daemonServiceName, + ) +} diff --git a/pkg/crc/preflight/preflight_windows.go b/pkg/crc/preflight/preflight_windows.go index ddc4fe9ce9..d8154ba451 100644 --- a/pkg/crc/preflight/preflight_windows.go +++ b/pkg/crc/preflight/preflight_windows.go @@ -56,31 +56,6 @@ var hypervPreflightChecks = [...]Check{ } var traySetupChecks = [...]Check{ - { - checkDescription: "Checking if user is allowed to log on as a service", - check: checkUserHasServiceLogonEnabled, - fixDescription: "Enabling service log on for user", - fix: fixUserHasServiceLogonEnabled, - cleanupDescription: "Removing service log on permission from user", - cleanup: disableUserServiceLogon, - flags: SetupOnly, - }, - { - checkDescription: "Checking if daemon service is installed", - check: checkIfDaemonServiceInstalled, - fixDescription: "Installing daemon service", - fix: fixDaemonServiceInstalled, - cleanupDescription: "Removing daemon service if exists", - cleanup: removeDaemonService, - flags: SetupOnly, - }, - { - checkDescription: "Checking if daemon service is running", - check: checkIfDaemonServiceRunning, - fixDescription: "Starting daemon service", - fix: fixDaemonServiceRunning, - flags: SetupOnly, - }, { checkDescription: "Checking if tray binary is present", check: checkTrayBinaryExists, @@ -89,28 +64,12 @@ var traySetupChecks = [...]Check{ flags: SetupOnly, }, { - checkDescription: "Checking if tray binary is added to startup applications", - check: checkTrayBinaryAddedToStartupFolder, - fixDescription: "Adding tray binary to startup applications", - fix: fixTrayBinaryAddedToStartupFolder, - cleanupDescription: "Removing tray binary from startup folder if exists", - cleanup: removeTrayBinaryFromStartupFolder, - flags: SetupOnly, - }, - { - checkDescription: "Checking if tray version is correct", - check: checkTrayBinaryVersion, - fixDescription: "Caching correct tray version", - fix: fixTrayBinaryVersion, - flags: SetupOnly, - }, - { - checkDescription: "Checking if tray is running", - check: checkTrayRunning, - fixDescription: "Starting CodeReady Containers tray", - fix: fixTrayRunning, - cleanupDescription: "Stopping tray process if running", - cleanup: stopTray, + checkDescription: "Checking if tray is installed", + check: checkIfTrayInstalled, + fixDescription: "Installing CodeReady Containers tray", + fix: fixTrayInstalled, + cleanupDescription: "Uninstalling tray if installed", + cleanup: removeTray, flags: SetupOnly, }, }