From 6641489a6c7fa8f30e3d9cb04bf85f0c80b0d81e Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 3 Apr 2024 14:28:16 -0300 Subject: [PATCH 1/7] use stdin to send the password to ansible-vault command --- .../plugins/ansible/ansible/AnsibleVault.java | 22 ++++++++++++---- .../plugins/ansible/util/ProcessExecutor.java | 25 ++++++++++--------- 2 files changed, 30 insertions(+), 17 deletions(-) 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..54f99893 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -73,10 +73,6 @@ public String encryptVariable(String key, //send values to STDIN in order List stdinVariables = new ArrayList<>(); - stdinVariables.add(content); - - Map env = new HashMap<>(); - env.put("VAULT_ID_SECRET", masterPassword); Process proc = null; @@ -85,9 +81,25 @@ public String encryptVariable(String key, .baseDirectory(baseDirectory.toFile()) .stdinVariables(stdinVariables) .redirectErrorStream(true) - .environmentVariables(env) .build().run(); + OutputStream stdin = proc.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin)); + + //send master password + Thread.sleep(1500); + writer.write(masterPassword); + writer.newLine(); + writer.flush(); + + //send content to encrypt + Thread.sleep(1500); + writer.write(content); + writer.flush(); + + writer.close(); + stdin.close(); + StringBuilder stringBuilder = new StringBuilder(); final InputStream stdoutInputStream = proc.getInputStream(); 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; } From f28341945789bdbac4ba827a9007cca3d055d444 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 4 Apr 2024 17:44:35 -0300 Subject: [PATCH 2/7] use a file to check prompt --- .../plugins/ansible/ansible/AnsibleVault.java | 108 +++++++++++++----- src/main/resources/vault-client.py | 16 ++- 2 files changed, 90 insertions(+), 34 deletions(-) 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 54f99893..86fa6042 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 @@ -23,6 +24,7 @@ public class AnsibleVault { private boolean debug; private Path baseDirectory; private Path ansibleBinariesDirectory; + private String executionId; public final String ANSIBLE_VAULT_COMMAND = "ansible-vault"; @@ -71,58 +73,60 @@ public String encryptVariable(String key, System.out.println("encryptVariable " + key + ": " + procArgs); } - //send values to STDIN in order - List stdinVariables = new ArrayList<>(); + File promptFile = File.createTempFile("vault-prompt", ".log"); + + Map env = new HashMap<>(); + env.put("LOG_PATH", promptFile.getAbsolutePath()); Process proc = null; try { proc = ProcessExecutor.builder().procArgs(procArgs) .baseDirectory(baseDirectory.toFile()) - .stdinVariables(stdinVariables) + .environmentVariables(env) .redirectErrorStream(true) .build().run(); - OutputStream stdin = proc.getOutputStream(); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin)); + final InputStream proccesInputStream = proc.getInputStream(); + final OutputStream processOutputStream = proc.getOutputStream(); - //send master password - Thread.sleep(1500); - writer.write(masterPassword); - writer.newLine(); - writer.flush(); + //capture output thread + Callable readOutputTask = () -> { + return readOutput(proccesInputStream); + }; - //send content to encrypt - Thread.sleep(1500); - writer.write(content); - writer.flush(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(readOutputTask); - writer.close(); - stdin.close(); + Thread stdinThread = new Thread(() -> sendValuesStdin(processOutputStream, masterPassword, content)); - StringBuilder stringBuilder = new StringBuilder(); - - final InputStream stdoutInputStream = proc.getInputStream(); - final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + //wait for prompt + boolean promptFound = false; + while (!promptFound) { + BufferedReader reader = new BufferedReader(new FileReader(promptFile)); + String currentLine = reader.readLine(); + if(currentLine!=null && currentLine.contains("Enter Password:")){ + promptFound = true; - 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"); + //send password / content + stdinThread.start(); + reader.close(); + }else{ + Thread.sleep(1500); } } int exitCode = proc.waitFor(); + //get encrypted value + String result = future.get(); + executor.shutdown(); + if (exitCode != 0) { System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); return null; } - return stringBuilder.toString(); + return result; } catch (Exception e) { System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); @@ -132,9 +136,55 @@ public String encryptVariable(String key, if (proc != null) { proc.destroy(); } + + if(promptFile!=null && promptFile.delete()){ + promptFile.deleteOnExit(); + } } } + 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("problem with executing program", 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/resources/vault-client.py b/src/main/resources/vault-client.py index 4c2c253b..3709cf9e 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -1,13 +1,19 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3 -u 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() From c7098be8cf0d58e9a0cfbcab583589179c826547 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 4 Apr 2024 20:43:41 -0300 Subject: [PATCH 3/7] fix password script --- src/main/resources/vault-client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 3709cf9e..61a5b921 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 -u +#!/usr/bin/env python3 import sys import os import getpass From aa5f07d85f401b5af0f50107179889379b3ce824 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 5 Apr 2024 09:02:00 -0300 Subject: [PATCH 4/7] clean --- .../com/rundeck/plugins/ansible/ansible/AnsibleVault.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 86fa6042..22ad6a2b 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -24,7 +24,6 @@ public class AnsibleVault { private boolean debug; private Path baseDirectory; private Path ansibleBinariesDirectory; - private String executionId; public final String ANSIBLE_VAULT_COMMAND = "ansible-vault"; @@ -107,7 +106,6 @@ public String encryptVariable(String key, String currentLine = reader.readLine(); if(currentLine!=null && currentLine.contains("Enter Password:")){ promptFound = true; - //send password / content stdinThread.start(); reader.close(); @@ -162,7 +160,7 @@ String readOutput(InputStream proccesInputStream) { } return stringBuilder.toString(); } catch (Throwable e) { - throw new RuntimeException("problem with executing program", e); + throw new RuntimeException("error reading output from ansible-vault", e); } } From 5ded806c2f39ad101ef80a06d7c96973a0df6541 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 5 Apr 2024 13:24:59 -0300 Subject: [PATCH 5/7] add timeout for ansible vault prompt add unit test --- .../plugins/ansible/ansible/AnsibleVault.java | 31 ++++++++--- .../ansible/ansible/AnsibleVaultSpec.groovy | 52 +++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVaultSpec.groovy 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 22ad6a2b..12f8f5b1 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -27,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(); @@ -63,6 +70,11 @@ public String encryptVariable(String key, if (ansibleBinariesDirectory != null) { ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); } + + if(processExecutorBuilder==null){ + processExecutorBuilder = ProcessExecutor.builder(); + } + procArgs.add(ansibleCommand); procArgs.add("encrypt_string"); procArgs.add("--vault-id"); @@ -80,7 +92,7 @@ public String encryptVariable(String key, Process proc = null; try { - proc = ProcessExecutor.builder().procArgs(procArgs) + proc = processExecutorBuilder.procArgs(procArgs) .baseDirectory(baseDirectory.toFile()) .environmentVariables(env) .redirectErrorStream(true) @@ -99,9 +111,12 @@ public String encryptVariable(String key, Thread stdinThread = new Thread(() -> sendValuesStdin(processOutputStream, masterPassword, content)); + long start = System.currentTimeMillis(); + long end = start + 60 * 1000; + //wait for prompt boolean promptFound = false; - while (!promptFound) { + while (!promptFound && System.currentTimeMillis() < end) { BufferedReader reader = new BufferedReader(new FileReader(promptFile)); String currentLine = reader.readLine(); if(currentLine!=null && currentLine.contains("Enter Password:")){ @@ -114,6 +129,10 @@ public String encryptVariable(String key, } } + if(!promptFound){ + throw new RuntimeException("Failed to find prompt for ansible-vault"); + } + int exitCode = proc.waitFor(); //get encrypted value @@ -121,14 +140,12 @@ public String encryptVariable(String key, executor.shutdown(); if (exitCode != 0) { - System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); - return null; + throw new RuntimeException("ERROR: encryptFileAnsibleVault:" + procArgs); } 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) { 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") + } + +} From 21d9908ce6cb626b0c3e951aadd99740355fb0a2 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 5 Apr 2024 14:09:38 -0300 Subject: [PATCH 6/7] improve clean up process in AnsibleVault --- .../rundeck/plugins/ansible/ansible/AnsibleVault.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 12f8f5b1..23053d0f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -90,6 +90,7 @@ public String encryptVariable(String key, env.put("LOG_PATH", promptFile.getAbsolutePath()); Process proc = null; + ExecutorService executor = null; try { proc = processExecutorBuilder.procArgs(procArgs) @@ -106,7 +107,7 @@ public String encryptVariable(String key, return readOutput(proccesInputStream); }; - ExecutorService executor = Executors.newSingleThreadExecutor(); + executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(readOutputTask); Thread stdinThread = new Thread(() -> sendValuesStdin(processOutputStream, masterPassword, content)); @@ -137,7 +138,6 @@ public String encryptVariable(String key, //get encrypted value String result = future.get(); - executor.shutdown(); if (exitCode != 0) { throw new RuntimeException("ERROR: encryptFileAnsibleVault:" + procArgs); @@ -152,9 +152,13 @@ public String encryptVariable(String key, proc.destroy(); } - if(promptFile!=null && promptFile.delete()){ + if(promptFile!=null && !promptFile.delete()){ promptFile.deleteOnExit(); } + + if(executor!=null){ + executor.shutdown(); + } } } From 306104815a2be01fa37d88893d8ed193b33a2abc Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 5 Apr 2024 15:03:42 -0300 Subject: [PATCH 7/7] improve wait for prompt --- .../plugins/ansible/ansible/AnsibleVault.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) 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 23053d0f..1384a03f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -65,7 +65,6 @@ 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(); @@ -75,10 +74,7 @@ public String encryptVariable(String key, processExecutorBuilder = ProcessExecutor.builder(); } - procArgs.add(ansibleCommand); - procArgs.add("encrypt_string"); - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); + List procArgs = List.of(ansibleCommand, "encrypt_string", "--vault-id", "internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); if(debug){ System.out.println("encryptVariable " + key + ": " + procArgs); @@ -112,23 +108,21 @@ public String encryptVariable(String key, 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)); - //wait for prompt - boolean promptFound = false; - while (!promptFound && System.currentTimeMillis() < end) { - 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(); - }else{ - Thread.sleep(1500); } } + reader.close(); if(!promptFound){ throw new RuntimeException("Failed to find prompt for ansible-vault");