From a7b407eab6afa1e4fd6574199e7bc4c33b9465f2 Mon Sep 17 00:00:00 2001 From: Matt Dainty Date: Thu, 6 Dec 2018 15:09:57 +0000 Subject: [PATCH] Naive support for elevated support for puppet-server provisioner This commit just lifts the various bits out of the powershell provisioner. --- provisioner/puppet-server/elevated.go | 100 +++++++++++++++++ provisioner/puppet-server/provisioner.go | 137 ++++++++++++++++++++++- 2 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 provisioner/puppet-server/elevated.go diff --git a/provisioner/puppet-server/elevated.go b/provisioner/puppet-server/elevated.go new file mode 100644 index 000000000..5890a2aa0 --- /dev/null +++ b/provisioner/puppet-server/elevated.go @@ -0,0 +1,100 @@ +package puppetserver + +import ( + "text/template" +) + +type elevatedOptions struct { + User string + Password string + TaskName string + TaskDescription string + LogFile string + XMLEscapedCommand string +} + +var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(` +$name = "{{.TaskName}}" +$log = [System.Environment]::ExpandEnvironmentVariables("{{.LogFile}}") +$s = New-Object -ComObject "Schedule.Service" +$s.Connect() +$t = $s.NewTask($null) +$xml = [xml]@' + + + + {{.TaskDescription}} + + + + {{.User}} + Password + HighestAvailable + + + + IgnoreNew + false + false + true + false + false + + false + false + + true + true + false + false + false + PT24H + 4 + + + + cmd + /c {{.XMLEscapedCommand}} + + + +'@ +$logon_type = 1 +$password = "{{.Password}}" +if ($password.Length -eq 0) { + $logon_type = 5 + $password = $null + $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI) + $node = $xml.SelectSingleNode("/ns:Task/ns:Principals/ns:Principal/ns:LogonType", $ns) + $node.ParentNode.RemoveChild($node) | Out-Null +} +$t.XmlText = $xml.OuterXml +if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"} +$f = $s.GetFolder("\") +$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", $password, $logon_type, $null) | Out-Null +$t = $f.GetTask("\$name") +$t.Run($null) | Out-Null +$timeout = 10 +$sec = 0 +while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) { + Start-Sleep -s 1 + $sec++ +} + +$line = 0 +do { + Start-Sleep -m 100 + if (Test-Path $log) { + Get-Content $log | select -skip $line | ForEach { + $line += 1 + Write-Output "$_" + } + } +} while (!($t.state -eq 3)) +$result = $t.LastTaskResult +if (Test-Path $log) { + Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null +} +[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null +exit $result`)) diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index b7ec34ea7..4612606fe 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -3,18 +3,30 @@ package puppetserver import ( + "bytes" + "encoding/xml" "fmt" + "log" "os" "path/filepath" "strings" "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/uuid" + commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/provisioner" "github.com/hashicorp/packer/template/interpolate" ) +var psEscape = strings.NewReplacer( + "$", "`$", + "\"", "`\"", + "`", "``", + "'", "`'", +) + type Config struct { common.PackerConfig `mapstructure:",squash"` ctx interpolate.Context @@ -63,6 +75,12 @@ type Config struct { // The directory from which the command will be executed. // Packer requires the directory to exist when running puppet. WorkingDir string `mapstructure:"working_directory"` + + // Instructs the communicator to run the remote script as a Windows + // scheduled task, effectively elevating the remote user by impersonating + // a logged-in user + ElevatedUser string `mapstructure:"elevated_user"` + ElevatedPassword string `mapstructure:"elevated_password"` } type guestOSTypeConfig struct { @@ -112,6 +130,7 @@ var guestOSTypeConfigs = map[string]guestOSTypeConfig{ type Provisioner struct { config Config + communicator packer.Communicator guestOSTypeConfig guestOSTypeConfig guestCommands *provisioner.GuestCommands } @@ -129,7 +148,17 @@ type ExecuteTemplate struct { WorkingDir string } +type EnvVarsTemplate struct { + WinRMPassword string +} + func (p *Provisioner) Prepare(raws ...interface{}) error { + // Create passthrough for winrm password so we can fill it in once we know + // it + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: `{{.WinRMPassword}}`, + } + err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, @@ -210,6 +239,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say("Provisioning with Puppet...") + p.communicator = comm ui.Message("Creating Puppet staging directory...") if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { return fmt.Errorf("Error creating staging directory: %s", err) @@ -269,6 +299,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + if p.config.ElevatedUser != "" { + command, err = p.createCommandTextPrivileged(command) + if err != nil { + return err + } + } + cmd := &packer.RemoteCmd{ Command: command, } @@ -321,10 +358,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri } func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("rm -fr '%s'", dir), - } - + cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -349,3 +383,98 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } + +func getWinRMPassword(buildName string) string { + winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName) + packer.LogSecretFilter.Set(winRMPass) + return winRMPass +} + +func (p *Provisioner) createCommandTextPrivileged(input string) (output string, err error) { + // OK so we need an elevated shell runner to wrap our command, this is + // going to have its own path generate the script and update the command + // runner in the process + path, err := p.generateElevatedRunner(input) + if err != nil { + return "", fmt.Errorf("Error generating elevated runner: %s", err) + } + + // Return the path to the elevated shell wrapper + output = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) + + return output, err +} + +func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { + log.Printf("Building elevated command wrapper for: %s", command) + + var buffer bytes.Buffer + + // Output from the elevated command cannot be returned directly to the + // Packer console. In order to be able to view output from elevated + // commands and scripts an indirect approach is used by which the commands + // output is first redirected to file. The output file is then 'watched' + // by Packer while the elevated command is running and any content + // appearing in the file is written out to the console. Below the portion + // of command required to redirect output from the command to file is + // built and appended to the existing command string + taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + // Only use %ENVVAR% format for environment variables when setting the log + // file path; Do NOT use $env:ENVVAR format as it won't be expanded + // correctly in the elevatedTemplate + logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out" + command += fmt.Sprintf(" > %s 2>&1", logFile) + + // elevatedTemplate wraps the command in a single quoted XML text string + // so we need to escape characters considered 'special' in XML. + err = xml.EscapeText(&buffer, []byte(command)) + if err != nil { + return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err) + } + escapedCommand := buffer.String() + log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand) + buffer.Reset() + + // Escape chars special to PowerShell in the ElevatedUser string + escapedElevatedUser := psEscape.Replace(p.config.ElevatedUser) + if escapedElevatedUser != p.config.ElevatedUser { + log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", + p.config.ElevatedUser, escapedElevatedUser) + } + // Replace ElevatedPassword for winrm users who used this feature + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: getWinRMPassword(p.config.PackerBuildName), + } + + p.config.ElevatedPassword, _ = interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) + + // Escape chars special to PowerShell in the ElevatedPassword string + escapedElevatedPassword := psEscape.Replace(p.config.ElevatedPassword) + if escapedElevatedPassword != p.config.ElevatedPassword { + log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", + p.config.ElevatedPassword, escapedElevatedPassword) + } + + // Generate command + err = elevatedTemplate.Execute(&buffer, elevatedOptions{ + User: escapedElevatedUser, + Password: escapedElevatedPassword, + TaskName: taskName, + TaskDescription: "Packer elevated task", + LogFile: logFile, + XMLEscapedCommand: escapedCommand, + }) + + if err != nil { + fmt.Printf("Error creating elevated template: %s", err) + return "", err + } + uuid := uuid.TimeOrderedUUID() + path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid) + log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path) + err = p.communicator.Upload(path, &buffer, nil) + if err != nil { + return "", fmt.Errorf("Error preparing elevated powershell script: %s", err) + } + return path, err +}