From 6872c9000b12bcdf72ab3fe92114c1562a2d287d Mon Sep 17 00:00:00 2001 From: Matthieu MARC Date: Mon, 27 Nov 2023 16:17:30 +0100 Subject: [PATCH 1/9] feat: retrieve git password from key storage --- .../rundeck/plugin/GitResourceModel.groovy | 57 ++++++++++++++++++- .../plugin/GitResourceModelFactory.groovy | 12 +++- .../rundeck/plugin/util/GitPluginUtil.groovy | 20 +++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index c1f19ad..cfe0f26 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -10,6 +10,12 @@ import com.dtolabs.rundeck.core.resources.format.ResourceFormatParser import com.dtolabs.rundeck.core.resources.format.ResourceFormatParserException import com.dtolabs.rundeck.core.resources.format.UnsupportedFormatException import com.dtolabs.utils.Streams +import com.dtolabs.rundeck.core.execution.ExecutionContext; +import com.dtolabs.rundeck.core.execution.ExecutionContextImpl; +import com.rundeck.plugin.util.GitPluginUtil +import org.rundeck.app.spi.Services; +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; +import com.dtolabs.rundeck.core.execution.ExecutionListener /** @@ -31,6 +37,42 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ this.writable=true; } + GitResourceModel(Services services, Properties configuration, Framework framework) { + + this.configuration = configuration + this.framework = framework + + this.extension=configuration.getProperty(GitResourceModelFactory.GIT_FORMAT_FILE) + this.writable=Boolean.valueOf(configuration.getProperty(GitResourceModelFactory.WRITABLE)) + this.fileName=configuration.getProperty(GitResourceModelFactory.GIT_FILE) + this.localPath=configuration.getProperty(GitResourceModelFactory.GIT_BASE_DIRECTORY) + + if(gitManager==null){ + gitManager = new GitManager(configuration) + } + + ExecutionContext context = null; + + if(services!=null){ + context = new ExecutionContextImpl.Builder() + .framework(framework) + .storageTree(services.getService(KeyStorageTree.class)) + .build(); + }else{ + context = new ExecutionContextImpl.Builder() + .framework(framework) + .build(); + } + + if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)){ + def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE), context) + gitManager.setGitPassword(password) + } + + if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { + gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) + } + } GitResourceModel(Properties configuration, Framework framework) { this.configuration = configuration @@ -45,10 +87,21 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ gitManager = new GitManager(configuration) } - if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { - gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) + ExecutionContext context = new ExecutionContextImpl.Builder() + .framework(this.framework) + .storageTree(services.getService(KeyStorageTree.class)) + .build(); + + + if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)){ + def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE), context) + gitManager.setGitPassword(password) } + // if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { + // gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) + // } + if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) } diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index 7c23c88..60002ce 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -12,6 +12,7 @@ import com.dtolabs.rundeck.plugins.ServiceNameConstants import com.dtolabs.rundeck.plugins.descriptions.PluginDescription import com.dtolabs.rundeck.plugins.util.DescriptionBuilder import com.rundeck.plugin.util.GitPluginUtil +import org.rundeck.app.spi.Services; /** * Created by luistoledo on 12/18/17. @@ -42,7 +43,7 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable final static Map renderingOptionsAuthentication = GitPluginUtil.getRenderOpt("Authentication",false) - final static Map renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, true) + final static Map renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, false, true) final static Map renderingOptionsConfig = GitPluginUtil.getRenderOpt("Configuration",false) GitResourceModelFactory(Framework framework) { @@ -76,7 +77,7 @@ Some examples: .property(PropertyUtil.bool(WRITABLE, "Writable", "Allow to write the remote file.", false,"false",null,renderingOptionsConfig)) - .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password", 'Password to authenticate remotely', false, + .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password", 'Password to authenticate remotely', false, null,null,null, renderingOptionsAuthenticationPassword)) .property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false, @@ -96,6 +97,13 @@ If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` fil ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { final GitResourceModel resource = new GitResourceModel(configuration,framework) + return resource + } + + @Override + ResourceModelSource createResourceModelSource(final Services services, final Properties configuration) throws ConfigurationException { + final GitResourceModel resource = new GitResourceModel(services, configuration,framework) + return resource } } diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index fdd5ab7..ab80f33 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -3,6 +3,9 @@ package com.rundeck.plugin.util import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants import com.dtolabs.rundeck.core.storage.ResourceMeta import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.dtolabs.rundeck.core.execution.ExecutionContextImpl +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; +import com.dtolabs.rundeck.core.execution.ExecutionListener /** * Created by luistoledo on 12/18/17. @@ -41,4 +44,21 @@ class GitPluginUtil { } + static String getFromKeyStorage(String path, ExecutionContextImpl context){ + KeyStorageTree storageTree = context.getStorageTree(); + + if (storageTree!=null){ + ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + String password = new String(byteArrayOutputStream.toByteArray()); + + return password; + } else { + ExecutionListener logger = context.getExecutionContext().getExecutionListener() + logger.log(1, "storageTree is null. Cannot retrieve password"); + return null + } + + } } From c0cf9540976482d86d990b3ed137496a490e1c0c Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 2 Apr 2025 19:41:45 -0300 Subject: [PATCH 2/9] clean add test --- .../rundeck/plugin/GitResourceModel.groovy | 78 ++++++------------- .../plugin/GitResourceModelFactory.groovy | 27 ++++--- .../rundeck/plugin/util/GitPluginUtil.groovy | 6 +- .../plugin/GitResourceModelSpec.groovy | 61 +++++++++++++++ 4 files changed, 101 insertions(+), 71 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index cfe0f26..eb61b27 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -10,17 +10,17 @@ import com.dtolabs.rundeck.core.resources.format.ResourceFormatParser import com.dtolabs.rundeck.core.resources.format.ResourceFormatParserException import com.dtolabs.rundeck.core.resources.format.UnsupportedFormatException import com.dtolabs.utils.Streams -import com.dtolabs.rundeck.core.execution.ExecutionContext; -import com.dtolabs.rundeck.core.execution.ExecutionContextImpl; +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.core.execution.ExecutionContextImpl import com.rundeck.plugin.util.GitPluginUtil -import org.rundeck.app.spi.Services; -import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; -import com.dtolabs.rundeck.core.execution.ExecutionListener - +import groovy.transform.CompileStatic +import org.rundeck.app.spi.Services +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree /** * Created by luistoledo on 12/18/17. */ +@CompileStatic class GitResourceModel implements ResourceModelSource , WriteableModelSource{ private Properties configuration; @@ -38,7 +38,14 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } GitResourceModel(Services services, Properties configuration, Framework framework) { + configure(configuration,framework,services) + } + + GitResourceModel(Properties configuration, Framework framework) { + configure(configuration,framework, null) + } + def configure(Properties configuration, Framework framework, Services services){ this.configuration = configuration this.framework = framework @@ -51,61 +58,23 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ gitManager = new GitManager(configuration) } - ExecutionContext context = null; - - if(services!=null){ - context = new ExecutionContextImpl.Builder() + if(services && configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)){ + ExecutionContext context = new ExecutionContextImpl.Builder() .framework(framework) .storageTree(services.getService(KeyStorageTree.class)) .build(); - }else{ - context = new ExecutionContextImpl.Builder() - .framework(framework) - .build(); - } - if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)){ - def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE), context) + def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH), context) gitManager.setGitPassword(password) } - if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { - gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) + if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { + gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) } - } - - GitResourceModel(Properties configuration, Framework framework) { - this.configuration = configuration - this.framework = framework - - this.extension=configuration.getProperty(GitResourceModelFactory.GIT_FORMAT_FILE) - this.writable=Boolean.valueOf(configuration.getProperty(GitResourceModelFactory.WRITABLE)) - this.fileName=configuration.getProperty(GitResourceModelFactory.GIT_FILE) - this.localPath=configuration.getProperty(GitResourceModelFactory.GIT_BASE_DIRECTORY) - - if(gitManager==null){ - gitManager = new GitManager(configuration) - } - - ExecutionContext context = new ExecutionContextImpl.Builder() - .framework(this.framework) - .storageTree(services.getService(KeyStorageTree.class)) - .build(); - - - if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)){ - def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE), context) - gitManager.setGitPassword(password) - } - - // if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { - // gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) - // } if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) } - } @Override @@ -133,7 +102,6 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ throw new ResourceModelSourceException( "Error requesting Resource Model Source from GIT, " +e.getMessage(),e); } - return null } private ResourceFormatParser getResourceFormatParser() throws UnsupportedFormatException { @@ -151,18 +119,18 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } @Override - public SourceType getSourceType() { + SourceType getSourceType() { return writable ? SourceType.READ_WRITE : SourceType.READ_ONLY; } @Override - public WriteableModelSource getWriteable() { + WriteableModelSource getWriteable() { return writable ? this : null; } @Override - public String getSyntaxMimeType() { + String getSyntaxMimeType() { try { return getResourceFormatParser().getPreferredMimeType(); } catch (UnsupportedFormatException e) { @@ -193,7 +161,7 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } @Override - public long writeData(InputStream data) throws IOException, ResourceModelSourceException { + long writeData(InputStream data) throws IOException, ResourceModelSourceException { if (!writable) { throw new IllegalArgumentException("Cannot write to file, it is not configured to be writeable"); } @@ -225,7 +193,7 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } @Override - public String getSourceDescription() { + String getSourceDescription() { String gitURL=configuration.getProperty(GitResourceModelFactory.GIT_URL) return "Git repo: "+gitURL+", file:"+this.fileName; } diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index 60002ce..ae943d8 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -17,8 +17,8 @@ import org.rundeck.app.spi.Services; /** * Created by luistoledo on 12/18/17. */ -@Plugin(name = GitResourceModelFactory.PROVIDER_NAME, service = ServiceNameConstants.ResourceModelSource) -@PluginDescription(title = GitResourceModelFactory.PROVIDER_TITLE, description = GitResourceModelFactory.PROVIDER_DESCRIPTION) +@Plugin(name = PROVIDER_NAME, service = ServiceNameConstants.ResourceModelSource) +@PluginDescription(title = PROVIDER_TITLE, description = PROVIDER_DESCRIPTION) class GitResourceModelFactory implements ResourceModelSourceFactory,Describable { private Framework framework; @@ -39,11 +39,14 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable public final static String GIT_HOSTKEY_CHECKING="strictHostKeyChecking" public final static String GIT_KEY_STORAGE="gitKeyPath" public final static String GIT_PASSWORD_STORAGE="gitPasswordPath" + public final static String GIT_PASSWORD_STORAGE_PATH="gitPasswordPathStorage" + public static final String WRITABLE="writable"; final static Map renderingOptionsAuthentication = GitPluginUtil.getRenderOpt("Authentication",false) - final static Map renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, false, true) + final static Map renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, true) + final static Map renderingOptionsAuthenticationPasswordStorage = GitPluginUtil.getRenderOpt("Authentication",false, false, true) final static Map renderingOptionsConfig = GitPluginUtil.getRenderOpt("Configuration",false) GitResourceModelFactory(Framework framework) { @@ -73,21 +76,21 @@ Some examples: .property(PropertyUtil.string(GIT_FILE, "Resource model File", "Resource model file inside the github repo.", true, null,null,null, renderingOptionsConfig)) .property(PropertyUtil.select(GIT_FORMAT_FILE, "File Format", 'File Format', true, - "xml",GitResourceModelFactory.LIST_FILE_TYPE,null, renderingOptionsConfig)) + "xml", LIST_FILE_TYPE,null, renderingOptionsConfig)) .property(PropertyUtil.bool(WRITABLE, "Writable", "Allow to write the remote file.", false,"false",null,renderingOptionsConfig)) - .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password", 'Password to authenticate remotely', false, + .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password", 'Password to authenticate remotely', false, null,null,null, renderingOptionsAuthenticationPassword)) + .property(PropertyUtil.string(GIT_PASSWORD_STORAGE_PATH, "Git Password", 'Password Storage to authenticate remotely', false, + null,null,null, renderingOptionsAuthenticationPasswordStorage)) .property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false, - "yes",GitResourceModelFactory.LIST_HOSTKEY_CHECKING,null, renderingOptionsAuthentication)) + "yes", LIST_HOSTKEY_CHECKING,null, renderingOptionsAuthentication)) .property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path", 'SSH Key Path', false, null,null,null, renderingOptionsAuthentication)) .build() - - @Override Description getDescription() { return DESCRIPTION @@ -95,15 +98,11 @@ If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` fil @Override ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { - final GitResourceModel resource = new GitResourceModel(configuration,framework) - - return resource + return new GitResourceModel(configuration,framework) } @Override ResourceModelSource createResourceModelSource(final Services services, final Properties configuration) throws ConfigurationException { - final GitResourceModel resource = new GitResourceModel(services, configuration,framework) - - return resource + return new GitResourceModel(services, configuration,framework) } } diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index ab80f33..0336d6d 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -6,10 +6,12 @@ import com.dtolabs.rundeck.plugins.step.PluginStepContext import com.dtolabs.rundeck.core.execution.ExecutionContextImpl import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; import com.dtolabs.rundeck.core.execution.ExecutionListener +import groovy.transform.CompileStatic /** * Created by luistoledo on 12/18/17. */ +@CompileStatic class GitPluginUtil { static Map getRenderOpt(String value, boolean secondary, boolean password = false, boolean storagePassword = false, boolean storageKey = false) { Map ret = new HashMap<>(); @@ -45,7 +47,7 @@ class GitPluginUtil { } static String getFromKeyStorage(String path, ExecutionContextImpl context){ - KeyStorageTree storageTree = context.getStorageTree(); + KeyStorageTree storageTree = (KeyStorageTree)context.getStorageTree() if (storageTree!=null){ ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); @@ -55,7 +57,7 @@ class GitPluginUtil { return password; } else { - ExecutionListener logger = context.getExecutionContext().getExecutionListener() + ExecutionListener logger = context.getExecutionListener() logger.log(1, "storageTree is null. Cannot retrieve password"); return null } diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index b3f869f..1dfa4de 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -4,7 +4,11 @@ import com.dtolabs.rundeck.core.common.Framework import com.dtolabs.rundeck.core.common.INodeSet import com.dtolabs.rundeck.core.resources.format.ResourceFormatParser import com.dtolabs.rundeck.core.resources.format.ResourceFormatParserService +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree +import org.rundeck.app.spi.Services +import org.rundeck.storage.api.Resource import spock.lang.Specification +import com.dtolabs.rundeck.core.storage.ResourceMeta /** * Created by luistoledo on 12/22/17. @@ -160,6 +164,63 @@ class GitResourceModelSpec extends Specification{ 'resources' |'resources.json' | 'json' } + def "retrieve resource success using password authentication from key storage"() { + given: + + def nodeSet = Mock(INodeSet) + def framework = getFramework(nodeSet) + + + + String path = "resources" + String fileName = "resources.xml" + String format = "xml" + + File folder = new File(path) + if(!folder.exists()){ + folder.mkdir() + } + + Properties configuration = [ + gitBaseDirectory:path, + gitFormatFile:format, + gitFile:fileName, + gitPasswordPathStorage:"gitPassword", + ] + + def gitManager = Mock(GitManager) + + def inputStream = GroovyMock(InputStream) + KeyStorageTree keyStorageTree = Mock(KeyStorageTree){ + 1 * getResource(_) >> Mock(Resource) { + 1* getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('password'.bytes) + return 6L + } + } + } + } + + Services services = Mock(Services){ + 1 * getService(KeyStorageTree) >> keyStorageTree + } + + when: + + def resource = new GitResourceModel(services,configuration,framework) + resource.setGitManager(gitManager) + + def result = resource.getNodes() + + then: + 1 * gitManager.getFile(path) >> inputStream + result == nodeSet + + + + } + private Framework getFramework(INodeSet nodeSet){ def resourceFormatParser = Mock(ResourceFormatParser){ From c3eff80ffd79dfc3fccb39e0cca0f443d5144524 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 11:43:17 -0800 Subject: [PATCH 3/9] fix: address Copilot PR review comments - Fix UI labels to distinguish plain text vs Key Storage password fields - Reverse precedence: Key Storage password now takes precedence over plain text - Add null safety check before setting password from Key Storage - Fix NullPointerException by reusing storageTree variable - Add error handling with try-catch and logging for Key Storage access - Add JavaDoc documentation to getFromKeyStorage method - Use explicit UTF-8 encoding for password strings - Update README with Key Storage authentication documentation - Clean up test file whitespace All changes maintain backwards compatibility - no breaking changes to property names. --- README.md | 14 ++++++- .../rundeck/plugin/GitResourceModel.groovy | 15 ++++--- .../plugin/GitResourceModelFactory.groovy | 4 +- .../rundeck/plugin/util/GitPluginUtil.groovy | 41 ++++++++++++++----- .../plugin/GitResourceModelSpec.groovy | 7 +--- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 7528aa0..636506f 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,23 @@ You need to set up the following options to use the plugin: ### Authentication -* **Git Password**: Password to authenticate remotely +The plugin supports multiple authentication methods: + +#### Password Authentication +* **Git Password (Plain Text)**: Password to authenticate remotely (plain text - less secure) +* **Git Password Storage Path**: Key storage path for Git password (more secure - recommended) + +If both password fields are configured, the Key Storage path takes precedence for security. + +#### SSH Key Authentication * **SSH: Strict Host Key Checking**: Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify. * **SSH Key Path**: SSH Key Path to authenticate +**Recommended:** Use Key Storage for passwords instead of plain text. To use Key Storage: +1. Store your password in Rundeck Key Storage (under `keys/` path) +2. Select the password path using the Key Storage browser in the plugin configuration + ### Limitations * The plugin needs to clone the full repo on the local directory path (Base Directory option) to get the file that will be added to the resource model. diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index eb61b27..ccd61ac 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -58,6 +58,13 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ gitManager = new GitManager(configuration) } + // Plain text password (less secure, checked first) + // Support old property name for backwards compatibility + if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { + gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) + } + + // Key Storage password (more secure, takes precedence if both are set) if(services && configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)){ ExecutionContext context = new ExecutionContextImpl.Builder() .framework(framework) @@ -65,11 +72,9 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ .build(); def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH), context) - gitManager.setGitPassword(password) - } - - if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { - gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) + if (password != null) { + gitManager.setGitPassword(password) + } } if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index ae943d8..c1254f6 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -80,9 +80,9 @@ Some examples: .property(PropertyUtil.bool(WRITABLE, "Writable", "Allow to write the remote file.", false,"false",null,renderingOptionsConfig)) - .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password", 'Password to authenticate remotely', false, + .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password (Plain Text)", 'Password to authenticate remotely (plain text)', false, null,null,null, renderingOptionsAuthenticationPassword)) - .property(PropertyUtil.string(GIT_PASSWORD_STORAGE_PATH, "Git Password", 'Password Storage to authenticate remotely', false, + .property(PropertyUtil.string(GIT_PASSWORD_STORAGE_PATH, "Git Password Storage Path", 'Key storage path for Git password to authenticate remotely', false, null,null,null, renderingOptionsAuthenticationPasswordStorage)) .property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false, diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index 0336d6d..b8f7572 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -7,6 +7,7 @@ import com.dtolabs.rundeck.core.execution.ExecutionContextImpl import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; import com.dtolabs.rundeck.core.execution.ExecutionListener import groovy.transform.CompileStatic +import java.nio.charset.StandardCharsets /** * Created by luistoledo on 12/18/17. @@ -40,27 +41,45 @@ class GitPluginUtil { ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); contents.writeContent(byteArrayOutputStream); - String password = new String(byteArrayOutputStream.toByteArray()); + String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); return password; } - static String getFromKeyStorage(String path, ExecutionContextImpl context){ + /** + * Retrieves the contents of a resource from the key storage using the provided path and execution context. + *

+ * If the storage tree is available, this method attempts to read the resource at the given path and + * returns its contents as a String. If the storage tree is null or an error occurs, it logs a message and returns null. + * + * @param path the path to the resource in the key storage + * @param context the Rundeck execution context + * @return the contents of the resource as a String, or null if the storage tree is null or an error occurs + */ + static String getFromKeyStorage(String path, ExecutionContextImpl context){ KeyStorageTree storageTree = (KeyStorageTree)context.getStorageTree() - if (storageTree!=null){ - ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - contents.writeContent(byteArrayOutputStream); - String password = new String(byteArrayOutputStream.toByteArray()); - - return password; - } else { + if (storageTree == null){ ExecutionListener logger = context.getExecutionListener() - logger.log(1, "storageTree is null. Cannot retrieve password"); + logger.log(1, "storageTree is null. Cannot retrieve password from Key Storage."); return null } + try { + ResourceMeta contents = storageTree.getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + contents.writeContent(byteArrayOutputStream); + String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); + return password; + } finally { + byteArrayOutputStream.close(); + } + } catch (Exception e) { + ExecutionListener logger = context.getExecutionListener() + logger.log(1, "Failed to retrieve password from Key Storage at path '${path}': ${e.message}"); + return null + } } } diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index 1dfa4de..b923524 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -170,8 +170,6 @@ class GitResourceModelSpec extends Specification{ def nodeSet = Mock(INodeSet) def framework = getFramework(nodeSet) - - String path = "resources" String fileName = "resources.xml" String format = "xml" @@ -185,7 +183,7 @@ class GitResourceModelSpec extends Specification{ gitBaseDirectory:path, gitFormatFile:format, gitFile:fileName, - gitPasswordPathStorage:"gitPassword", + gitPasswordPathStorage:"keys/git/password", ] def gitManager = Mock(GitManager) @@ -216,9 +214,6 @@ class GitResourceModelSpec extends Specification{ then: 1 * gitManager.getFile(path) >> inputStream result == nodeSet - - - } From 1ff647d86284ee3fabb9ab24d4b67e8a3dc2a244 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 11:48:56 -0800 Subject: [PATCH 4/9] feat: add SSH Key Storage support and refactor code duplication - Refactor GitPluginUtil: Extract duplicate code into readResourceMetaAsString() helper method - Add SSH Key Storage support for Resource Model (feature parity with Workflow Steps) - Add GIT_KEY_STORAGE_PATH property for SSH keys from Rundeck Key Storage - Update GitResourceModel to retrieve SSH keys from Key Storage with precedence over filesystem - Add UI field label clarification (Filesystem vs Storage Path) - Add comprehensive test for SSH Key Storage authentication - Update README with SSH Key Storage documentation Maintains backwards compatibility - filesystem SSH key paths still work. --- README.md | 11 ++-- .../rundeck/plugin/GitResourceModel.groovy | 14 +++++ .../plugin/GitResourceModelFactory.groovy | 6 ++- .../rundeck/plugin/util/GitPluginUtil.groovy | 33 +++++++----- .../plugin/GitResourceModelSpec.groovy | 52 +++++++++++++++++++ 5 files changed, 97 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 636506f..dc7609d 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,14 @@ If both password fields are configured, the Key Storage path takes precedence fo #### SSH Key Authentication * **SSH: Strict Host Key Checking**: Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify. -* **SSH Key Path**: SSH Key Path to authenticate +* **SSH Key Path (Filesystem)**: SSH Key Path from filesystem to authenticate +* **SSH Key Storage Path**: SSH Key storage path from Rundeck Key Storage (more secure - recommended) -**Recommended:** Use Key Storage for passwords instead of plain text. To use Key Storage: -1. Store your password in Rundeck Key Storage (under `keys/` path) -2. Select the password path using the Key Storage browser in the plugin configuration +If both SSH key fields are configured, the Key Storage path takes precedence for security. + +**Recommended:** Use Key Storage for credentials instead of plain text or filesystem paths. To use Key Storage: +1. Store your password or SSH key in Rundeck Key Storage (under `keys/` path) +2. Select the credential path using the Key Storage browser in the plugin configuration ### Limitations diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index ccd61ac..49827fa 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -77,9 +77,23 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } } + // SSH Key from filesystem path (checked first) if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) } + + // SSH Key from Key Storage (takes precedence if both are set) + if(services && configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)){ + ExecutionContext context = new ExecutionContextImpl.Builder() + .framework(framework) + .storageTree(services.getService(KeyStorageTree.class)) + .build(); + + def sshKey = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH), context) + if (sshKey != null) { + gitManager.setSshPrivateKey(sshKey) + } + } } @Override diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index c1254f6..285881f 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -38,6 +38,7 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable public final static String GIT_BRANCH="gitBranch" public final static String GIT_HOSTKEY_CHECKING="strictHostKeyChecking" public final static String GIT_KEY_STORAGE="gitKeyPath" + public final static String GIT_KEY_STORAGE_PATH="gitKeyPathStorage" public final static String GIT_PASSWORD_STORAGE="gitPasswordPath" public final static String GIT_PASSWORD_STORAGE_PATH="gitPasswordPathStorage" @@ -47,6 +48,7 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable final static Map renderingOptionsAuthentication = GitPluginUtil.getRenderOpt("Authentication",false) final static Map renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, true) final static Map renderingOptionsAuthenticationPasswordStorage = GitPluginUtil.getRenderOpt("Authentication",false, false, true) + final static Map renderingOptionsAuthenticationKeyStorage = GitPluginUtil.getRenderOpt("Authentication",false, false, false, true) final static Map renderingOptionsConfig = GitPluginUtil.getRenderOpt("Configuration",false) GitResourceModelFactory(Framework framework) { @@ -87,8 +89,10 @@ Some examples: .property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false, "yes", LIST_HOSTKEY_CHECKING,null, renderingOptionsAuthentication)) - .property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path", 'SSH Key Path', false, + .property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path (Filesystem)", 'SSH Key Path from filesystem', false, null,null,null, renderingOptionsAuthentication)) + .property(PropertyUtil.string(GIT_KEY_STORAGE_PATH, "SSH Key Storage Path", 'SSH Key storage path from Rundeck Key Storage', false, + null,null,null, renderingOptionsAuthenticationKeyStorage)) .build() @Override diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index b8f7572..121857c 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -37,14 +37,26 @@ class GitPluginUtil { return ret; } - static String getFromKeyStorage(String path, PluginStepContext context){ - ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents(); + /** + * Reads the contents of a ResourceMeta and returns it as a String. + * + * @param contents the ResourceMeta to read + * @return the contents as a UTF-8 String + * @throws IOException if an error occurs reading the contents + */ + private static String readResourceMetaAsString(ResourceMeta contents) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - contents.writeContent(byteArrayOutputStream); - String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); - - return password; + try { + contents.writeContent(byteArrayOutputStream); + return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); + } finally { + byteArrayOutputStream.close(); + } + } + static String getFromKeyStorage(String path, PluginStepContext context){ + ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents(); + return readResourceMetaAsString(contents); } /** @@ -68,14 +80,7 @@ class GitPluginUtil { try { ResourceMeta contents = storageTree.getResource(path).getContents(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try { - contents.writeContent(byteArrayOutputStream); - String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); - return password; - } finally { - byteArrayOutputStream.close(); - } + return readResourceMetaAsString(contents); } catch (Exception e) { ExecutionListener logger = context.getExecutionListener() logger.log(1, "Failed to retrieve password from Key Storage at path '${path}': ${e.message}"); diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index b923524..a319a8a 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -216,6 +216,58 @@ class GitResourceModelSpec extends Specification{ result == nodeSet } + def "retrieve resource success using SSH key authentication from key storage"() { + given: + + def nodeSet = Mock(INodeSet) + def framework = getFramework(nodeSet) + + String path = "resources" + String fileName = "resources.xml" + String format = "xml" + + File folder = new File(path) + if(!folder.exists()){ + folder.mkdir() + } + + Properties configuration = [ + gitBaseDirectory:path, + gitFormatFile:format, + gitFile:fileName, + gitKeyPathStorage:"keys/git/ssh-key", + ] + + def gitManager = Mock(GitManager) + + def inputStream = GroovyMock(InputStream) + KeyStorageTree keyStorageTree = Mock(KeyStorageTree){ + 1 * getResource(_) >> Mock(Resource) { + 1* getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('-----BEGIN RSA PRIVATE KEY-----\ntest key content\n-----END RSA PRIVATE KEY-----'.bytes) + return 65L + } + } + } + } + + Services services = Mock(Services){ + 1 * getService(KeyStorageTree) >> keyStorageTree + } + + when: + + def resource = new GitResourceModel(services,configuration,framework) + resource.setGitManager(gitManager) + + def result = resource.getNodes() + + then: + 1 * gitManager.getFile(path) >> inputStream + result == nodeSet + } + private Framework getFramework(INodeSet nodeSet){ def resourceFormatParser = Mock(ResourceFormatParser){ From 1b6fa025ba82ce9169f664c9a4cb385a6649fb42 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 11:52:56 -0800 Subject: [PATCH 5/9] docs: comprehensive README improvements for authentication and Key Storage - Added detailed step-by-step setup instructions for Key Storage - Added example Key Storage paths for different scenarios - Created authentication examples table by Git URL type - Added comprehensive troubleshooting section for common issues - Added security best practices section - Added Quick Reference section with: - Key Storage setup instructions - Common configuration scenarios (GitHub, GitLab, HTTPS, SSH) - Property reference table - Version requirements - Improved workflow steps authentication documentation - Organized content with clear sections and examples README expanded from 175 to 344 lines with actionable guidance. --- README.md | 204 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index dc7609d..f67069a 100644 --- a/README.md +++ b/README.md @@ -54,25 +54,111 @@ You need to set up the following options to use the plugin: ### Authentication -The plugin supports multiple authentication methods: +The plugin supports multiple authentication methods. **Key Storage is recommended** for secure credential management. #### Password Authentication -* **Git Password (Plain Text)**: Password to authenticate remotely (plain text - less secure) -* **Git Password Storage Path**: Key storage path for Git password (more secure - recommended) -If both password fields are configured, the Key Storage path takes precedence for security. +##### Option 1: Key Storage (Recommended) +* **Git Password Storage Path**: Key storage path for Git password (secure) + +**How to use:** +1. Navigate to Rundeck **System Menu** → **Key Storage** +2. Click **Add or Upload a Key** → **Password** +3. Enter a path like `keys/git/myrepo-password` and your Git password +4. In the Resource Model configuration, use the **Key Storage browser** to select the password path + +**Example paths:** +``` +keys/git/github-token # GitHub personal access token +keys/git/gitlab-password # GitLab password +keys/project1/git-auth # Project-specific credentials +``` + +##### Option 2: Plain Text (Less Secure) +* **Git Password (Plain Text)**: Password to authenticate remotely (not recommended for production) + +**Note:** If both are configured, Key Storage takes precedence. + +--- #### SSH Key Authentication -* **SSH: Strict Host Key Checking**: Use strict host key checking. -If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify. -* **SSH Key Path (Filesystem)**: SSH Key Path from filesystem to authenticate -* **SSH Key Storage Path**: SSH Key storage path from Rundeck Key Storage (more secure - recommended) -If both SSH key fields are configured, the Key Storage path takes precedence for security. +##### Option 1: Key Storage (Recommended) +* **SSH Key Storage Path**: SSH Key from Rundeck Key Storage (secure) + +**How to use:** +1. Navigate to Rundeck **System Menu** → **Key Storage** +2. Click **Add or Upload a Key** → **Private Key** +3. Upload your SSH private key (e.g., `id_rsa`) and save it with a path like `keys/git/ssh-key` +4. In the Resource Model configuration, use the **Key Storage browser** to select the key path + +**Example paths:** +``` +keys/git/deployment-key # Deployment key for specific repo +keys/git/github-ssh-key # GitHub SSH key +keys/shared/git-readonly-key # Shared read-only access key +``` + +##### Option 2: Filesystem Path (Legacy) +* **SSH Key Path (Filesystem)**: Path to SSH key file on the Rundeck server filesystem + +**Example:** `/home/rundeck/.ssh/id_rsa` + +**Note:** If both are configured, Key Storage takes precedence. + +##### SSH Host Key Checking +* **SSH: Strict Host Key Checking**: + - `yes` - Require remote host SSH key is defined in `~/.ssh/known_hosts` (more secure) + - `no` - Skip host key verification (less secure, useful for testing) + +--- + +#### Authentication Examples by Git URL Type + +| Git URL Type | Recommended Auth | Example URL | +|--------------|------------------|-------------| +| HTTPS with token | Password Storage (token) | `https://github.com/user/repo.git` | +| HTTPS with password | Password Storage | `https://username@github.com/user/repo.git` | +| SSH | SSH Key Storage | `git@github.com:user/repo.git` | +| SSH | SSH Key Storage | `ssh://git@github.com/user/repo.git` | + +**Important:** For HTTPS authentication, include the username in the URL: `https://username@host.com/repo.git` + +--- + +#### Troubleshooting Authentication + +**Problem: "Authentication failed" error** +- Verify the Key Storage path is correct (e.g., `keys/git/password`, not `/keys/git/password`) +- Ensure the credential exists in Key Storage +- For HTTPS: Include username in Git URL (`https://user@github.com/...`) +- For SSH: Verify host key is in `known_hosts` if strict checking is enabled + +**Problem: "storageTree is null" in logs** +- The plugin requires Services API (Rundeck 5.16.0+) +- Fallback to filesystem/plain text options if Key Storage is unavailable + +**Problem: SSH authentication fails** +- Verify SSH key format (OpenSSH format, starts with `-----BEGIN RSA PRIVATE KEY-----` or similar) +- Check SSH key permissions if using filesystem path (should be `600`) +- For GitHub/GitLab, ensure the public key is added to your account +- Try with `Strict Host Key Checking = no` for initial testing + +**Problem: Key Storage path not found** +- Key Storage paths should start with `keys/` (e.g., `keys/git/password`) +- Use the Key Storage browser in the UI to select the correct path +- Verify the key type matches (password vs private key) + +--- + +#### Security Best Practices -**Recommended:** Use Key Storage for credentials instead of plain text or filesystem paths. To use Key Storage: -1. Store your password or SSH key in Rundeck Key Storage (under `keys/` path) -2. Select the credential path using the Key Storage browser in the plugin configuration +1. **Always use Key Storage** in production environments +2. **Use project-specific keys** when possible (e.g., `keys/project1/git-key`) +3. **Use deployment keys** with minimal permissions for SSH +4. **Use personal access tokens** instead of passwords for HTTPS (GitHub, GitLab, etc.) +5. **Rotate credentials regularly** and update them in Key Storage +6. **Enable strict host key checking** for SSH in production ### Limitations @@ -101,10 +187,17 @@ This plugin can clone/pull, add, commit, and push a git repository via 4 Workflo ##### Authentication -* **Password Storage Path**: Password storage path to authenticate remotely. This can be an Access Token - such as a Github access token. -* **SSH: Strict Host Key Checking**: Use strict host key checking. -If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify. -* **SSH Key Storage Path**: SSH Key storage path to authenticate +The workflow steps support the same authentication methods as the Resource Model: + +* **Password Storage Path**: Key Storage path for Git password or access token (e.g., `keys/git/github-token`) +* **SSH Key Storage Path**: Key Storage path for SSH private key (e.g., `keys/git/ssh-key`) +* **SSH: Strict Host Key Checking**: + - `yes` - Require host key in `~/.ssh/known_hosts` (recommended for production) + - `no` - Skip host key verification (useful for testing) + +**Tip:** You can use GitHub/GitLab personal access tokens as passwords for HTTPS authentication. + +For detailed authentication setup and troubleshooting, see the [Authentication section](#authentication) above. ### GIT Clone Workflow Step @@ -172,4 +265,81 @@ You need to set up following additional options to use the plugin: * **Message**: Commit message to be used. Defaults to `Rundeck Commit` * **Add**: Adds all contents of the git repo before commiting. Defaults to `False`. If you need to be more specific, please use `GIT / Add` workflow step. -* **Push**: Pushes the repository after commiting the changes. Defaults to `False`. \ No newline at end of file +* **Push**: Pushes the repository after commiting the changes. Defaults to `False`. + +--- + +## Quick Reference + +### Key Storage Setup + +**For Passwords/Tokens:** +1. System Menu → Key Storage → Add or Upload a Key → **Password** +2. Path format: `keys/git/your-credential-name` +3. Select in plugin using Key Storage browser + +**For SSH Keys:** +1. System Menu → Key Storage → Add or Upload a Key → **Private Key** +2. Upload your private key file (e.g., `id_rsa`) +3. Path format: `keys/git/your-key-name` +4. Select in plugin using Key Storage browser + +### Common Configuration Scenarios + +#### Scenario 1: Public GitHub Repo (Read-Only) +``` +Git URL: https://github.com/user/repo.git +Branch: main +Authentication: None required +``` + +#### Scenario 2: Private GitHub Repo with Personal Access Token +``` +Git URL: https://github.com/user/repo.git +Branch: main +Authentication: Git Password Storage Path → keys/git/github-token +(Store GitHub PAT in Key Storage as password) +``` + +#### Scenario 3: Private GitLab Repo with SSH Key +``` +Git URL: git@gitlab.com:user/repo.git +Branch: main +Authentication: SSH Key Storage Path → keys/git/gitlab-ssh-key +Strict Host Key Checking: yes +``` + +#### Scenario 4: Private Repo with HTTPS Username/Password +``` +Git URL: https://username@github.com/user/repo.git +Branch: main +Authentication: Git Password Storage Path → keys/git/password +(Include username in URL) +``` + +### Property Reference + +| Property Name | Description | Example Value | +|--------------|-------------|---------------| +| `gitUrl` | Git repository URL | `https://github.com/user/repo.git` | +| `gitBaseDirectory` | Local checkout directory | `/var/rundeck/git-repos/project1` | +| `gitBranch` | Branch to checkout | `main` or `develop` | +| `gitFile` | Resource model file in repo | `resources.yaml` | +| `gitFormatFile` | File format | `xml`, `yaml`, or `json` | +| `gitPasswordPath` | Plain text password | `mypassword` (not recommended) | +| `gitPasswordPathStorage` | Key Storage path for password | `keys/git/password` | +| `gitKeyPath` | Filesystem SSH key path | `/home/rundeck/.ssh/id_rsa` | +| `gitKeyPathStorage` | Key Storage path for SSH key | `keys/git/ssh-key` | +| `strictHostKeyChecking` | SSH host key verification | `yes` or `no` | +| `writable` | Allow writing to remote | `true` or `false` | + +### Version Requirements + +- **Rundeck 5.16.0 or later** - Required for Key Storage support +- Earlier versions can use filesystem paths and plain text authentication + +### Support + +For issues or questions: +- GitHub Issues: [rundeck-plugins/git-plugin](https://github.com/rundeck-plugins/git-plugin/issues) +- Rundeck Documentation: [https://docs.rundeck.com](https://docs.rundeck.com) \ No newline at end of file From b1740a3e927da0a0ea7dce637ff3e389bd390eb6 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 12:26:40 -0800 Subject: [PATCH 6/9] Fix JGit SSH module version mismatch and update imports for JGit 6.x compatibility - Updated jgitSsh from 5.13.3 to 6.6.1 to match core jgit version - Fixed package imports in PluginSshSessionFactory for JGit 6.x - JschConfigSessionFactory moved to org.eclipse.jgit.transport.ssh.jsch - OpenSshConfig moved to org.eclipse.jgit.transport.ssh.jsch - Resolves version mismatch that could cause SSH authentication issues --- gradle/libs.versions.toml | 2 +- .../com/rundeck/plugin/util/PluginSshSessionFactory.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f32098c..b99b02c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ junit = "4.13.2" rundeckCore = "5.16.0-20251006" slf4j = "1.7.36" jgit = "6.6.1.202309021850-r" -jgitSsh = "5.13.3.202401111512-r" +jgitSsh = "6.6.1.202309021850-r" spock = "2.0-groovy-3.0" cglib = "3.3.0" objenesis = "1.4" diff --git a/src/main/groovy/com/rundeck/plugin/util/PluginSshSessionFactory.groovy b/src/main/groovy/com/rundeck/plugin/util/PluginSshSessionFactory.groovy index e2c87fb..1c77aab 100644 --- a/src/main/groovy/com/rundeck/plugin/util/PluginSshSessionFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/PluginSshSessionFactory.groovy @@ -4,8 +4,8 @@ import com.jcraft.jsch.JSch import com.jcraft.jsch.JSchException import com.jcraft.jsch.Session import org.eclipse.jgit.api.TransportConfigCallback -import org.eclipse.jgit.transport.JschConfigSessionFactory -import org.eclipse.jgit.transport.OpenSshConfig +import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory +import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig import org.eclipse.jgit.transport.SshTransport import org.eclipse.jgit.transport.Transport import org.eclipse.jgit.util.FS From d6d8de286275aa32a2bda8ece46cb4578c7a7643 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 12:35:18 -0800 Subject: [PATCH 7/9] Address Copilot feedback: code quality improvements - Renamed misleading constant names for clarity (no breaking changes to property values) - GIT_KEY_STORAGE -> GIT_KEY_PATH (filesystem paths) - GIT_PASSWORD_STORAGE -> GIT_PASSWORD_PATH (plain text passwords) - Fixed potential NullPointerException in GitPluginUtil - Added null checks before calling logger.log() - Fixed misleading error messages to use 'credential' instead of 'password' - Messages now accurate for both passwords and SSH keys - Added comprehensive documentation to getFromKeyStorage methods - Documented parameters, return values, and exception behavior - Fixed spelling errors in README: 'commiting' -> 'committing' - All tests passing --- README.md | 4 ++-- .../com/rundeck/plugin/GitResourceModel.groovy | 8 ++++---- .../plugin/GitResourceModelFactory.groovy | 8 ++++---- .../com/rundeck/plugin/util/GitPluginUtil.groovy | 16 ++++++++++++++-- .../rundeck/plugin/GitResourceModelSpec.groovy | 1 - 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f67069a..addfb03 100644 --- a/README.md +++ b/README.md @@ -264,8 +264,8 @@ You need to set up following additional options to use the plugin: ##### Repo Settings * **Message**: Commit message to be used. Defaults to `Rundeck Commit` -* **Add**: Adds all contents of the git repo before commiting. Defaults to `False`. If you need to be more specific, please use `GIT / Add` workflow step. -* **Push**: Pushes the repository after commiting the changes. Defaults to `False`. +* **Add**: Adds all contents of the git repo before committing. Defaults to `False`. If you need to be more specific, please use `GIT / Add` workflow step. +* **Push**: Pushes the repository after committing the changes. Defaults to `False`. --- diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index 49827fa..9452040 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -60,8 +60,8 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ // Plain text password (less secure, checked first) // Support old property name for backwards compatibility - if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) { - gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE)) + if(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_PATH)) { + gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_PATH)) } // Key Storage password (more secure, takes precedence if both are set) @@ -78,8 +78,8 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ } // SSH Key from filesystem path (checked first) - if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) { - gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) + if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) { + gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) } // SSH Key from Key Storage (takes precedence if both are set) diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index 285881f..9829097 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -37,9 +37,9 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable public final static String GIT_FORMAT_FILE="gitFormatFile" public final static String GIT_BRANCH="gitBranch" public final static String GIT_HOSTKEY_CHECKING="strictHostKeyChecking" - public final static String GIT_KEY_STORAGE="gitKeyPath" + public final static String GIT_KEY_PATH="gitKeyPath" public final static String GIT_KEY_STORAGE_PATH="gitKeyPathStorage" - public final static String GIT_PASSWORD_STORAGE="gitPasswordPath" + public final static String GIT_PASSWORD_PATH="gitPasswordPath" public final static String GIT_PASSWORD_STORAGE_PATH="gitPasswordPathStorage" public static final String WRITABLE="writable"; @@ -82,14 +82,14 @@ Some examples: .property(PropertyUtil.bool(WRITABLE, "Writable", "Allow to write the remote file.", false,"false",null,renderingOptionsConfig)) - .property(PropertyUtil.string(GIT_PASSWORD_STORAGE, "Git Password (Plain Text)", 'Password to authenticate remotely (plain text)', false, + .property(PropertyUtil.string(GIT_PASSWORD_PATH, "Git Password (Plain Text)", 'Password to authenticate remotely (plain text)', false, null,null,null, renderingOptionsAuthenticationPassword)) .property(PropertyUtil.string(GIT_PASSWORD_STORAGE_PATH, "Git Password Storage Path", 'Key storage path for Git password to authenticate remotely', false, null,null,null, renderingOptionsAuthenticationPasswordStorage)) .property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking. If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false, "yes", LIST_HOSTKEY_CHECKING,null, renderingOptionsAuthentication)) - .property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path (Filesystem)", 'SSH Key Path from filesystem', false, + .property(PropertyUtil.string(GIT_KEY_PATH, "SSH Key Path (Filesystem)", 'SSH Key Path from filesystem', false, null,null,null, renderingOptionsAuthentication)) .property(PropertyUtil.string(GIT_KEY_STORAGE_PATH, "SSH Key Storage Path", 'SSH Key storage path from Rundeck Key Storage', false, null,null,null, renderingOptionsAuthenticationKeyStorage)) diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index 121857c..3805f46 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -54,6 +54,14 @@ class GitPluginUtil { } } + /** + * Retrieves the contents of a resource from the key storage using the provided path and plugin step context. + * + * @param path the path to the resource in the key storage + * @param context the Rundeck plugin step context + * @return the contents of the resource as a String + * @throws Exception if the resource cannot be found or an error occurs reading the contents + */ static String getFromKeyStorage(String path, PluginStepContext context){ ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents(); return readResourceMetaAsString(contents); @@ -74,7 +82,9 @@ class GitPluginUtil { if (storageTree == null){ ExecutionListener logger = context.getExecutionListener() - logger.log(1, "storageTree is null. Cannot retrieve password from Key Storage."); + if (logger != null) { + logger.log(1, "storageTree is null. Cannot retrieve credential from Key Storage."); + } return null } @@ -83,7 +93,9 @@ class GitPluginUtil { return readResourceMetaAsString(contents); } catch (Exception e) { ExecutionListener logger = context.getExecutionListener() - logger.log(1, "Failed to retrieve password from Key Storage at path '${path}': ${e.message}"); + if (logger != null) { + logger.log(1, "Failed to retrieve credential from Key Storage at path '${path}': ${e.message}"); + } return null } } diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index a319a8a..9e94cbd 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -268,7 +268,6 @@ class GitResourceModelSpec extends Specification{ result == nodeSet } - private Framework getFramework(INodeSet nodeSet){ def resourceFormatParser = Mock(ResourceFormatParser){ parseDocument(_) >> nodeSet From 3e9375f5f9bc475c95c5bdaba41f55f83ba32d10 Mon Sep 17 00:00:00 2001 From: Rundeck CI Date: Tue, 2 Dec 2025 12:46:23 -0800 Subject: [PATCH 8/9] Address additional Copilot feedback: refactoring and style improvements - Refactored ExecutionContext creation to avoid duplication - Create context once and reuse for both password and SSH key retrieval - Changed method parameter from ExecutionContextImpl to ExecutionContext interface - Improves flexibility and follows dependency inversion principle - Removed unnecessary semicolon from import statement - Fixed inconsistent spacing in test mock interactions (1* -> 1 *) - Fixed @Override annotation formatting for consistency - All tests passing --- .../rundeck/plugin/GitResourceModel.groovy | 29 +++++++++---------- .../plugin/GitResourceModelFactory.groovy | 2 +- .../rundeck/plugin/util/GitPluginUtil.groovy | 6 ++-- .../plugin/GitResourceModelSpec.groovy | 4 +-- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy index 9452040..62669b2 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy @@ -64,31 +64,30 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{ gitManager.setGitPassword(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_PATH)) } - // Key Storage password (more secure, takes precedence if both are set) - if(services && configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)){ - ExecutionContext context = new ExecutionContextImpl.Builder() + // SSH Key from filesystem path (checked first) + if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) { + gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) + } + + // Create execution context once for Key Storage operations + ExecutionContext context = null + if (services) { + context = new ExecutionContextImpl.Builder() .framework(framework) .storageTree(services.getService(KeyStorageTree.class)) - .build(); + .build() + } + // Key Storage password (more secure, takes precedence if both are set) + if(context && configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)){ def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH), context) if (password != null) { gitManager.setGitPassword(password) } } - // SSH Key from filesystem path (checked first) - if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) { - gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH)) - } - // SSH Key from Key Storage (takes precedence if both are set) - if(services && configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)){ - ExecutionContext context = new ExecutionContextImpl.Builder() - .framework(framework) - .storageTree(services.getService(KeyStorageTree.class)) - .build(); - + if(context && configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)){ def sshKey = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH), context) if (sshKey != null) { gitManager.setSshPrivateKey(sshKey) diff --git a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy index 9829097..0f4efdf 100644 --- a/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy +++ b/src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy @@ -105,7 +105,7 @@ If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` fil return new GitResourceModel(configuration,framework) } - @Override + @Override ResourceModelSource createResourceModelSource(final Services services, final Properties configuration) throws ConfigurationException { return new GitResourceModel(services, configuration,framework) } diff --git a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy index 3805f46..b4a5d6d 100644 --- a/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy +++ b/src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy @@ -3,8 +3,8 @@ package com.rundeck.plugin.util import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants import com.dtolabs.rundeck.core.storage.ResourceMeta import com.dtolabs.rundeck.plugins.step.PluginStepContext -import com.dtolabs.rundeck.core.execution.ExecutionContextImpl -import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree import com.dtolabs.rundeck.core.execution.ExecutionListener import groovy.transform.CompileStatic import java.nio.charset.StandardCharsets @@ -77,7 +77,7 @@ class GitPluginUtil { * @param context the Rundeck execution context * @return the contents of the resource as a String, or null if the storage tree is null or an error occurs */ - static String getFromKeyStorage(String path, ExecutionContextImpl context){ + static String getFromKeyStorage(String path, ExecutionContext context){ KeyStorageTree storageTree = (KeyStorageTree)context.getStorageTree() if (storageTree == null){ diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index 9e94cbd..3eed488 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -191,7 +191,7 @@ class GitResourceModelSpec extends Specification{ def inputStream = GroovyMock(InputStream) KeyStorageTree keyStorageTree = Mock(KeyStorageTree){ 1 * getResource(_) >> Mock(Resource) { - 1* getContents() >> Mock(ResourceMeta) { + 1 * getContents() >> Mock(ResourceMeta) { writeContent(_) >> { args -> args[0].write('password'.bytes) return 6L @@ -243,7 +243,7 @@ class GitResourceModelSpec extends Specification{ def inputStream = GroovyMock(InputStream) KeyStorageTree keyStorageTree = Mock(KeyStorageTree){ 1 * getResource(_) >> Mock(Resource) { - 1* getContents() >> Mock(ResourceMeta) { + 1 * getContents() >> Mock(ResourceMeta) { writeContent(_) >> { args -> args[0].write('-----BEGIN RSA PRIVATE KEY-----\ntest key content\n-----END RSA PRIVATE KEY-----'.bytes) return 65L From 172df772249be014d0e109476010dd4ae48f8580 Mon Sep 17 00:00:00 2001 From: Forrest Evans Date: Tue, 13 Jan 2026 10:51:47 -0800 Subject: [PATCH 9/9] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy index 3eed488..7180432 100644 --- a/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy +++ b/src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy @@ -225,7 +225,6 @@ class GitResourceModelSpec extends Specification{ String path = "resources" String fileName = "resources.xml" String format = "xml" - File folder = new File(path) if(!folder.exists()){ folder.mkdir()