From 3d571c0319465ee9ad10c45cda53c52a82293c24 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Fri, 5 Apr 2024 14:36:04 +0200 Subject: [PATCH] Use Stdin to pass secrets to ansible-playbook (#1911) * feat: pass secrets via stdin * feat: use pty * feat(pty): logs * feat(secrets): works * fix(secrets): use correct ask flag of ansible playbook * test(secrets): change tests --- db/AccessKey.go | 44 +++++++++-------------------- db_lib/AnsibleApp.go | 4 +-- db_lib/AnsiblePlaybook.go | 36 ++++++++++++++++++++--- db_lib/LocalApp.go | 2 +- go.mod | 1 + go.sum | 2 ++ services/tasks/LocalJob.go | 47 +++++++++++++++++++++++++------ services/tasks/TaskRunner_test.go | 10 +++---- 8 files changed, 94 insertions(+), 52 deletions(-) diff --git a/db/AccessKey.go b/db/AccessKey.go index ea76e3e2..aaee6e01 100644 --- a/db/AccessKey.go +++ b/db/AccessKey.go @@ -67,6 +67,8 @@ const ( type AccessKeyInstallation struct { InstallationKey int64 SshAgent *lib.SshAgent + Login string + Password string } func (key AccessKeyInstallation) Destroy() error { @@ -115,8 +117,6 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat return } - installationPath := installation.GetPath() - err = key.DeserializeSecret() if err != nil { @@ -132,44 +132,26 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat installation.SshAgent = &agent } case AccessKeyRoleAnsiblePasswordVault: - switch key.Type { - case AccessKeyLoginPassword: - err = os.WriteFile(installationPath, []byte(key.LoginPassword.Password), 0600) - } - case AccessKeyRoleAnsibleBecomeUser: - switch key.Type { - case AccessKeyLoginPassword: - content := make(map[string]string) - if len(key.LoginPassword.Login) > 0 { - content["ansible_become_user"] = key.LoginPassword.Login - } - content["ansible_become_password"] = key.LoginPassword.Password - var bytes []byte - bytes, err = json.Marshal(content) - if err != nil { - return - } - err = os.WriteFile(installationPath, bytes, 0600) - default: + if key.Type != AccessKeyLoginPassword { err = fmt.Errorf("access key type not supported for ansible user") } + installation.Password = key.LoginPassword.Password + case AccessKeyRoleAnsibleBecomeUser: + if key.Type != AccessKeyLoginPassword { + err = fmt.Errorf("access key type not supported for ansible user") + } + installation.Login = key.LoginPassword.Login + installation.Password = key.LoginPassword.Password case AccessKeyRoleAnsibleUser: switch key.Type { case AccessKeySSH: var agent lib.SshAgent agent, err = key.startSshAgent(logger) installation.SshAgent = &agent + installation.Login = key.LoginPassword.Login case AccessKeyLoginPassword: - content := make(map[string]string) - content["ansible_user"] = key.LoginPassword.Login - content["ansible_password"] = key.LoginPassword.Password - var bytes []byte - bytes, err = json.Marshal(content) - if err != nil { - return - } - err = os.WriteFile(installationPath, bytes, 0600) - + installation.Login = key.LoginPassword.Login + installation.Password = key.LoginPassword.Password default: err = fmt.Errorf("access key type not supported for ansible user") } diff --git a/db_lib/AnsibleApp.go b/db_lib/AnsibleApp.go index 3d3e2a1a..5cf56621 100644 --- a/db_lib/AnsibleApp.go +++ b/db_lib/AnsibleApp.go @@ -60,8 +60,8 @@ func (t *AnsibleApp) SetLogger(logger lib.Logger) lib.Logger { return logger } -func (t *AnsibleApp) Run(args []string, environmentVars *[]string, cb func(*os.Process)) error { - return t.Playbook.RunPlaybook(args, environmentVars, cb) +func (t *AnsibleApp) Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error { + return t.Playbook.RunPlaybook(args, environmentVars, inputs, cb) } func (t *AnsibleApp) Log(msg string) { diff --git a/db_lib/AnsiblePlaybook.go b/db_lib/AnsiblePlaybook.go index 6094fbcf..21da0bb2 100644 --- a/db_lib/AnsiblePlaybook.go +++ b/db_lib/AnsiblePlaybook.go @@ -5,6 +5,7 @@ import ( "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/lib" "github.com/ansible-semaphore/semaphore/util" + "github.com/creack/pty" "os" "os/exec" "strings" @@ -53,14 +54,41 @@ func (p AnsiblePlaybook) runCmd(command string, args []string) error { return cmd.Run() } -func (p AnsiblePlaybook) RunPlaybook(args []string, environmentVars *[]string, cb func(*os.Process)) error { +func (p AnsiblePlaybook) RunPlaybook(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error { cmd := p.makeCmd("ansible-playbook", args, environmentVars) p.Logger.LogCmd(cmd) - cmd.Stdin = strings.NewReader("") - err := cmd.Start() + + ptmx, err := pty.Start(cmd) + if err != nil { - return err + panic(err) } + + go func() { + + b := make([]byte, 100) + + var e error + + for { + var n int + n, e = ptmx.Read(b) + if e != nil { + break + } + + s := strings.TrimSpace(string(b[0:n])) + + for k, v := range inputs { + if strings.HasPrefix(s, k) { + _, _ = ptmx.WriteString(v + "\n") + } + } + } + + }() + + defer func() { _ = ptmx.Close() }() cb(cmd.Process) return cmd.Wait() } diff --git a/db_lib/LocalApp.go b/db_lib/LocalApp.go index fc74d1ad..e2b1ecdb 100644 --- a/db_lib/LocalApp.go +++ b/db_lib/LocalApp.go @@ -8,5 +8,5 @@ import ( type LocalApp interface { SetLogger(logger lib.Logger) lib.Logger InstallRequirements() error - Run(args []string, environmentVars *[]string, cb func(*os.Process)) error + Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error } diff --git a/go.mod b/go.mod index 0578826e..fd9ef02f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/Masterminds/squirrel v1.5.4 github.com/coreos/go-oidc/v3 v3.9.0 + github.com/creack/pty v1.1.21 github.com/go-git/go-git/v5 v5.11.0 github.com/go-gorp/gorp/v3 v3.1.0 github.com/go-ldap/ldap/v3 v3.4.6 diff --git a/go.sum b/go.sum index c2f9b45f..0fe0dac8 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/services/tasks/LocalJob.go b/services/tasks/LocalJob.go index 764ce840..2babf73f 100644 --- a/services/tasks/LocalJob.go +++ b/services/tasks/LocalJob.go @@ -175,7 +175,11 @@ func (t *LocalJob) getTerraformArgs(username string, incomingVersion *string) (a } // nolint: gocyclo -func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (args []string, err error) { +func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (args []string, inputs map[string]string, err error) { + + inputMap := make(map[db.AccessKeyRole]string) + inputs = make(map[string]string) + playbookName := t.Task.Playbook if playbookName == "" { playbookName = t.Template.Playbook @@ -202,12 +206,17 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar if t.Inventory.SSHKeyID != nil { switch t.Inventory.SSHKey.Type { case db.AccessKeySSH: - //args = append(args, "--extra-vars={\"ansible_ssh_private_key_file\": \""+t.inventory.SSHKey.GetPath()+"\"}") - if t.Inventory.SSHKey.SshKey.Login != "" { - args = append(args, "--extra-vars={\"ansible_user\": \""+t.Inventory.SSHKey.SshKey.Login+"\"}") + if t.sshKeyInstallation.Login != "" { + args = append(args, "--user", t.sshKeyInstallation.Login) } case db.AccessKeyLoginPassword: - args = append(args, "--extra-vars=@"+t.sshKeyInstallation.GetPath()) + if t.sshKeyInstallation.Login != "" { + args = append(args, "--user", t.sshKeyInstallation.Login) + } + if t.sshKeyInstallation.Password != "" { + args = append(args, "--ask-pass") + inputMap[db.AccessKeyRoleAnsibleUser] = t.sshKeyInstallation.Password + } case db.AccessKeyNone: default: err = fmt.Errorf("access key does not suite for inventory's user credentials") @@ -218,7 +227,13 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar if t.Inventory.BecomeKeyID != nil { switch t.Inventory.BecomeKey.Type { case db.AccessKeyLoginPassword: - args = append(args, "--extra-vars=@"+t.becomeKeyInstallation.GetPath()) + if t.sshKeyInstallation.Login != "" { + args = append(args, "--user", t.becomeKeyInstallation.Login) + } + if t.becomeKeyInstallation.Password != "" { + args = append(args, "--ask-become-pass") + inputMap[db.AccessKeyRoleAnsibleBecomeUser] = t.sshKeyInstallation.Password + } case db.AccessKeyNone: default: err = fmt.Errorf("access key does not suite for inventory's sudo user credentials") @@ -239,7 +254,8 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar } if t.Template.VaultKeyID != nil { - args = append(args, "--vault-password-file", t.vaultFileInstallation.GetPath()) + args = append(args, "--ask-vault-pass") + inputMap[db.AccessKeyRoleAnsiblePasswordVault] = t.vaultFileInstallation.Password } extraVars, err := t.getEnvironmentExtraVarsJSON(username, incomingVersion) @@ -277,6 +293,18 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar args = append(args, taskExtraArgs...) args = append(args, playbookName) + if line, ok := inputMap[db.AccessKeyRoleAnsibleUser]; ok { + inputs["SSH password:"] = line + } + + if line, ok := inputMap[db.AccessKeyRoleAnsibleBecomeUser]; ok { + inputs["BECOME password"] = line + } + + if line, ok := inputMap[db.AccessKeyRoleAnsiblePasswordVault]; ok { + inputs["Vault password:"] = line + } + return } @@ -294,10 +322,11 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) { }() var args []string + var inputs map[string]string switch t.Template.App { case db.TemplateAnsible: - args, err = t.getPlaybookArgs(username, incomingVersion) + args, inputs, err = t.getPlaybookArgs(username, incomingVersion) default: panic("unknown template app") } @@ -315,7 +344,7 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) { environmentVariables = append(environmentVariables, fmt.Sprintf("SSH_AUTH_SOCK=%s", t.sshKeyInstallation.SshAgent.SocketFile)) } - return t.App.Run(args, &environmentVariables, func(p *os.Process) { + return t.App.Run(args, &environmentVariables, inputs, func(p *os.Process) { t.Process = p }) diff --git a/services/tasks/TaskRunner_test.go b/services/tasks/TaskRunner_test.go index 371dc5c4..aec24779 100644 --- a/services/tasks/TaskRunner_test.go +++ b/services/tasks/TaskRunner_test.go @@ -299,7 +299,7 @@ func TestTaskGetPlaybookArgs(t *testing.T) { }, } - args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) + args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) if err != nil { t.Fatal(err) @@ -355,14 +355,14 @@ func TestTaskGetPlaybookArgs2(t *testing.T) { }, } - args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) + args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) if err != nil { t.Fatal(err) } res := strings.Join(args, " ") - if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" { + if res != "-i /tmp/inventory_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" { t.Fatal("incorrect result") } } @@ -411,14 +411,14 @@ func TestTaskGetPlaybookArgs3(t *testing.T) { }, } - args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) + args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil) if err != nil { t.Fatal(err) } res := strings.Join(args, " ") - if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" { + if res != "-i /tmp/inventory_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" { t.Fatal("incorrect result") } }