Skip to content

Commit 1a06bd9

Browse files
committed
Fix classloader isolation, Key Storage integration, and Ed25519 support
- Add withPluginClassLoader wrapper to set thread context classloader before JGit SSH operations, fixing TranslationBundle loading failures caused by Rundeck's plugin classloader isolation - Add ensureEdDSASupport to remove external EdDSA provider on Java 15+, allowing MINA SSHD to use native JDK Ed25519 support instead of the external net.i2p.crypto.eddsa which fails across classloader boundaries - Fix GitResourceModel Key Storage integration: properly obtain KeyStorageTree from Services with error handling, enabling Resource Model plugins to read SSH keys and passwords from Rundeck Key Storage - Replace ExecutionListener-based logging with SLF4J in GitPluginUtil for reliable logging when ExecutionListener is unavailable - Clean up log levels: debug for normal operations, warn for errors, info for one-time configuration events Made-with: Cursor
1 parent c656008 commit 1a06bd9

3 files changed

Lines changed: 89 additions & 30 deletions

File tree

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

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import org.eclipse.jgit.util.FileUtils
1515
import java.nio.file.Files
1616
import java.nio.file.Path
1717
import java.nio.file.Paths
18-
1918
import org.slf4j.Logger
2019
import org.slf4j.LoggerFactory
2120
/**
@@ -26,6 +25,51 @@ class GitManager {
2625
private static final Logger logger = LoggerFactory.getLogger(GitManager.class);
2726

2827
public static final String REMOTE_NAME = "origin"
28+
29+
/**
30+
* Executes a closure with the thread context classloader set to the plugin's classloader.
31+
* Required because Rundeck isolates plugins with separate classloaders, and JGit's
32+
* TranslationBundle mechanism uses ResourceBundle.getBundle() which relies on the
33+
* thread context classloader to find resource bundles like SshdText.
34+
* Also ensures Ed25519 key support works correctly on Java 15+.
35+
*/
36+
private static <T> T withPluginClassLoader(Closure<T> closure) {
37+
ClassLoader original = Thread.currentThread().getContextClassLoader()
38+
try {
39+
Thread.currentThread().setContextClassLoader(GitManager.class.getClassLoader())
40+
ensureEdDSASupport()
41+
return closure.call()
42+
} finally {
43+
Thread.currentThread().setContextClassLoader(original)
44+
}
45+
}
46+
47+
private static volatile boolean eddsaChecked = false
48+
49+
/**
50+
* Ensures Ed25519 key support works correctly by removing the external EdDSA provider
51+
* if present. On Java 15+, Ed25519 is supported natively by the JDK. The external
52+
* net.i2p.crypto.eddsa provider causes ClassNotFoundException due to Rundeck's plugin
53+
* classloader isolation, so it must be removed to let SSHD use native support.
54+
*/
55+
private static void ensureEdDSASupport() {
56+
if (eddsaChecked) return
57+
try {
58+
int javaVersion = Runtime.version().feature()
59+
if (javaVersion >= 15) {
60+
if (java.security.Security.getProvider("EdDSA") != null) {
61+
java.security.Security.removeProvider("EdDSA")
62+
logger.info("Removed external EdDSA provider to use native Java {} Ed25519 support", javaVersion)
63+
}
64+
} else {
65+
logger.info("Java {} does not have native Ed25519 support. Ed25519 keys require Java 15+.", javaVersion)
66+
}
67+
eddsaChecked = true
68+
} catch (Exception e) {
69+
logger.warn("EdDSA provider check failed: {}", e.message)
70+
}
71+
}
72+
2973
Git git
3074
String branch
3175
String fileName
@@ -134,7 +178,7 @@ class GitManager {
134178

135179
try {
136180
setupTransportAuthentication(sshConfig, cloneCommand, this.gitURL)
137-
git = cloneCommand.call()
181+
git = withPluginClassLoader { cloneCommand.call() }
138182
} catch (Exception e) {
139183
e.printStackTrace()
140184
logger.debug("Failed cloning the repository from ${this.gitURL}: ${e.message}", e)
@@ -151,7 +195,7 @@ class GitManager {
151195

152196
try {
153197
setupTransportAuthentication(sshConfig, pullCommand, this.gitURL)
154-
PullResult result = pullCommand.call()
198+
PullResult result = withPluginClassLoader { pullCommand.call() }
155199
if (!result.isSuccessful()) {
156200
logger.info("Pull is not successful.")
157201
} else {
@@ -171,7 +215,7 @@ class GitManager {
171215

172216
try {
173217
setupTransportAuthentication(sshConfig, pushCommand, this.gitURL)
174-
pushCommand.call()
218+
withPluginClassLoader { pushCommand.call() }
175219
logger.info("Push is not successful.")
176220
} catch (Exception e) {
177221
e.printStackTrace()
@@ -222,17 +266,17 @@ class GitManager {
222266
}
223267

224268
URIish u = new URIish(url);
225-
logger.debug("transport url ${u}, scheme ${u.scheme}, user ${u.user}")
269+
logger.debug("setupTransportAuth: url={}, scheme={}, user={}", u, u.scheme, u.user)
226270
if ((u.scheme == null || u.scheme == 'ssh') && u.user && (sshPrivateKeyPath || sshPrivateKey)) {
227271

228272
byte[] keyData
229273
if (sshPrivateKeyPath) {
230-
logger.debug("using ssh private key path ${sshPrivateKeyPath}")
274+
logger.debug("Using SSH private key from filesystem path")
231275
Path path = Paths.get(sshPrivateKeyPath);
232276
keyData = Files.readAllBytes(path);
233277

234278
} else if (sshPrivateKey) {
235-
logger.debug("using ssh private key")
279+
logger.debug("Using SSH private key from Key Storage")
236280
keyData = sshPrivateKey.getBytes()
237281
}
238282

@@ -251,7 +295,7 @@ class GitManager {
251295
PullResult gitPull(Git git1 = null) {
252296
def pullCommand = (git1 ?: git).pull().setRemote(REMOTE_NAME).setRemoteBranchName(branch)
253297
setupTransportAuthentication(sshConfig, pullCommand)
254-
pullCommand.call()
298+
withPluginClassLoader { pullCommand.call() }
255299
}
256300

257301
def gitCommitAndPush() {
@@ -277,7 +321,7 @@ class GitManager {
277321

278322
def push
279323
try {
280-
push = pushb.call()
324+
push = withPluginClassLoader { pushb.call() }
281325
} catch (Exception e) {
282326
logger.debug("Failed push to remote: ${e.message}", e)
283327
throw new Exception("Failed push to remote: ${e.message}", e)

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ import com.rundeck.plugin.util.GitPluginUtil
1616
import groovy.transform.CompileStatic
1717
import org.rundeck.app.spi.Services
1818
import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree
19+
import org.slf4j.Logger
20+
import org.slf4j.LoggerFactory
1921

2022
/**
2123
* Created by luistoledo on 12/18/17.
2224
*/
2325
@CompileStatic
2426
class GitResourceModel implements ResourceModelSource , WriteableModelSource{
2527

28+
private static final Logger logger = LoggerFactory.getLogger(GitResourceModel.class)
29+
2630
private Properties configuration;
2731
private Framework framework;
2832
private boolean writable=false;
@@ -69,29 +73,42 @@ class GitResourceModel implements ResourceModelSource , WriteableModelSource{
6973
gitManager.setSshPrivateKeyPath(configuration.getProperty(GitResourceModelFactory.GIT_KEY_PATH))
7074
}
7175

72-
// Create execution context once for Key Storage operations
76+
// Create execution context for Key Storage operations
7377
ExecutionContext context = null
7478
if (services) {
75-
context = new ExecutionContextImpl.Builder()
76-
.framework(framework)
77-
.storageTree(services.getService(KeyStorageTree.class))
78-
.build()
79+
try {
80+
KeyStorageTree storageTree = services.getService(KeyStorageTree.class)
81+
if (storageTree != null) {
82+
context = new ExecutionContextImpl.Builder()
83+
.framework(framework)
84+
.storageTree(storageTree)
85+
.build()
86+
}
87+
} catch (Exception e) {
88+
logger.warn("Failed to get KeyStorageTree from services: {}", e.message)
89+
}
7990
}
8091

81-
// Key Storage password (more secure, takes precedence if both are set)
82-
if(context && configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)){
83-
def password = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH), context)
92+
// Key Storage password (takes precedence over plain text password)
93+
String passwordStoragePath = configuration.getProperty(GitResourceModelFactory.GIT_PASSWORD_STORAGE_PATH)
94+
if(context && passwordStoragePath){
95+
def password = GitPluginUtil.getFromKeyStorage(passwordStoragePath, context)
8496
if (password != null) {
8597
gitManager.setGitPassword(password)
8698
}
8799
}
88100

89-
// SSH Key from Key Storage (takes precedence if both are set)
90-
if(context && configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)){
91-
def sshKey = GitPluginUtil.getFromKeyStorage(configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH), context)
101+
// SSH Key from Key Storage (takes precedence over filesystem path)
102+
String keyStoragePath = configuration.getProperty(GitResourceModelFactory.GIT_KEY_STORAGE_PATH)
103+
if(context && keyStoragePath){
104+
def sshKey = GitPluginUtil.getFromKeyStorage(keyStoragePath, context)
92105
if (sshKey != null) {
93106
gitManager.setSshPrivateKey(sshKey)
107+
} else {
108+
logger.warn("SSH key from Key Storage at path '{}' could not be retrieved", keyStoragePath)
94109
}
110+
} else if (keyStoragePath && !context) {
111+
logger.warn("SSH Key Storage path '{}' configured but Key Storage service is unavailable", keyStoragePath)
95112
}
96113
}
97114

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import com.dtolabs.rundeck.core.storage.ResourceMeta
55
import com.dtolabs.rundeck.plugins.step.PluginStepContext
66
import com.dtolabs.rundeck.core.execution.ExecutionContext
77
import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree
8-
import com.dtolabs.rundeck.core.execution.ExecutionListener
98
import groovy.transform.CompileStatic
109
import java.nio.charset.StandardCharsets
10+
import org.slf4j.Logger
11+
import org.slf4j.LoggerFactory
1112

1213
/**
1314
* Created by luistoledo on 12/18/17.
1415
*/
1516
@CompileStatic
1617
class GitPluginUtil {
18+
private static final Logger logger = LoggerFactory.getLogger(GitPluginUtil.class)
1719
static Map<String, Object> getRenderOpt(String value, boolean secondary, boolean password = false, boolean storagePassword = false, boolean storageKey = false) {
1820
Map<String, Object> ret = new HashMap<>();
1921
ret.put(StringRenderingConstants.GROUP_NAME,value);
@@ -81,21 +83,17 @@ class GitPluginUtil {
8183
KeyStorageTree storageTree = (KeyStorageTree)context.getStorageTree()
8284

8385
if (storageTree == null){
84-
ExecutionListener logger = context.getExecutionListener()
85-
if (logger != null) {
86-
logger.log(1, "storageTree is null. Cannot retrieve credential from Key Storage.");
87-
}
86+
logger.warn("storageTree is null. Cannot retrieve credential from Key Storage at path '{}'.", path)
8887
return null
8988
}
9089

9190
try {
9291
ResourceMeta contents = storageTree.getResource(path).getContents();
93-
return readResourceMetaAsString(contents);
92+
String result = readResourceMetaAsString(contents);
93+
logger.debug("Successfully retrieved credential from Key Storage at path '{}' ({} chars)", path, result?.length())
94+
return result
9495
} catch (Exception e) {
95-
ExecutionListener logger = context.getExecutionListener()
96-
if (logger != null) {
97-
logger.log(1, "Failed to retrieve credential from Key Storage at path '${path}': ${e.message}");
98-
}
96+
logger.warn("Failed to retrieve credential from Key Storage at path '{}': {}", path, e.message)
9997
return null
10098
}
10199
}

0 commit comments

Comments
 (0)