From a2237d54c69b74a59160fef000ab22180076c785 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 14 Jul 2023 19:18:04 -0400 Subject: [PATCH 1/5] add key storage support for resource model add ProxySecretBundleCreator to resource model --- .../plugin/AnsibleResourceModelSource.java | 125 +++++++++++++++++- .../AnsibleResourceModelSourceFactory.java | 19 ++- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index e622237b..35240de9 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -1,5 +1,11 @@ package com.rundeck.plugins.ansible.plugin; +import com.dtolabs.rundeck.core.execution.proxy.DefaultSecretBundle; +import com.dtolabs.rundeck.core.execution.proxy.ProxySecretBundleCreator; +import com.dtolabs.rundeck.core.execution.proxy.SecretBundle; +import com.dtolabs.rundeck.core.storage.ResourceMeta; +import com.dtolabs.rundeck.core.storage.StorageTree; +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable.AuthenticationType; import com.rundeck.plugins.ansible.ansible.AnsibleException; @@ -18,8 +24,12 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import org.rundeck.app.spi.Services; +import org.rundeck.storage.api.PathUtil; +import org.rundeck.storage.api.StorageException; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @@ -28,10 +38,12 @@ import java.util.*; import java.util.Map.Entry; -public class AnsibleResourceModelSource implements ResourceModelSource { +public class AnsibleResourceModelSource implements ResourceModelSource, ProxySecretBundleCreator { private Framework framework; + private Services services; + private String project; private String sshAuthType; @@ -55,6 +67,9 @@ public class AnsibleResourceModelSource implements ResourceModelSource { protected Boolean sshUsePassword; protected String sshPassword; protected String sshPrivateKeyFile; + + protected String sshPasswordPath; + protected String sshPrivateKeyPath; protected String sshPass; protected Integer sshTimeout; @@ -99,6 +114,10 @@ private static Boolean skipVar(final String hostVar, final List varList) return false; } + public void setServices(Services services) { + this.services = services; + } + public void configure(Properties configuration) throws ConfigurationException { project = configuration.getProperty("project"); @@ -156,6 +175,10 @@ public void configure(Properties configuration) throws ConfigurationException { extraParameters = (String) resolveProperty(AnsibleDescribable.ANSIBLE_EXTRA_PARAM,null,configuration,executionDataContext); + sshPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); + sshPrivateKeyPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH,null,configuration,executionDataContext); + + } public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ @@ -172,6 +195,9 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner.limit(limitList); } + StorageTree storageTree = services.getService(KeyStorageTree.class); + + if ( sshAuthType.equalsIgnoreCase(AuthenticationType.privateKey.name()) ) { if (sshPrivateKeyFile != null) { String sshPrivateKey; @@ -182,10 +208,29 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } runner = runner.sshPrivateKey(sshPrivateKey); } + + if(sshPrivateKeyPath !=null && !sshPrivateKeyPath.isEmpty()){ + try { + String sshPrivateKey = getStorageContentString(sshPrivateKeyPath, storageTree); + runner = runner.sshPrivateKey(sshPrivateKey); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); + } + } + } else if ( sshAuthType.equalsIgnoreCase(AuthenticationType.password.name()) ) { if (sshPassword != null) { runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); } + + if(sshPasswordPath !=null && !sshPasswordPath.isEmpty()){ + try { + sshPassword = getStorageContentString(sshPasswordPath, storageTree); + runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); + } + } } @@ -249,6 +294,9 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner.extraParams(extraParameters); } + + + return runner; } @@ -534,4 +582,79 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx return nodes; } + + + + private String getStorageContentString(String storagePath, StorageTree storageTree) throws ConfigurationException { + return new String(this.getStorageContent(storagePath, storageTree)); + } + + private byte[] getStorageContent(String storagePath, StorageTree storageTree) throws ConfigurationException { + org.rundeck.storage.api.Path path = PathUtil.asPath(storagePath); + try { + ResourceMeta contents = storageTree.getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (StorageException e) { + throw new ConfigurationException("Failed to read the ssh private key for " + + "storage path: " + storagePath + ": " + e.getMessage()); + } catch (IOException e) { + throw new ConfigurationException("Failed to read the ssh private key for " + + "storage path: " + storagePath + ": " + e.getMessage()); + } + } + + + @Override + List listSecretsPathResourceModel(Map configuration){ + List keys = new ArrayList<>(); + + String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); + String privateKeyStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH); + + if(passwordStoragePath!=null){ + keys.add(passwordStoragePath); + } + + if(privateKeyStoragePath!=null){ + keys.add(privateKeyStoragePath); + } + + return null; + + } + + @Override + SecretBundle prepareSecretBundleResourceModel(Services services, Map configuration){ + DefaultSecretBundle secretBundle = new DefaultSecretBundle(); + + try { + StorageTree storageTree = services.getService(KeyStorageTree.class); + + String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); + String privateKeyStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH); + + if(passwordStoragePath!=null){ + secretBundle.addSecret( + passwordStoragePath, + getStorageContent(passwordStoragePath,storageTree ) + ); + } + + if(privateKeyStoragePath!=null){ + secretBundle.addSecret( + privateKeyStoragePath, + getStorageContent(privateKeyStoragePath,storageTree ) + ); + } + + return secretBundle; + + } catch (Exception e) { + return null; + } + } + + } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java index a4159ac2..431a0ef1 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java @@ -9,6 +9,7 @@ import com.dtolabs.rundeck.core.resources.ResourceModelSourceFactory; import com.dtolabs.rundeck.plugins.ServiceNameConstants; import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; +import org.rundeck.app.spi.Services; import java.util.Properties; @@ -53,6 +54,10 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { builder.property(BECOME_PASSWORD_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_PASSWORD_PROP); + + builder.property(SSH_PASSWORD_STORAGE_PROP); + builder.property(SSH_KEY_STORAGE_PROP); + builder.mapping(ANSIBLE_INVENTORY,PROJ_PROP_PREFIX + ANSIBLE_INVENTORY); builder.frameworkMapping(ANSIBLE_INVENTORY,FWK_PROP_PREFIX + ANSIBLE_INVENTORY); builder.mapping(ANSIBLE_CONFIG_FILE_PATH,PROJ_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); @@ -68,12 +73,18 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { @Override public ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { - AnsibleResourceModelSource ansibleResourceModelSource = new AnsibleResourceModelSource(framework); - ansibleResourceModelSource.configure(configuration); - return ansibleResourceModelSource; + return null; } - @Override + @Override + public ResourceModelSource createResourceModelSource(Services services, Properties configuration) throws ConfigurationException { + AnsibleResourceModelSource ansibleResourceModelSource = new AnsibleResourceModelSource(framework); + ansibleResourceModelSource.configure(configuration); + ansibleResourceModelSource.setServices(services); + return ansibleResourceModelSource; + } + + @Override public Description getDescription() { return DESC; } From c9c0982ad8fcf05ec7797cb2126a08605095fe28 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 18 Jul 2023 09:51:44 -0400 Subject: [PATCH 2/5] return an error if the key doesnt have permissions --- .../ansible/plugin/AnsibleResourceModelSource.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index 35240de9..57ff7152 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -606,8 +606,8 @@ private byte[] getStorageContent(String storagePath, StorageTree storageTree) th } - @Override - List listSecretsPathResourceModel(Map configuration){ + //@Override + public List listSecretsPathResourceModel(Map configuration){ List keys = new ArrayList<>(); String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); @@ -625,8 +625,8 @@ List listSecretsPathResourceModel(Map configuration){ } - @Override - SecretBundle prepareSecretBundleResourceModel(Services services, Map configuration){ + //@Override + public SecretBundle prepareSecretBundleResourceModel(Services services, Map configuration){ DefaultSecretBundle secretBundle = new DefaultSecretBundle(); try { @@ -652,7 +652,7 @@ SecretBundle prepareSecretBundleResourceModel(Services services, Map Date: Thu, 20 Jul 2023 11:37:53 -0400 Subject: [PATCH 3/5] fix listSecretsPathResourceModel --- .../plugins/ansible/plugin/AnsibleResourceModelSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index 57ff7152..973dc87d 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -621,7 +621,7 @@ public List listSecretsPathResourceModel(Map configurati keys.add(privateKeyStoragePath); } - return null; + return keys; } From af43f4a9742f8aeafaf174f6f6275985328d82c4 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 16 Aug 2023 11:33:59 -0400 Subject: [PATCH 4/5] update core version use key storage for passwords and keys --- build.gradle | 2 +- .../plugin/AnsibleResourceModelSource.java | 143 ++++++++++++++---- .../AnsibleResourceModelSourceFactory.java | 5 + 3 files changed, 123 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index ed7b06e4..38970e34 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ configurations { dependencies { pluginLibs 'com.google.code.gson:gson:2.10.1' - implementation('org.rundeck:rundeck-core:4.14.0-rc1-20230606') + implementation('org.rundeck:rundeck-core:4.16.0-rc1-20230815') implementation 'org.codehaus.groovy:groovy-all:3.0.9' } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index 973dc87d..e23cf21a 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -83,12 +83,20 @@ public class AnsibleResourceModelSource implements ResourceModelSource, ProxySec protected String vaultFile; protected String vaultPassword; + protected String vaultPasswordPath; + protected String baseDirectoryPath; protected String ansibleBinariesDirectoryPath; protected String extraParameters; + protected String sshAgent; + protected String sshPassphraseStoragePath; + + protected String becamePasswordStoragePath; + + public AnsibleResourceModelSource(final Framework framework) { this.framework = framework; } @@ -178,7 +186,13 @@ public void configure(Properties configuration) throws ConfigurationException { sshPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); sshPrivateKeyPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH,null,configuration,executionDataContext); + vaultPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_VAULTSTORE_PATH,null,configuration,executionDataContext); + sshAgent = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_USE_AGENT,null,configuration,executionDataContext); + sshPassphraseStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE,null,configuration,executionDataContext); + vaultPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); + + becamePasswordStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); } public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ @@ -218,6 +232,19 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } } + if(sshAgent != null && sshAgent.equalsIgnoreCase("true")) { + runner = runner.sshUseAgent(Boolean.TRUE); + + if(sshPassphraseStoragePath != null && !sshPassphraseStoragePath.isEmpty()) { + try { + String sshPassphrase = getStorageContentString(sshPassphraseStoragePath, storageTree); + runner = runner.sshPassphrase(sshPassphrase); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read passphrase from storage path " + sshPassphraseStoragePath,e); + } + } + } + } else if ( sshAuthType.equalsIgnoreCase(AuthenticationType.password.name()) ) { if (sshPassword != null) { runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); @@ -233,7 +260,6 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } } - if (inventory != null) { runner = runner.setInventory(inventory); } @@ -265,34 +291,52 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner = runner.becomePassword(becomePassword); } - if (configFile != null) { - runner = runner.configFile(configFile); + if(becamePasswordStoragePath != null && !becamePasswordStoragePath.isEmpty()){ + try { + becomePassword = getStorageContentString(becamePasswordStoragePath, storageTree); + runner = runner.becomePassword(becomePassword); + } catch (Exception e) { + throw new ResourceModelSourceException("Could not read becomePassword from storage path " + becamePasswordStoragePath,e); } + } - if(vaultPassword!=null) { + if (configFile != null) { + runner = runner.configFile(configFile); + } + + if(vaultPassword!=null) { runner.vaultPass(vaultPassword); - } + } - if (vaultFile != null) { - String vaultPassword; - try { - vaultPassword = new String(Files.readAllBytes(Paths.get(vaultFile))); - } catch (IOException e) { - throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); - } - runner.vaultPass(vaultPassword); - } - if (baseDirectoryPath != null) { - runner.baseDirectory(baseDirectoryPath); + if(vaultPasswordPath!=null && !vaultPasswordPath.isEmpty()){ + try { + vaultPassword = getStorageContentString(vaultPasswordPath, storageTree); + } catch (Exception e) { + throw new ResourceModelSourceException("Could not read vaultPassword " + vaultPasswordPath,e); } + runner = runner.vaultPass(vaultPassword); + } - if (ansibleBinariesDirectoryPath != null) { - runner.ansibleBinariesDirectory(ansibleBinariesDirectoryPath); + if (vaultFile != null) { + String vaultPassword; + try { + vaultPassword = new String(Files.readAllBytes(Paths.get(vaultFile))); + } catch (IOException e) { + throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); } + runner.vaultPass(vaultPassword); + } + if (baseDirectoryPath != null) { + runner.baseDirectory(baseDirectoryPath); + } - if (extraParameters != null){ - runner.extraParams(extraParameters); - } + if (ansibleBinariesDirectoryPath != null) { + runner.ansibleBinariesDirectory(ansibleBinariesDirectoryPath); + } + + if (extraParameters != null){ + runner.extraParams(extraParameters); + } @@ -612,13 +656,36 @@ public List listSecretsPathResourceModel(Map configurati String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); String privateKeyStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH); + String passphraseStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE); + String vaultPasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_VAULTSTORE_PATH); + String becamePasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH); - if(passwordStoragePath!=null){ + if(passwordStoragePath!=null && !passwordStoragePath.isEmpty()){ keys.add(passwordStoragePath); } - if(privateKeyStoragePath!=null){ - keys.add(privateKeyStoragePath); + if(privateKeyStoragePath!=null && !privateKeyStoragePath.isEmpty()){ + if(!keys.contains(privateKeyStoragePath)){ + keys.add(privateKeyStoragePath); + } + } + + if(passphraseStoragePath!=null && !passphraseStoragePath.isEmpty()){ + if(!keys.contains(passphraseStoragePath)){ + keys.add(passphraseStoragePath); + } + } + + if(vaultPasswordStoragePath!=null && !vaultPasswordStoragePath.isEmpty()){ + if(!keys.contains(vaultPasswordStoragePath)){ + keys.add(vaultPasswordStoragePath); + } + } + + if(becamePasswordStoragePath!=null && !becamePasswordStoragePath.isEmpty()){ + if(!keys.contains(becamePasswordStoragePath)){ + keys.add(becamePasswordStoragePath); + } } return keys; @@ -634,21 +701,45 @@ public SecretBundle prepareSecretBundleResourceModel(Services services, Map Date: Wed, 16 Aug 2023 11:34:49 -0400 Subject: [PATCH 5/5] clean --- .../plugins/ansible/plugin/AnsibleResourceModelSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index e23cf21a..8a5544dd 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -650,7 +650,7 @@ private byte[] getStorageContent(String storagePath, StorageTree storageTree) th } - //@Override + @Override public List listSecretsPathResourceModel(Map configuration){ List keys = new ArrayList<>(); @@ -692,7 +692,7 @@ public List listSecretsPathResourceModel(Map configurati } - //@Override + @Override public SecretBundle prepareSecretBundleResourceModel(Services services, Map configuration){ DefaultSecretBundle secretBundle = new DefaultSecretBundle();