diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java index ce7519f1..1384a03f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -13,6 +13,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.*; +import java.util.concurrent.*; @Data @Builder @@ -26,19 +27,26 @@ public class AnsibleVault { public final String ANSIBLE_VAULT_COMMAND = "ansible-vault"; + private ProcessExecutor.ProcessExecutorBuilder processExecutorBuilder; + public boolean checkAnsibleVault() { List procArgs = new ArrayList<>(); String ansibleCommand = ANSIBLE_VAULT_COMMAND; if (ansibleBinariesDirectory != null) { ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); } + + if(processExecutorBuilder==null){ + processExecutorBuilder = ProcessExecutor.builder(); + } + procArgs.add(ansibleCommand); procArgs.add("--version"); Process proc = null; try { - proc = ProcessExecutor.builder().procArgs(procArgs) + proc = processExecutorBuilder.procArgs(procArgs) .redirectErrorStream(true) .build().run(); @@ -57,72 +65,139 @@ public boolean checkAnsibleVault() { public String encryptVariable(String key, String content ) throws IOException { - List procArgs = new ArrayList<>(); String ansibleCommand = ANSIBLE_VAULT_COMMAND; if (ansibleBinariesDirectory != null) { ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); } - procArgs.add(ansibleCommand); - procArgs.add("encrypt_string"); - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); + + if(processExecutorBuilder==null){ + processExecutorBuilder = ProcessExecutor.builder(); + } + + List procArgs = List.of(ansibleCommand, "encrypt_string", "--vault-id", "internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); if(debug){ System.out.println("encryptVariable " + key + ": " + procArgs); } - //send values to STDIN in order - List stdinVariables = new ArrayList<>(); - stdinVariables.add(content); + File promptFile = File.createTempFile("vault-prompt", ".log"); Map env = new HashMap<>(); - env.put("VAULT_ID_SECRET", masterPassword); + env.put("LOG_PATH", promptFile.getAbsolutePath()); Process proc = null; + ExecutorService executor = null; try { - proc = ProcessExecutor.builder().procArgs(procArgs) + proc = processExecutorBuilder.procArgs(procArgs) .baseDirectory(baseDirectory.toFile()) - .stdinVariables(stdinVariables) - .redirectErrorStream(true) .environmentVariables(env) + .redirectErrorStream(true) .build().run(); - StringBuilder stringBuilder = new StringBuilder(); + final InputStream proccesInputStream = proc.getInputStream(); + final OutputStream processOutputStream = proc.getOutputStream(); - final InputStream stdoutInputStream = proc.getInputStream(); - final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + //capture output thread + Callable readOutputTask = () -> { + return readOutput(proccesInputStream); + }; - String line1 = null; - boolean capture = false; - while ((line1 = stdoutReader.readLine()) != null) { - if (line1.toLowerCase().contains("!vault")) { - capture = true; - } - if (capture) { - stringBuilder.append(line1).append("\n"); + executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(readOutputTask); + + Thread stdinThread = new Thread(() -> sendValuesStdin(processOutputStream, masterPassword, content)); + + //wait for prompt + boolean promptFound = false; + long start = System.currentTimeMillis(); + long end = start + 60 * 1000; + BufferedReader reader = new BufferedReader(new FileReader(promptFile)); + + while (!promptFound && System.currentTimeMillis() < end){ + String currentLine = reader.readLine(); + if(currentLine!=null && currentLine.contains("Enter Password:")){ + promptFound = true; + //send password / content + stdinThread.start(); } } + reader.close(); + + if(!promptFound){ + throw new RuntimeException("Failed to find prompt for ansible-vault"); + } int exitCode = proc.waitFor(); + //get encrypted value + String result = future.get(); + if (exitCode != 0) { - System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); - return null; + throw new RuntimeException("ERROR: encryptFileAnsibleVault:" + procArgs); } - return stringBuilder.toString(); + return result; } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - return null; + throw new RuntimeException("Failed to encrypt variable: " + e.getMessage()); } finally { // Make sure to always cleanup on failure and success if (proc != null) { proc.destroy(); } + + if(promptFile!=null && !promptFile.delete()){ + promptFile.deleteOnExit(); + } + + if(executor!=null){ + executor.shutdown(); + } + } + } + + String readOutput(InputStream proccesInputStream) { + try ( + InputStreamReader isr = new InputStreamReader(proccesInputStream); + BufferedReader stdoutReader = new BufferedReader(isr); + ) { + + StringBuilder stringBuilder = new StringBuilder(); + String line1 = null; + boolean capture = false; + while ((line1 = stdoutReader.readLine()) != null) { + if (line1.toLowerCase().contains("!vault")) { + capture = true; + } + if (capture) { + stringBuilder.append(line1).append("\n"); + } + } + return stringBuilder.toString(); + } catch (Throwable e) { + throw new RuntimeException("error reading output from ansible-vault", e); } } + void sendValuesStdin(OutputStream stdin, String masterPassword, String content){ + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin))) { + //send master password + writer.write(masterPassword); + writer.newLine(); + writer.flush(); + + //send content to encrypt + Thread.sleep(1500); + writer.write(content); + writer.flush(); + + writer.close(); + stdin.close(); + + } catch (Throwable e) { + throw new RuntimeException("error sending stdin for ansible-vault", e); + } + } public static File createVaultScriptAuth(String suffix) throws IOException { File tempInternalVaultFile = File.createTempFile("ansible-runner", suffix + "-client.py"); diff --git a/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java b/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java index f3590f2a..131de969 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java @@ -2,11 +2,8 @@ import lombok.Builder; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; +import java.io.*; import java.util.List; -import java.io.File; import java.util.Map; @Builder @@ -42,22 +39,26 @@ public Process run() throws IOException { Process proc = processBuilder.start(); - OutputStream stdin = proc.getOutputStream(); - OutputStreamWriter stdinw = new OutputStreamWriter(stdin); - - if (stdinVariables != null) { + if (stdinVariables != null && !stdinVariables.isEmpty()){ + OutputStream stdin = proc.getOutputStream(); + OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + BufferedWriter writer = new BufferedWriter(stdinw); try { for (String stdinVariable : stdinVariables) { - stdinw.write(stdinVariable); + writer.write(stdinVariable); + writer.newLine(); + writer.flush(); } - stdinw.flush(); + } catch (Exception e) { System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); } + writer.close(); + stdinw.close(); + stdin.close(); } - stdinw.close(); - stdin.close(); + return proc; } diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 4c2c253b..61a5b921 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -2,12 +2,18 @@ import sys import os import getpass +import logging +from logging import handlers -secret=os.getenv('VAULT_ID_SECRET', None) +log = logging.getLogger('') +log.setLevel(logging.DEBUG) -if secret: - sys.stdout.write('%s\n' % (secret)) - sys.exit(0) +log_path=os.getenv('LOG_PATH', None) + +if log_path: + fh = handlers.RotatingFileHandler(log_path) + log.addHandler(fh) + log.info("Enter Password:") if sys.stdin.isatty(): secret = getpass.getpass() diff --git a/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVaultSpec.groovy b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVaultSpec.groovy new file mode 100644 index 00000000..574e0f67 --- /dev/null +++ b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVaultSpec.groovy @@ -0,0 +1,52 @@ +package com.rundeck.plugins.ansible.ansible + +import com.rundeck.plugins.ansible.util.ProcessExecutor +import spock.lang.Specification + +import java.nio.file.Path + +class AnsibleVaultSpec extends Specification{ + + def "prompt message not found finished with timeout"() { + given: + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + File passwordScript = File.createTempFile("password", ".python") + Path baseDirectory = Path.of(passwordScript.getParentFile().getPath()) + + when: + def vault = AnsibleVault.builder() + .processExecutorBuilder(processBuilder) + .vaultPasswordScriptFile(passwordScript) + .baseDirectory(baseDirectory) + .build() + + def key = "password" + def content = "1234" + def result = vault.encryptVariable(key, content) + + then: + 1* processBuilder.procArgs(_) >> processBuilder + 1* processBuilder.baseDirectory(_) >> processBuilder + 1* processBuilder.environmentVariables(_) >> processBuilder + 1* processBuilder.redirectErrorStream(_) >> processBuilder + + def e = thrown(RuntimeException) + e.message.contains("Failed to find prompt for ansible-vault") + } + +}