Skip to content

Commit 1ff647d

Browse files
committed
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.
1 parent c3eff80 commit 1ff647d

5 files changed

Lines changed: 97 additions & 19 deletions

File tree

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@ If both password fields are configured, the Key Storage path takes precedence fo
6565
#### SSH Key Authentication
6666
* **SSH: Strict Host Key Checking**: Use strict host key checking.
6767
If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.
68-
* **SSH Key Path**: SSH Key Path to authenticate
68+
* **SSH Key Path (Filesystem)**: SSH Key Path from filesystem to authenticate
69+
* **SSH Key Storage Path**: SSH Key storage path from Rundeck Key Storage (more secure - recommended)
6970

70-
**Recommended:** Use Key Storage for passwords instead of plain text. To use Key Storage:
71-
1. Store your password in Rundeck Key Storage (under `keys/` path)
72-
2. Select the password path using the Key Storage browser in the plugin configuration
71+
If both SSH key fields are configured, the Key Storage path takes precedence for security.
72+
73+
**Recommended:** Use Key Storage for credentials instead of plain text or filesystem paths. To use Key Storage:
74+
1. Store your password or SSH key in Rundeck Key Storage (under `keys/` path)
75+
2. Select the credential path using the Key Storage browser in the plugin configuration
7376

7477
### Limitations
7578

src/main/groovy/com/rundeck/plugin/GitResourceModel.groovy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,23 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{
7777
}
7878
}
7979

80+
// SSH Key from filesystem path (checked first)
8081
if(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE)) {
8182
gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE))
8283
}
84+
85+
// SSH Key from Key Storage (takes precedence if both are set)
86+
if(services && configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)){
87+
ExecutionContext context = new ExecutionContextImpl.Builder()
88+
.framework(framework)
89+
.storageTree(services.getService(KeyStorageTree.class))
90+
.build();
91+
92+
def sshKey = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH), context)
93+
if (sshKey != null) {
94+
gitManager.setSshPrivateKey(sshKey)
95+
}
96+
}
8397
}
8498

8599
@Override

src/main/groovy/com/rundeck/plugin/GitResourceModelFactory.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable
3838
public final static String GIT_BRANCH="gitBranch"
3939
public final static String GIT_HOSTKEY_CHECKING="strictHostKeyChecking"
4040
public final static String GIT_KEY_STORAGE="gitKeyPath"
41+
public final static String GIT_KEY_STORAGE_PATH="gitKeyPathStorage"
4142
public final static String GIT_PASSWORD_STORAGE="gitPasswordPath"
4243
public final static String GIT_PASSWORD_STORAGE_PATH="gitPasswordPathStorage"
4344

@@ -47,6 +48,7 @@ class GitResourceModelFactory implements ResourceModelSourceFactory,Describable
4748
final static Map<String, Object> renderingOptionsAuthentication = GitPluginUtil.getRenderOpt("Authentication",false)
4849
final static Map<String, Object> renderingOptionsAuthenticationPassword = GitPluginUtil.getRenderOpt("Authentication",false, true)
4950
final static Map<String, Object> renderingOptionsAuthenticationPasswordStorage = GitPluginUtil.getRenderOpt("Authentication",false, false, true)
51+
final static Map<String, Object> renderingOptionsAuthenticationKeyStorage = GitPluginUtil.getRenderOpt("Authentication",false, false, false, true)
5052
final static Map<String, Object> renderingOptionsConfig = GitPluginUtil.getRenderOpt("Configuration",false)
5153

5254
GitResourceModelFactory(Framework framework) {
@@ -87,8 +89,10 @@ Some examples:
8789
.property(PropertyUtil.select(GIT_HOSTKEY_CHECKING, "SSH: Strict Host Key Checking", '''Use strict host key checking.
8890
If `yes`, require remote host SSH key is defined in the `~/.ssh/known_hosts` file, otherwise do not verify.''', false,
8991
"yes", LIST_HOSTKEY_CHECKING,null, renderingOptionsAuthentication))
90-
.property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path", 'SSH Key Path', false,
92+
.property(PropertyUtil.string(GIT_KEY_STORAGE, "SSH Key Path (Filesystem)", 'SSH Key Path from filesystem', false,
9193
null,null,null, renderingOptionsAuthentication))
94+
.property(PropertyUtil.string(GIT_KEY_STORAGE_PATH, "SSH Key Storage Path", 'SSH Key storage path from Rundeck Key Storage', false,
95+
null,null,null, renderingOptionsAuthenticationKeyStorage))
9296
.build()
9397

9498
@Override

src/main/groovy/com/rundeck/plugin/util/GitPluginUtil.groovy

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,26 @@ class GitPluginUtil {
3737
return ret;
3838
}
3939

40-
static String getFromKeyStorage(String path, PluginStepContext context){
41-
ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents();
40+
/**
41+
* Reads the contents of a ResourceMeta and returns it as a String.
42+
*
43+
* @param contents the ResourceMeta to read
44+
* @return the contents as a UTF-8 String
45+
* @throws IOException if an error occurs reading the contents
46+
*/
47+
private static String readResourceMetaAsString(ResourceMeta contents) throws IOException {
4248
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
43-
contents.writeContent(byteArrayOutputStream);
44-
String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
45-
46-
return password;
49+
try {
50+
contents.writeContent(byteArrayOutputStream);
51+
return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
52+
} finally {
53+
byteArrayOutputStream.close();
54+
}
55+
}
4756

57+
static String getFromKeyStorage(String path, PluginStepContext context){
58+
ResourceMeta contents = context.getExecutionContext().getStorageTree().getResource(path).getContents();
59+
return readResourceMetaAsString(contents);
4860
}
4961

5062
/**
@@ -68,14 +80,7 @@ class GitPluginUtil {
6880

6981
try {
7082
ResourceMeta contents = storageTree.getResource(path).getContents();
71-
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
72-
try {
73-
contents.writeContent(byteArrayOutputStream);
74-
String password = new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
75-
return password;
76-
} finally {
77-
byteArrayOutputStream.close();
78-
}
83+
return readResourceMetaAsString(contents);
7984
} catch (Exception e) {
8085
ExecutionListener logger = context.getExecutionListener()
8186
logger.log(1, "Failed to retrieve password from Key Storage at path '${path}': ${e.message}");

src/test/groovy/com/rundeck/plugin/GitResourceModelSpec.groovy

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,58 @@ class GitResourceModelSpec extends Specification{
216216
result == nodeSet
217217
}
218218

219+
def "retrieve resource success using SSH key authentication from key storage"() {
220+
given:
221+
222+
def nodeSet = Mock(INodeSet)
223+
def framework = getFramework(nodeSet)
224+
225+
String path = "resources"
226+
String fileName = "resources.xml"
227+
String format = "xml"
228+
229+
File folder = new File(path)
230+
if(!folder.exists()){
231+
folder.mkdir()
232+
}
233+
234+
Properties configuration = [
235+
gitBaseDirectory:path,
236+
gitFormatFile:format,
237+
gitFile:fileName,
238+
gitKeyPathStorage:"keys/git/ssh-key",
239+
]
240+
241+
def gitManager = Mock(GitManager)
242+
243+
def inputStream = GroovyMock(InputStream)
244+
KeyStorageTree keyStorageTree = Mock(KeyStorageTree){
245+
1 * getResource(_) >> Mock(Resource) {
246+
1* getContents() >> Mock(ResourceMeta) {
247+
writeContent(_) >> { args ->
248+
args[0].write('-----BEGIN RSA PRIVATE KEY-----\ntest key content\n-----END RSA PRIVATE KEY-----'.bytes)
249+
return 65L
250+
}
251+
}
252+
}
253+
}
254+
255+
Services services = Mock(Services){
256+
1 * getService(KeyStorageTree) >> keyStorageTree
257+
}
258+
259+
when:
260+
261+
def resource = new GitResourceModel(services,configuration,framework)
262+
resource.setGitManager(gitManager)
263+
264+
def result = resource.getNodes()
265+
266+
then:
267+
1 * gitManager.getFile(path) >> inputStream
268+
result == nodeSet
269+
}
270+
219271

220272
private Framework getFramework(INodeSet nodeSet){
221273
def resourceFormatParser = Mock(ResourceFormatParser){

0 commit comments

Comments
 (0)