From 14b4424d1a7558108198c0784c1d26185736c359 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sat, 19 Nov 2022 15:43:37 +0100 Subject: [PATCH 01/36] CertificateCredentialsImplTest: add tests for local/remote Jenkins nodes [JENKINS-70101] --- pom.xml | 18 ++ .../impl/CertificateCredentialsImplTest.java | 290 ++++++++++++++++++ 2 files changed, 308 insertions(+) diff --git a/pom.xml b/pom.xml index b73040a64..215e860ef 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,24 @@ workflow-basic-steps test + + org.jenkins-ci.plugins + job-dsl + 1.81 + test + + + org.jenkins-ci.plugins + command-launcher + 1.6 + test + + + org.jenkins-ci.plugins + script-security + 1218.v39ca_7f7ed0a_c + test + diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 78b095bde..833b0b348 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -51,12 +51,29 @@ import hudson.Util; import hudson.cli.CLICommandInvoker; import hudson.cli.UpdateJobCommand; +import hudson.model.Descriptor; import hudson.model.ItemGroup; import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Result; +import hudson.model.Slave; import hudson.security.ACL; +import hudson.slaves.CommandLauncher; +import hudson.slaves.ComputerLauncher; +import hudson.slaves.DumbSlave; +import hudson.slaves.RetentionStrategy; import hudson.util.Secret; +import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; +import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + +import javaposse.jobdsl.plugin.GlobalJobDslSecurityConfiguration; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -64,8 +81,10 @@ import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.recipes.LocalData; +import org.kohsuke.stapler.StaplerRequest; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLEncoder; @@ -81,6 +100,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assume.assumeThat; public class CertificateCredentialsImplTest { @@ -96,6 +116,18 @@ public class CertificateCredentialsImplTest { private static final String INVALID_PASSWORD = "blabla"; private static final String EXPECTED_DISPLAY_NAME = "EMAILADDRESS=me@myhost.mydomain, CN=pkcs12, O=Fort-Funston, L=SanFrancisco, ST=CA, C=US"; + // See setupAgent() below + @Rule + public TemporaryFolder tmpAgent = new TemporaryFolder(); + @Rule + public TemporaryFolder tmpWorker = new TemporaryFolder(); + // Where did we save that file?.. + private File agentJar = null; + // Can this be reused for many test cases? + private Slave agent = null; + // Unknown/started/not usable + private Boolean agentUsable = null; + @Before public void setup() throws IOException { p12 = tmp.newFile("test.p12"); @@ -106,6 +138,99 @@ public void setup() throws IOException { r.jenkins.setCrumbIssuer(null); } + // Helpers for some of the test cases (initially tied to JENKINS-70101 research) + // TODO: Offload to some class many tests can call upon? + Boolean isAvailableAgent() { + // Can be used to skip optional tests if we know we could not set up an agent + if (agentJar == null) + return false; + if (agent == null) + return false; + return agentUsable; + } + + Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError { + // Note we anticipate this might fail; it should not block the whole test suite from running + // Loosely inspired by + // https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/create-agent-node-from-groovy + + // Is it known-impossible to start the agent? + if (agentUsable != null && agentUsable == false) + return agentUsable; // quickly for re-runs + + // Did we download this file for earlier test cases? + if (agentJar == null) { + try { + URL url = new URL(r.jenkins.getRootUrl() + "jnlpJars/agent.jar"); + agentJar = tmpAgent.newFile("agent.jar"); + FileOutputStream out = new FileOutputStream(agentJar); + out.write(url.openStream().readAllBytes()); + out.close(); + } catch (IOException | OutOfMemoryError e) { + agentJar = null; + agentUsable = false; + + System.out.println("Failed to download agent.jar from test instance: " + + e.toString()); + + return agentUsable; + } + } + + // This CLI spelling and quoting should play well with both Windows + // (including spaces in directory names) and Unix/Linux + ComputerLauncher launcher = new CommandLauncher( + "\"" + System.getProperty("java.home") + File.separator + "bin" + + File.separator + "java\" -jar \"" + agentJar.getAbsolutePath().toString() + "\"" + ); + + try { + // Define a "Permanent Agent" + agent = new DumbSlave( + "worker", + tmpWorker.getRoot().getAbsolutePath().toString(), + launcher); + agent.setNodeDescription("Worker in another JVM, remoting used"); + agent.setNumExecutors(1); + agent.setLabelString("worker"); + agent.setMode(Node.Mode.EXCLUSIVE); + agent.setRetentionStrategy(new RetentionStrategy.Always()); + +/* + // Add node envvars + List env = new ArrayList(); + env.add(new Entry("key1","value1")); + env.add(new Entry("key2","value2")); + EnvironmentVariablesNodeProperty envPro = new EnvironmentVariablesNodeProperty(env); + agent.getNodeProperties().add(envPro); +*/ + + r.jenkins.addNode(agent); + + String agentLog = null; + agentUsable = false; + for (long i = 0; i < 5; i++) { + Thread.sleep(1000); + agentLog = agent.getComputer().getLog(); + if (i == 2 && (agentLog == null || agentLog.isEmpty())) { + // Give it a little time to autostart, then kick it up if needed: + agent.getComputer().connect(true); // "always" should have started it; avoid duplicate runs + } + if (agentLog != null && agentLog.contains("Agent successfully connected and online")) { + agentUsable = true; + break; + } + } + System.out.println("Spawned build agent " + + "usability: " + agentUsable.toString() + + "; connection log:" + (agentLog == null ? " " : "\n" + agentLog)); + } catch (Descriptor.FormException | NullPointerException e) { + agentUsable = false; + } + + return agentUsable; + } + @Test public void displayName() throws IOException { SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); @@ -397,4 +522,169 @@ private CredentialsStore getFolderStore(Folder f) { return folderStore; } + // Helper for a few tests below + // Roughly follows what tests above were proven to succeed doing + private void prepareUploadedKeystore() throws IOException { + prepareUploadedKeystore("myCert", "password"); + } + + private void prepareUploadedKeystore(String id, String password) throws IOException { + SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); + CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(uploadedKeystore); + CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(null, id, null, password, storeSource); + SystemCredentialsProvider.getInstance().getCredentials().add(credentials); + SystemCredentialsProvider.getInstance().save(); + } + + String cpsScriptCredentialTestImports() { + return "import com.cloudbees.plugins.credentials.CredentialsMatchers;\n" + + "import com.cloudbees.plugins.credentials.CredentialsProvider;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;\n" + + "import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + + "import hudson.security.ACL;\n" + + "import java.security.KeyStore;\n" + + "\n"; + } + + String cpsScriptCredentialTest(String runnerTag) { + return cpsScriptCredentialTest("myCert", "password", runnerTag); + } + + String cpsScriptCredentialTest(String id, String password, String runnerTag) { + return "def authentication='" + id + "';\n" + + "def password='" + password + "';\n" + + "StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + + " CredentialsProvider.lookupCredentials(\n" + + " StandardCredentials.class,\n" + + " Jenkins.instance, null, null),\n" + + " CredentialsMatchers.withId(authentication));\n" + + "StandardCredentials credentialSnap = CredentialsProvider.snapshot(credential);\n\n" + + "\n" + + "echo \"CRED ON " + runnerTag + ":\"\n" + + "echo credential.toString()\n" + + "KeyStore keyStore = credential.getKeyStore();\n" + + "KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + + "echo \"KSS: \" + kss.toString()\n" + + "byte[] kssb = kss.getKeyStoreBytes();\n" + + "echo \"KSS bytes (len): \" + kssb.length\n" + + "\n" + + "echo \"CRED-SNAP ON " + runnerTag + ":\"\n" + + "echo credentialSnap.toString()\n" + + "KeyStore keyStoreSnap = credentialSnap.getKeyStore();\n" + + "KeyStoreSource kssSnap = ((CertificateCredentialsImpl) credentialSnap).getKeyStoreSource();\n" + + "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + + "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + + "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + + "\n"; + } + + private void relaxScriptSecurityScript(String script) throws IOException { + ScriptApproval.get().preapprove(script, GroovyLanguage.get()); + for (ScriptApproval.PendingScript p : ScriptApproval.get().getPendingScripts()) { + ScriptApproval.get().approveScript(p.getHash()); + } + } + + private void relaxScriptSecurityGlobal() throws IOException { + StaplerRequest stapler = null; + net.sf.json.JSONObject jsonObject = new net.sf.json.JSONObject(); + jsonObject.put("useScriptSecurity", false); + GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).configure(stapler, jsonObject); + GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save(); +/* + GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false; + GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save(); + */ + } + + private void relaxScriptSecurityCredentialTestSignatures() throws IOException { + ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.common.CertificateCredentials getKeyStore"); + ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl getKeyStoreSource"); + ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl$KeyStoreSource getKeyStoreBytes"); + ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsMatchers firstOrNull java.lang.Iterable com.cloudbees.plugins.credentials.CredentialsMatcher"); + ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsMatchers withId java.lang.String"); + ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsProvider lookupCredentials java.lang.Class hudson.model.ItemGroup org.acegisecurity.Authentication java.util.List"); + ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsProvider snapshot com.cloudbees.plugins.credentials.Credentials"); + ScriptApproval.get().approveSignature("staticMethod jenkins.model.Jenkins getInstance"); + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnController() throws Exception { + // Check that credentials are usable with pipeline script + // running without a node{} + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + cpsScriptCredentialTest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, true)); + relaxScriptSecurityCredentialTestSignatures(); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnNodeLocal() throws Exception { + // Check that credentials are usable with pipeline script + // running on a node{} (provided by the controller) + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node {\n" + + cpsScriptCredentialTest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, true)); + relaxScriptSecurityCredentialTestSignatures(); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node(\"worker\") {\n" + + cpsScriptCredentialTest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, true)); + relaxScriptSecurityCredentialTestSignatures(); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } } From 666186a35ecb9968856fb6441e498a452495aea4 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 12:53:38 +0100 Subject: [PATCH 02/36] CertificateCredentialsImplTest: simplify relaxation of pipeline script security --- pom.xml | 6 --- .../impl/CertificateCredentialsImplTest.java | 45 ++----------------- 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/pom.xml b/pom.xml index 215e860ef..5a790d309 100644 --- a/pom.xml +++ b/pom.xml @@ -173,12 +173,6 @@ 1.6 test - - org.jenkins-ci.plugins - script-security - 1218.v39ca_7f7ed0a_c - test - diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 833b0b348..119ab5416 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -63,17 +63,12 @@ import hudson.slaves.DumbSlave; import hudson.slaves.RetentionStrategy; import hudson.util.Secret; -import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; -import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; -import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import javaposse.jobdsl.plugin.GlobalJobDslSecurityConfiguration; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -81,7 +76,6 @@ import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.recipes.LocalData; -import org.kohsuke.stapler.StaplerRequest; import java.io.File; import java.io.FileOutputStream; @@ -582,36 +576,6 @@ String cpsScriptCredentialTest(String id, String password, String runnerTag) { "\n"; } - private void relaxScriptSecurityScript(String script) throws IOException { - ScriptApproval.get().preapprove(script, GroovyLanguage.get()); - for (ScriptApproval.PendingScript p : ScriptApproval.get().getPendingScripts()) { - ScriptApproval.get().approveScript(p.getHash()); - } - } - - private void relaxScriptSecurityGlobal() throws IOException { - StaplerRequest stapler = null; - net.sf.json.JSONObject jsonObject = new net.sf.json.JSONObject(); - jsonObject.put("useScriptSecurity", false); - GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).configure(stapler, jsonObject); - GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save(); -/* - GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false; - GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save(); - */ - } - - private void relaxScriptSecurityCredentialTestSignatures() throws IOException { - ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.common.CertificateCredentials getKeyStore"); - ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl getKeyStoreSource"); - ScriptApproval.get().approveSignature("method com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl$KeyStoreSource getKeyStoreBytes"); - ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsMatchers firstOrNull java.lang.Iterable com.cloudbees.plugins.credentials.CredentialsMatcher"); - ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsMatchers withId java.lang.String"); - ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsProvider lookupCredentials java.lang.Class hudson.model.ItemGroup org.acegisecurity.Authentication java.util.List"); - ScriptApproval.get().approveSignature("staticMethod com.cloudbees.plugins.credentials.CredentialsProvider snapshot com.cloudbees.plugins.credentials.Credentials"); - ScriptApproval.get().approveSignature("staticMethod jenkins.model.Jenkins getInstance"); - } - @Test @Issue("JENKINS-70101") public void keyStoreReadableOnController() throws Exception { @@ -623,8 +587,7 @@ public void keyStoreReadableOnController() throws Exception { WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + cpsScriptCredentialTest("CONTROLLER BUILT-IN"); - proj.setDefinition(new CpsFlowDefinition(script, true)); - relaxScriptSecurityCredentialTestSignatures(); + proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); @@ -648,8 +611,7 @@ public void keyStoreReadableOnNodeLocal() throws Exception { "node {\n" + cpsScriptCredentialTest("CONTROLLER NODE") + "}\n"; - proj.setDefinition(new CpsFlowDefinition(script, true)); - relaxScriptSecurityCredentialTestSignatures(); + proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); @@ -676,8 +638,7 @@ public void keyStoreReadableOnNodeRemote() throws Exception { "node(\"worker\") {\n" + cpsScriptCredentialTest("REMOTE NODE") + "}\n"; - proj.setDefinition(new CpsFlowDefinition(script, true)); - relaxScriptSecurityCredentialTestSignatures(); + proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); From 58c4e2682469a4d9170af2ffe8e1774da52e3bc6 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 13:54:35 +0100 Subject: [PATCH 03/36] CertificateCredentialsImplTest: add withCredentials() tests --- pom.xml | 6 +++++ .../impl/CertificateCredentialsImplTest.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pom.xml b/pom.xml index 5a790d309..0ea2a9c2c 100644 --- a/pom.xml +++ b/pom.xml @@ -173,6 +173,12 @@ 1.6 test + + org.jenkins-ci.plugins + credentials-binding + 523.vd859a_4b_122e6 + test + diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 119ab5416..7b0b19c58 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -541,6 +541,15 @@ String cpsScriptCredentialTestImports() { "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + "import hudson.security.ACL;\n" + "import java.security.KeyStore;\n" + + "\n" + + "@NonCPS\n" + + "def getKey(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + + " def p12file = new FileInputStream(keystoreName)\n" + + " def keystore = KeyStore.getInstance(keystoreFormat)\n" + + " keystore.load(p12file, keyPassword.toCharArray())\n" + + " def key = keystore.getKey(alias, keyPassword.toCharArray())\n" + + " return key.getEncoded().encodeBase64().toString()\n" + + "}\n" + "\n"; } @@ -573,6 +582,21 @@ String cpsScriptCredentialTest(String id, String password, String runnerTag) { "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + + "\n" + + "echo \"WITH-CREDENTIAL ON " + runnerTag + ":\"\n" + // https://groups.google.com/g/jenkinsci-users/c/evyx0O3bMWE + "withCredentials([certificate(\n" + + " credentialsId: authentication,\n" + + " keystoreVariable: 'keystoreName',\n" + + " passwordVariable: 'keyPassword',\n" + + " aliasVariable: 'myKeyAlias')\n" + + "]) {\n" + + " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + + " def keystoreFormat = \"PKCS12\"\n" + + " def keyValue = '' //getKeyValue(keystoreName, keystoreFormat, keyPassword, myKeyAlias)\n" + + " println \"-----BEGIN PRIVATE KEY-----\"\n" + + " println keyValue\n" + + " println \"-----END PRIVATE KEY-----\"\n" + + "}\n" + "\n"; } From f4afd9f0a6aaa6b8d3519269729cd2b5076c21fd Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 14:15:21 +0100 Subject: [PATCH 04/36] Move multi-agent pipeline tests to dedicated CredentialsInPipelineTest source --- .../CredentialsInPipelineTest.java | 392 ++++++++++++++++++ .../impl/CertificateCredentialsImplTest.java | 275 ------------ 2 files changed, 392 insertions(+), 275 deletions(-) create mode 100644 src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java new file mode 100644 index 000000000..7585bb333 --- /dev/null +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -0,0 +1,392 @@ +/* + * The MIT License + * + * Copyright 2022 Jim Klimov. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.cloudbees.plugins.credentials; + +import com.cloudbees.hudson.plugins.folder.Folder; +import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsNameProvider; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.SecretBytes; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.common.CertificateCredentials; +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; +import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; +import com.gargoylesoftware.htmlunit.FormEncodingType; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.DomNode; +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; +import com.gargoylesoftware.htmlunit.html.HtmlFileInput; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlOption; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput; +import hudson.FilePath; +import hudson.Util; +import hudson.cli.CLICommandInvoker; +import hudson.cli.UpdateJobCommand; +import hudson.model.Descriptor; +import hudson.model.ItemGroup; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Result; +import hudson.model.Slave; +import hudson.security.ACL; +import hudson.slaves.CommandLauncher; +import hudson.slaves.ComputerLauncher; +import hudson.slaves.DumbSlave; +import hudson.slaves.RetentionStrategy; +import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +import static hudson.cli.CLICommandInvoker.Matcher.failedWith; +import static hudson.cli.CLICommandInvoker.Matcher.succeeded; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assume.assumeThat; + +public class CredentialsInPipelineTest { + /** + * The CredentialsInPipelineTest suite prepares pipeline scripts to + * retrieve some previously saved credentials, on the controller, + * on a node provided by it, and on a worker agent in separate JVM. + * This picks known-working test cases and their setup from other + * test classes which address those credential types in more detail. + * Initially tied to JENKINS-70101 research. + */ + + @Rule + public JenkinsRule r = new JenkinsRule(); + + // Data for build agent setup + @Rule + public TemporaryFolder tmpAgent = new TemporaryFolder(); + @Rule + public TemporaryFolder tmpWorker = new TemporaryFolder(); + // Where did we save that file?.. + private File agentJar = null; + // Can this be reused for many test cases? + private Slave agent = null; + // Unknown/started/not usable + private Boolean agentUsable = null; + + // From CertificateCredentialImplTest + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + private File p12; + + @Before + public void setup() { + r.jenkins.setCrumbIssuer(null); + } + + Boolean isAvailableAgent() { + // Can be used to skip optional tests if we know we could not set up an agent + if (agentJar == null) + return false; + if (agent == null) + return false; + return agentUsable; + } + + Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError { + // Note we anticipate this might fail; it should not block the whole test suite from running + // Loosely inspired by + // https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/create-agent-node-from-groovy + + // Is it known-impossible to start the agent? + if (agentUsable != null && agentUsable == false) + return agentUsable; // quickly for re-runs + + // Did we download this file for earlier test cases? + if (agentJar == null) { + try { + URL url = new URL(r.jenkins.getRootUrl() + "jnlpJars/agent.jar"); + agentJar = tmpAgent.newFile("agent.jar"); + FileOutputStream out = new FileOutputStream(agentJar); + out.write(url.openStream().readAllBytes()); + out.close(); + } catch (IOException | OutOfMemoryError e) { + agentJar = null; + agentUsable = false; + + System.out.println("Failed to download agent.jar from test instance: " + + e.toString()); + + return agentUsable; + } + } + + // This CLI spelling and quoting should play well with both Windows + // (including spaces in directory names) and Unix/Linux + ComputerLauncher launcher = new CommandLauncher( + "\"" + System.getProperty("java.home") + File.separator + "bin" + + File.separator + "java\" -jar \"" + agentJar.getAbsolutePath().toString() + "\"" + ); + + try { + // Define a "Permanent Agent" + agent = new DumbSlave( + "worker", + tmpWorker.getRoot().getAbsolutePath().toString(), + launcher); + agent.setNodeDescription("Worker in another JVM, remoting used"); + agent.setNumExecutors(1); + agent.setLabelString("worker"); + agent.setMode(Node.Mode.EXCLUSIVE); + agent.setRetentionStrategy(new RetentionStrategy.Always()); + +/* + // Add node envvars + List env = new ArrayList(); + env.add(new Entry("key1","value1")); + env.add(new Entry("key2","value2")); + EnvironmentVariablesNodeProperty envPro = new EnvironmentVariablesNodeProperty(env); + agent.getNodeProperties().add(envPro); +*/ + + r.jenkins.addNode(agent); + + String agentLog = null; + agentUsable = false; + for (long i = 0; i < 5; i++) { + Thread.sleep(1000); + agentLog = agent.getComputer().getLog(); + if (i == 2 && (agentLog == null || agentLog.isEmpty())) { + // Give it a little time to autostart, then kick it up if needed: + agent.getComputer().connect(true); // "always" should have started it; avoid duplicate runs + } + if (agentLog != null && agentLog.contains("Agent successfully connected and online")) { + agentUsable = true; + break; + } + } + System.out.println("Spawned build agent " + + "usability: " + agentUsable.toString() + + "; connection log:" + (agentLog == null ? " " : "\n" + agentLog)); + } catch (Descriptor.FormException | NullPointerException e) { + agentUsable = false; + } + + return agentUsable; + } + + ///////////////////////////////////////////////////////////////// + // Certificate credentials tests + ///////////////////////////////////////////////////////////////// + + // Partially from CertificateCredentialImplTest setup() + private void prepareUploadedKeystore() throws IOException { + prepareUploadedKeystore("myCert", "password"); + } + + private void prepareUploadedKeystore(String id, String password) throws IOException { + if (p12 == null) { + p12 = tmp.newFile("test.p12"); + FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("test.p12"), p12); + } + + SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); + CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(uploadedKeystore); + CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(null, id, null, password, storeSource); + SystemCredentialsProvider.getInstance().getCredentials().add(credentials); + SystemCredentialsProvider.getInstance().save(); + } + + String cpsScriptCredentialTestImports() { + return "import com.cloudbees.plugins.credentials.CredentialsMatchers;\n" + + "import com.cloudbees.plugins.credentials.CredentialsProvider;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;\n" + + "import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + + "import hudson.security.ACL;\n" + + "import java.security.KeyStore;\n" + + "\n" + + "@NonCPS\n" + + "def getKey(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + + " def p12file = new FileInputStream(keystoreName)\n" + + " def keystore = KeyStore.getInstance(keystoreFormat)\n" + + " keystore.load(p12file, keyPassword.toCharArray())\n" + + " def key = keystore.getKey(alias, keyPassword.toCharArray())\n" + + " return key.getEncoded().encodeBase64().toString()\n" + + "}\n" + + "\n"; + } + + String cpsScriptCredentialTest(String runnerTag) { + return cpsScriptCredentialTest("myCert", "password", runnerTag); + } + + String cpsScriptCredentialTest(String id, String password, String runnerTag) { + return "def authentication='" + id + "';\n" + + "def password='" + password + "';\n" + + "StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + + " CredentialsProvider.lookupCredentials(\n" + + " StandardCredentials.class,\n" + + " Jenkins.instance, null, null),\n" + + " CredentialsMatchers.withId(authentication));\n" + + "StandardCredentials credentialSnap = CredentialsProvider.snapshot(credential);\n\n" + + "\n" + + "echo \"CRED ON " + runnerTag + ":\"\n" + + "echo credential.toString()\n" + + "KeyStore keyStore = credential.getKeyStore();\n" + + "KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + + "echo \"KSS: \" + kss.toString()\n" + + "byte[] kssb = kss.getKeyStoreBytes();\n" + + "echo \"KSS bytes (len): \" + kssb.length\n" + + "\n" + + "echo \"CRED-SNAP ON " + runnerTag + ":\"\n" + + "echo credentialSnap.toString()\n" + + "KeyStore keyStoreSnap = credentialSnap.getKeyStore();\n" + + "KeyStoreSource kssSnap = ((CertificateCredentialsImpl) credentialSnap).getKeyStoreSource();\n" + + "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + + "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + + "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + + "\n" + + "echo \"WITH-CREDENTIAL ON " + runnerTag + ":\"\n" + // https://groups.google.com/g/jenkinsci-users/c/evyx0O3bMWE + "withCredentials([certificate(\n" + + " credentialsId: authentication,\n" + + " keystoreVariable: 'keystoreName',\n" + + " passwordVariable: 'keyPassword',\n" + + " aliasVariable: 'myKeyAlias')\n" + + "]) {\n" + + " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + + " def keystoreFormat = \"PKCS12\"\n" + + " def keyValue = '' //getKeyValue(keystoreName, keystoreFormat, keyPassword, myKeyAlias)\n" + + " println \"-----BEGIN PRIVATE KEY-----\"\n" + + " println keyValue\n" + + " println \"-----END PRIVATE KEY-----\"\n" + + "}\n" + + "\n"; + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnController() throws Exception { + // Check that credentials are usable with pipeline script + // running without a node{} + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + cpsScriptCredentialTest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnNodeLocal() throws Exception { + // Check that credentials are usable with pipeline script + // running on a node{} (provided by the controller) + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node {\n" + + cpsScriptCredentialTest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void keyStoreReadableOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node(\"worker\") {\n" + + cpsScriptCredentialTest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + +} diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 7b0b19c58..78b095bde 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -51,24 +51,12 @@ import hudson.Util; import hudson.cli.CLICommandInvoker; import hudson.cli.UpdateJobCommand; -import hudson.model.Descriptor; import hudson.model.ItemGroup; import hudson.model.Job; -import hudson.model.Node; -import hudson.model.Result; -import hudson.model.Slave; import hudson.security.ACL; -import hudson.slaves.CommandLauncher; -import hudson.slaves.ComputerLauncher; -import hudson.slaves.DumbSlave; -import hudson.slaves.RetentionStrategy; import hudson.util.Secret; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; -import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -78,7 +66,6 @@ import org.jvnet.hudson.test.recipes.LocalData; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLEncoder; @@ -94,7 +81,6 @@ import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assume.assumeThat; public class CertificateCredentialsImplTest { @@ -110,18 +96,6 @@ public class CertificateCredentialsImplTest { private static final String INVALID_PASSWORD = "blabla"; private static final String EXPECTED_DISPLAY_NAME = "EMAILADDRESS=me@myhost.mydomain, CN=pkcs12, O=Fort-Funston, L=SanFrancisco, ST=CA, C=US"; - // See setupAgent() below - @Rule - public TemporaryFolder tmpAgent = new TemporaryFolder(); - @Rule - public TemporaryFolder tmpWorker = new TemporaryFolder(); - // Where did we save that file?.. - private File agentJar = null; - // Can this be reused for many test cases? - private Slave agent = null; - // Unknown/started/not usable - private Boolean agentUsable = null; - @Before public void setup() throws IOException { p12 = tmp.newFile("test.p12"); @@ -132,99 +106,6 @@ public void setup() throws IOException { r.jenkins.setCrumbIssuer(null); } - // Helpers for some of the test cases (initially tied to JENKINS-70101 research) - // TODO: Offload to some class many tests can call upon? - Boolean isAvailableAgent() { - // Can be used to skip optional tests if we know we could not set up an agent - if (agentJar == null) - return false; - if (agent == null) - return false; - return agentUsable; - } - - Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError { - // Note we anticipate this might fail; it should not block the whole test suite from running - // Loosely inspired by - // https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/create-agent-node-from-groovy - - // Is it known-impossible to start the agent? - if (agentUsable != null && agentUsable == false) - return agentUsable; // quickly for re-runs - - // Did we download this file for earlier test cases? - if (agentJar == null) { - try { - URL url = new URL(r.jenkins.getRootUrl() + "jnlpJars/agent.jar"); - agentJar = tmpAgent.newFile("agent.jar"); - FileOutputStream out = new FileOutputStream(agentJar); - out.write(url.openStream().readAllBytes()); - out.close(); - } catch (IOException | OutOfMemoryError e) { - agentJar = null; - agentUsable = false; - - System.out.println("Failed to download agent.jar from test instance: " + - e.toString()); - - return agentUsable; - } - } - - // This CLI spelling and quoting should play well with both Windows - // (including spaces in directory names) and Unix/Linux - ComputerLauncher launcher = new CommandLauncher( - "\"" + System.getProperty("java.home") + File.separator + "bin" + - File.separator + "java\" -jar \"" + agentJar.getAbsolutePath().toString() + "\"" - ); - - try { - // Define a "Permanent Agent" - agent = new DumbSlave( - "worker", - tmpWorker.getRoot().getAbsolutePath().toString(), - launcher); - agent.setNodeDescription("Worker in another JVM, remoting used"); - agent.setNumExecutors(1); - agent.setLabelString("worker"); - agent.setMode(Node.Mode.EXCLUSIVE); - agent.setRetentionStrategy(new RetentionStrategy.Always()); - -/* - // Add node envvars - List env = new ArrayList(); - env.add(new Entry("key1","value1")); - env.add(new Entry("key2","value2")); - EnvironmentVariablesNodeProperty envPro = new EnvironmentVariablesNodeProperty(env); - agent.getNodeProperties().add(envPro); -*/ - - r.jenkins.addNode(agent); - - String agentLog = null; - agentUsable = false; - for (long i = 0; i < 5; i++) { - Thread.sleep(1000); - agentLog = agent.getComputer().getLog(); - if (i == 2 && (agentLog == null || agentLog.isEmpty())) { - // Give it a little time to autostart, then kick it up if needed: - agent.getComputer().connect(true); // "always" should have started it; avoid duplicate runs - } - if (agentLog != null && agentLog.contains("Agent successfully connected and online")) { - agentUsable = true; - break; - } - } - System.out.println("Spawned build agent " + - "usability: " + agentUsable.toString() + - "; connection log:" + (agentLog == null ? " " : "\n" + agentLog)); - } catch (Descriptor.FormException | NullPointerException e) { - agentUsable = false; - } - - return agentUsable; - } - @Test public void displayName() throws IOException { SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); @@ -516,160 +397,4 @@ private CredentialsStore getFolderStore(Folder f) { return folderStore; } - // Helper for a few tests below - // Roughly follows what tests above were proven to succeed doing - private void prepareUploadedKeystore() throws IOException { - prepareUploadedKeystore("myCert", "password"); - } - - private void prepareUploadedKeystore(String id, String password) throws IOException { - SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); - CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(uploadedKeystore); - CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(null, id, null, password, storeSource); - SystemCredentialsProvider.getInstance().getCredentials().add(credentials); - SystemCredentialsProvider.getInstance().save(); - } - - String cpsScriptCredentialTestImports() { - return "import com.cloudbees.plugins.credentials.CredentialsMatchers;\n" + - "import com.cloudbees.plugins.credentials.CredentialsProvider;\n" + - "import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;\n" + - "import com.cloudbees.plugins.credentials.common.StandardCredentials;\n" + - "import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;\n" + - "import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;\n" + - "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;\n" + - "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + - "import hudson.security.ACL;\n" + - "import java.security.KeyStore;\n" + - "\n" + - "@NonCPS\n" + - "def getKey(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + - " def p12file = new FileInputStream(keystoreName)\n" + - " def keystore = KeyStore.getInstance(keystoreFormat)\n" + - " keystore.load(p12file, keyPassword.toCharArray())\n" + - " def key = keystore.getKey(alias, keyPassword.toCharArray())\n" + - " return key.getEncoded().encodeBase64().toString()\n" + - "}\n" + - "\n"; - } - - String cpsScriptCredentialTest(String runnerTag) { - return cpsScriptCredentialTest("myCert", "password", runnerTag); - } - - String cpsScriptCredentialTest(String id, String password, String runnerTag) { - return "def authentication='" + id + "';\n" + - "def password='" + password + "';\n" + - "StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + - " CredentialsProvider.lookupCredentials(\n" + - " StandardCredentials.class,\n" + - " Jenkins.instance, null, null),\n" + - " CredentialsMatchers.withId(authentication));\n" + - "StandardCredentials credentialSnap = CredentialsProvider.snapshot(credential);\n\n" + - "\n" + - "echo \"CRED ON " + runnerTag + ":\"\n" + - "echo credential.toString()\n" + - "KeyStore keyStore = credential.getKeyStore();\n" + - "KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + - "echo \"KSS: \" + kss.toString()\n" + - "byte[] kssb = kss.getKeyStoreBytes();\n" + - "echo \"KSS bytes (len): \" + kssb.length\n" + - "\n" + - "echo \"CRED-SNAP ON " + runnerTag + ":\"\n" + - "echo credentialSnap.toString()\n" + - "KeyStore keyStoreSnap = credentialSnap.getKeyStore();\n" + - "KeyStoreSource kssSnap = ((CertificateCredentialsImpl) credentialSnap).getKeyStoreSource();\n" + - "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + - "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + - "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + - "\n" + - "echo \"WITH-CREDENTIAL ON " + runnerTag + ":\"\n" + // https://groups.google.com/g/jenkinsci-users/c/evyx0O3bMWE - "withCredentials([certificate(\n" + - " credentialsId: authentication,\n" + - " keystoreVariable: 'keystoreName',\n" + - " passwordVariable: 'keyPassword',\n" + - " aliasVariable: 'myKeyAlias')\n" + - "]) {\n" + - " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + - " def keystoreFormat = \"PKCS12\"\n" + - " def keyValue = '' //getKeyValue(keystoreName, keystoreFormat, keyPassword, myKeyAlias)\n" + - " println \"-----BEGIN PRIVATE KEY-----\"\n" + - " println keyValue\n" + - " println \"-----END PRIVATE KEY-----\"\n" + - "}\n" + - "\n"; - } - - @Test - @Issue("JENKINS-70101") - public void keyStoreReadableOnController() throws Exception { - // Check that credentials are usable with pipeline script - // running without a node{} - prepareUploadedKeystore(); - - // Configure the build to use the credential - WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTest("CONTROLLER BUILT-IN"); - proj.setDefinition(new CpsFlowDefinition(script, false)); - - // Execute the build - WorkflowRun run = proj.scheduleBuild2(0).get(); - - // Check expectations - r.assertBuildStatus(Result.SUCCESS, run); - // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); - } - - @Test - @Issue("JENKINS-70101") - public void keyStoreReadableOnNodeLocal() throws Exception { - // Check that credentials are usable with pipeline script - // running on a node{} (provided by the controller) - prepareUploadedKeystore(); - - // Configure the build to use the credential - WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = cpsScriptCredentialTestImports() + - "node {\n" + - cpsScriptCredentialTest("CONTROLLER NODE") + - "}\n"; - proj.setDefinition(new CpsFlowDefinition(script, false)); - - // Execute the build - WorkflowRun run = proj.scheduleBuild2(0).get(); - - // Check expectations - r.assertBuildStatus(Result.SUCCESS, run); - // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); - } - - @Test - @Issue("JENKINS-70101") - public void keyStoreReadableOnNodeRemote() throws Exception { - // Check that credentials are usable with pipeline script - // running on a remote node{} with separate JVM (check - // that remoting/snapshot work properly) - assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); - - prepareUploadedKeystore(); - - // Configure the build to use the credential - WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = cpsScriptCredentialTestImports() + - "node(\"worker\") {\n" + - cpsScriptCredentialTest("REMOTE NODE") + - "}\n"; - proj.setDefinition(new CpsFlowDefinition(script, false)); - - // Execute the build - WorkflowRun run = proj.scheduleBuild2(0).get(); - - // Check expectations - r.assertBuildStatus(Result.SUCCESS, run); - // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); - } } From f4896f908554d7eb8b646eedf12cc73e86cc31c1 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 14:25:13 +0100 Subject: [PATCH 05/36] CredentialsInPipelineTest: separate cpsScriptCredentialTestWithCredentials() et al to standalone cases --- .../CredentialsInPipelineTest.java | 144 +++++++++++++++--- 1 file changed, 123 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 7585bb333..756ca7989 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -72,6 +72,7 @@ import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -235,6 +236,8 @@ private void prepareUploadedKeystore() throws IOException { private void prepareUploadedKeystore(String id, String password) throws IOException { if (p12 == null) { + // Contains a private key + openvpn certs, + // as alias named "1" (according to keytool) p12 = tmp.newFile("test.p12"); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("test.p12"), p12); } @@ -257,18 +260,13 @@ String cpsScriptCredentialTestImports() { "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + "import hudson.security.ACL;\n" + "import java.security.KeyStore;\n" + - "\n" + - "@NonCPS\n" + - "def getKey(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + - " def p12file = new FileInputStream(keystoreName)\n" + - " def keystore = KeyStore.getInstance(keystoreFormat)\n" + - " keystore.load(p12file, keyPassword.toCharArray())\n" + - " def key = keystore.getKey(alias, keyPassword.toCharArray())\n" + - " return key.getEncoded().encodeBase64().toString()\n" + - "}\n" + "\n"; } + ///////////////////////////////////////////////////////////////// + // Certificate credentials retrievability in (trusted) pipeline + ///////////////////////////////////////////////////////////////// + String cpsScriptCredentialTest(String runnerTag) { return cpsScriptCredentialTest("myCert", "password", runnerTag); } @@ -298,8 +296,107 @@ String cpsScriptCredentialTest(String id, String password, String runnerTag) { "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + - "\n" + - "echo \"WITH-CREDENTIAL ON " + runnerTag + ":\"\n" + // https://groups.google.com/g/jenkinsci-users/c/evyx0O3bMWE + "\n"; + } + + @Test + @Issue("JENKINS-70101") + public void testCertKeyStoreReadableOnController() throws Exception { + // Check that credentials are usable with pipeline script + // running without a node{} + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + cpsScriptCredentialTest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void testCertKeyStoreReadableOnNodeLocal() throws Exception { + // Check that credentials are usable with pipeline script + // running on a node{} (provided by the controller) + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node {\n" + + cpsScriptCredentialTest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + @Test + @Issue("JENKINS-70101") + public void testCertKeyStoreReadableOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCredentialTestImports() + + "node(\"worker\") {\n" + + cpsScriptCredentialTest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("KSS-SNAP bytes", run); + } + + ///////////////////////////////////////////////////////////////// + // Certificate credentials retrievability by withCredentials() step + ///////////////////////////////////////////////////////////////// + + String cpsScriptCredentialTestGetKeyValue() { + return "@NonCPS\n" + + "def getKeyValue(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + + " def p12file = new FileInputStream(keystoreName)\n" + + " def keystore = KeyStore.getInstance(keystoreFormat)\n" + + " keystore.load(p12file, keyPassword.toCharArray())\n" + + " p12file.close()\n" + + " def key = keystore.getKey(alias ? alias : \"1\", keyPassword.toCharArray())\n" + + " return key.getEncoded().encodeBase64().toString()\n" + + "}\n" + + "\n"; + } + + String cpsScriptCredentialTestWithCredentials(String runnerTag) { + return cpsScriptCredentialTestWithCredentials("myCert", "password", runnerTag); + } + + String cpsScriptCredentialTestWithCredentials(String id, String password, String runnerTag) { + return "def authentication='" + id + "';\n" + + "def password='" + password + "';\n" + + "echo \"WITH-CREDENTIALS ON " + runnerTag + ":\"\n" + "withCredentials([certificate(\n" + " credentialsId: authentication,\n" + " keystoreVariable: 'keystoreName',\n" + @@ -307,8 +404,9 @@ String cpsScriptCredentialTest(String id, String password, String runnerTag) { " aliasVariable: 'myKeyAlias')\n" + "]) {\n" + " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + + " echo \"Got expected password? ${keyPassword == password}\"\n" + " def keystoreFormat = \"PKCS12\"\n" + - " def keyValue = '' //getKeyValue(keystoreName, keystoreFormat, keyPassword, myKeyAlias)\n" + + " def keyValue = getKeyValue(keystoreName, keystoreFormat, keyPassword, env?.myKeyAlias)\n" + " println \"-----BEGIN PRIVATE KEY-----\"\n" + " println keyValue\n" + " println \"-----END PRIVATE KEY-----\"\n" + @@ -317,8 +415,9 @@ String cpsScriptCredentialTest(String id, String password, String runnerTag) { } @Test + @Ignore("Work with keystore file requires a node") @Issue("JENKINS-70101") - public void keyStoreReadableOnController() throws Exception { + public void testCertWithCredentialsOnController() throws Exception { // Check that credentials are usable with pipeline script // running without a node{} prepareUploadedKeystore(); @@ -326,7 +425,8 @@ public void keyStoreReadableOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTest("CONTROLLER BUILT-IN"); + cpsScriptCredentialTestGetKeyValue() + + cpsScriptCredentialTestWithCredentials("CONTROLLER BUILT-IN"); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -335,12 +435,12 @@ public void keyStoreReadableOnController() throws Exception { // Check expectations r.assertBuildStatus(Result.SUCCESS, run); // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); + r.assertLogContains("END PRIVATE KEY", run); } @Test @Issue("JENKINS-70101") - public void keyStoreReadableOnNodeLocal() throws Exception { + public void testCertWithCredentialsOnNodeLocal() throws Exception { // Check that credentials are usable with pipeline script // running on a node{} (provided by the controller) prepareUploadedKeystore(); @@ -348,8 +448,9 @@ public void keyStoreReadableOnNodeLocal() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + + cpsScriptCredentialTestGetKeyValue() + "node {\n" + - cpsScriptCredentialTest("CONTROLLER NODE") + + cpsScriptCredentialTestWithCredentials("CONTROLLER NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -359,12 +460,12 @@ public void keyStoreReadableOnNodeLocal() throws Exception { // Check expectations r.assertBuildStatus(Result.SUCCESS, run); // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); + r.assertLogContains("END PRIVATE KEY", run); } @Test @Issue("JENKINS-70101") - public void keyStoreReadableOnNodeRemote() throws Exception { + public void testCertWithCredentialsOnNodeRemote() throws Exception { // Check that credentials are usable with pipeline script // running on a remote node{} with separate JVM (check // that remoting/snapshot work properly) @@ -375,8 +476,9 @@ public void keyStoreReadableOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + + cpsScriptCredentialTestGetKeyValue() + "node(\"worker\") {\n" + - cpsScriptCredentialTest("REMOTE NODE") + + cpsScriptCredentialTestWithCredentials("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -386,7 +488,7 @@ public void keyStoreReadableOnNodeRemote() throws Exception { // Check expectations r.assertBuildStatus(Result.SUCCESS, run); // Got to the end? - r.assertLogContains("KSS-SNAP bytes", run); + r.assertLogContains("END PRIVATE KEY", run); } } From 3077afa9f67d70d9c76db95d14e6eb951ac0f97b Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 14:52:48 +0100 Subject: [PATCH 06/36] CredentialsInPipelineTest: print logs of pipeline runs to test output --- .../credentials/CredentialsInPipelineTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 756ca7989..5d0c11a48 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -80,6 +80,7 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.recipes.LocalData; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -225,6 +226,12 @@ Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError return agentUsable; } + String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + f.getLogText().writeLogTo(0, baos); + return baos.toString(); + } + ///////////////////////////////////////////////////////////////// // Certificate credentials tests ///////////////////////////////////////////////////////////////// @@ -314,6 +321,7 @@ public void testCertKeyStoreReadableOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -338,6 +346,7 @@ public void testCertKeyStoreReadableOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -365,6 +374,7 @@ public void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -431,6 +441,7 @@ public void testCertWithCredentialsOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -456,6 +467,7 @@ public void testCertWithCredentialsOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -484,6 +496,7 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); From fda8836c7a50c6c51973089fc715cf2943ac77a8 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 14:59:11 +0100 Subject: [PATCH 07/36] CredentialsInPipelineTest: refactor alias passing to getKey() --- .../credentials/CredentialsInPipelineTest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 5d0c11a48..7fd4bff66 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -275,12 +275,13 @@ String cpsScriptCredentialTestImports() { ///////////////////////////////////////////////////////////////// String cpsScriptCredentialTest(String runnerTag) { - return cpsScriptCredentialTest("myCert", "password", runnerTag); + return cpsScriptCredentialTest("myCert", "password", "1", runnerTag); } - String cpsScriptCredentialTest(String id, String password, String runnerTag) { + String cpsScriptCredentialTest(String id, String password, String alias, String runnerTag) { return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + + "def alias='" + alias + "';\n" + "StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + " CredentialsProvider.lookupCredentials(\n" + " StandardCredentials.class,\n" + @@ -393,19 +394,21 @@ String cpsScriptCredentialTestGetKeyValue() { " def keystore = KeyStore.getInstance(keystoreFormat)\n" + " keystore.load(p12file, keyPassword.toCharArray())\n" + " p12file.close()\n" + - " def key = keystore.getKey(alias ? alias : \"1\", keyPassword.toCharArray())\n" + + " def key = keystore.getKey(alias, keyPassword.toCharArray())\n" + " return key.getEncoded().encodeBase64().toString()\n" + "}\n" + "\n"; } String cpsScriptCredentialTestWithCredentials(String runnerTag) { - return cpsScriptCredentialTestWithCredentials("myCert", "password", runnerTag); + return cpsScriptCredentialTestWithCredentials("myCert", "password", "1", runnerTag); } - String cpsScriptCredentialTestWithCredentials(String id, String password, String runnerTag) { + String cpsScriptCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { + // Note: for some reason does not pass (env?.)myKeyAlias to closure return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + + "def alias='" + alias + "';\n" + "echo \"WITH-CREDENTIALS ON " + runnerTag + ":\"\n" + "withCredentials([certificate(\n" + " credentialsId: authentication,\n" + @@ -416,7 +419,7 @@ String cpsScriptCredentialTestWithCredentials(String id, String password, String " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + " echo \"Got expected password? ${keyPassword == password}\"\n" + " def keystoreFormat = \"PKCS12\"\n" + - " def keyValue = getKeyValue(keystoreName, keystoreFormat, keyPassword, env?.myKeyAlias)\n" + + " def keyValue = getKeyValue(keystoreName, keystoreFormat, keyPassword, (env?.myKeyAlias ? env?.myKeyAlias : alias))\n" + " println \"-----BEGIN PRIVATE KEY-----\"\n" + " println keyValue\n" + " println \"-----END PRIVATE KEY-----\"\n" + From f45cdbe7f059d169466082c09746d0c1601691ab Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 15:04:59 +0100 Subject: [PATCH 08/36] CredentialsInPipelineTest: report private key in testCertKeyStoreReadable*() to make sure it is right --- .../plugins/credentials/CredentialsInPipelineTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 7fd4bff66..2920d17c7 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -296,6 +296,10 @@ String cpsScriptCredentialTest(String id, String password, String alias, String "echo \"KSS: \" + kss.toString()\n" + "byte[] kssb = kss.getKeyStoreBytes();\n" + "echo \"KSS bytes (len): \" + kssb.length\n" + + "String keyValue = keyStore.getKey(alias, password.toCharArray()).getEncoded().encodeBase64().toString()\n" + + "echo \"-----BEGIN PRIVATE KEY-----\"\n" + + "echo keyValue\n" + + "echo \"-----END PRIVATE KEY-----\"\n" + "\n" + "echo \"CRED-SNAP ON " + runnerTag + ":\"\n" + "echo credentialSnap.toString()\n" + @@ -304,6 +308,10 @@ String cpsScriptCredentialTest(String id, String password, String alias, String "echo \"KSS-SNAP: \" + kssSnap.toString()\n" + "byte[] kssbSnap = kssSnap.getKeyStoreBytes();\n" + "echo \"KSS-SNAP bytes (len): \" + kssbSnap.length\n" + + "String keyValueSnap = keyStoreSnap.getKey(alias, password.toCharArray()).getEncoded().encodeBase64().toString()\n" + + "echo \"-----BEGIN PRIVATE KEY-----\"\n" + + "echo keyValueSnap\n" + + "echo \"-----END PRIVATE KEY-----\"\n" + "\n"; } From b5e0ec8473af8a65c3d91b129132680777ef5615 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 15:05:13 +0100 Subject: [PATCH 09/36] CredentialsInPipelineTest: reword message so it is not obfuscated --- .../plugins/credentials/CredentialsInPipelineTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 2920d17c7..2d3dc5c42 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -425,7 +425,7 @@ String cpsScriptCredentialTestWithCredentials(String id, String password, String " aliasVariable: 'myKeyAlias')\n" + "]) {\n" + " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\n" + - " echo \"Got expected password? ${keyPassword == password}\"\n" + + " echo \"Got expected key pass? ${keyPassword == password}\"\n" + " def keystoreFormat = \"PKCS12\"\n" + " def keyValue = getKeyValue(keystoreName, keystoreFormat, keyPassword, (env?.myKeyAlias ? env?.myKeyAlias : alias))\n" + " println \"-----BEGIN PRIVATE KEY-----\"\n" + From 981355e720881c300f0a9de96be2e2530d321bb1 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 15:16:03 +0100 Subject: [PATCH 10/36] CredentialsInPipelineTest: rename "testCert" and "cpsScriptCertCredential" to differentiate from expected other credential types --- .../CredentialsInPipelineTest.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 2d3dc5c42..6444510a7 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -274,11 +274,11 @@ String cpsScriptCredentialTestImports() { // Certificate credentials retrievability in (trusted) pipeline ///////////////////////////////////////////////////////////////// - String cpsScriptCredentialTest(String runnerTag) { - return cpsScriptCredentialTest("myCert", "password", "1", runnerTag); + String cpsScriptCertCredentialTestScriptedPipeline(String runnerTag) { + return cpsScriptCertCredentialTestScriptedPipeline("myCert", "password", "1", runnerTag); } - String cpsScriptCredentialTest(String id, String password, String alias, String runnerTag) { + String cpsScriptCertCredentialTestScriptedPipeline(String id, String password, String alias, String runnerTag) { return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + "def alias='" + alias + "';\n" + @@ -325,7 +325,7 @@ public void testCertKeyStoreReadableOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTest("CONTROLLER BUILT-IN"); + cpsScriptCertCredentialTestScriptedPipeline("CONTROLLER BUILT-IN"); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -349,7 +349,7 @@ public void testCertKeyStoreReadableOnNodeLocal() throws Exception { WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + "node {\n" + - cpsScriptCredentialTest("CONTROLLER NODE") + + cpsScriptCertCredentialTestScriptedPipeline("CONTROLLER NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -377,7 +377,7 @@ public void testCertKeyStoreReadableOnNodeRemote() throws Exception { WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + "node(\"worker\") {\n" + - cpsScriptCredentialTest("REMOTE NODE") + + cpsScriptCertCredentialTestScriptedPipeline("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -395,7 +395,7 @@ public void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Certificate credentials retrievability by withCredentials() step ///////////////////////////////////////////////////////////////// - String cpsScriptCredentialTestGetKeyValue() { + String cpsScriptCertCredentialTestGetKeyValue() { return "@NonCPS\n" + "def getKeyValue(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + " def p12file = new FileInputStream(keystoreName)\n" + @@ -408,11 +408,11 @@ String cpsScriptCredentialTestGetKeyValue() { "\n"; } - String cpsScriptCredentialTestWithCredentials(String runnerTag) { - return cpsScriptCredentialTestWithCredentials("myCert", "password", "1", runnerTag); + String cpsScriptCertCredentialTestWithCredentials(String runnerTag) { + return cpsScriptCertCredentialTestWithCredentials("myCert", "password", "1", runnerTag); } - String cpsScriptCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { + String cpsScriptCertCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { // Note: for some reason does not pass (env?.)myKeyAlias to closure return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + @@ -446,8 +446,8 @@ public void testCertWithCredentialsOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTestGetKeyValue() + - cpsScriptCredentialTestWithCredentials("CONTROLLER BUILT-IN"); + cpsScriptCertCredentialTestGetKeyValue() + + cpsScriptCertCredentialTestWithCredentials("CONTROLLER BUILT-IN"); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -470,9 +470,9 @@ public void testCertWithCredentialsOnNodeLocal() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTestGetKeyValue() + + cpsScriptCertCredentialTestGetKeyValue() + "node {\n" + - cpsScriptCredentialTestWithCredentials("CONTROLLER NODE") + + cpsScriptCertCredentialTestWithCredentials("CONTROLLER NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -499,9 +499,9 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - cpsScriptCredentialTestGetKeyValue() + + cpsScriptCertCredentialTestGetKeyValue() + "node(\"worker\") {\n" + - cpsScriptCredentialTestWithCredentials("REMOTE NODE") + + cpsScriptCertCredentialTestWithCredentials("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); From 9d3cb56f013bef7c8840eedec3ccf1bbe2c8ae4a Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 15:49:05 +0100 Subject: [PATCH 11/36] CredentialsInPipelineTest: add testCertHttpRequest*() --- pom.xml | 7 ++ .../CredentialsInPipelineTest.java | 99 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/pom.xml b/pom.xml index 0ea2a9c2c..c91e85623 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,13 @@ 523.vd859a_4b_122e6 test + + org.jenkins-ci.plugins + http_request + + 1.16 + test + diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 6444510a7..8df2e005b 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -515,4 +515,103 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { r.assertLogContains("END PRIVATE KEY", run); } + ///////////////////////////////////////////////////////////////// + // Certificate credentials retrievability by http-request-plugin + ///////////////////////////////////////////////////////////////// + + String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { + return cpsScriptCredentialTestHttpRequest("myCert", runnerTag); + } + + String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + // Note: we accept any outcome (for the plugin, unresolved host is HTTP-404) + // but it may not crash making use of the credential + return "def authentication='" + id + "';\n" + + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n" + + " httpMode: 'GET',\n" + + " authentication: authentication,\n" + + " consoleLogResponseBody: true,\n" + + " contentType : 'APPLICATION_FORM',\n" + + " validResponseCodes: '100:599',\n" + + " quiet: false)\n" + + "println('HTTP Request Plugin Status: '+ response.getStatus())\n" + + "println('HTTP Request Plugin Response: '+ response.getContent())\n" + + "\n"; + } + + @Test + @Issue("JENKINS-70101") + public void testCertHttpRequestOnController() throws Exception { + // Check that credentials are usable with pipeline script + // running without a node{} + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptCertCredentialTestHttpRequest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + + @Test + @Issue("JENKINS-70101") + public void testCertHttpRequestOnNodeLocal() throws Exception { + // Check that credentials are usable with pipeline script + // running on a node{} (provided by the controller) + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node {\n" + + cpsScriptCertCredentialTestHttpRequest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + + @Test + @Issue("JENKINS-70101") + public void testCertHttpRequestOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node(\"worker\") {\n" + + cpsScriptCertCredentialTestHttpRequest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + } From 2b9fa6f5cfa57f93b4aae2f93fb56b3682f6129e Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 16:40:58 +0100 Subject: [PATCH 12/36] CredentialsInPipelineTest: add testUsernamePasswordHttpRequest*() --- .../CredentialsInPipelineTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 8df2e005b..2ec2cf142 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -37,6 +37,7 @@ import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.gargoylesoftware.htmlunit.FormEncodingType; import com.gargoylesoftware.htmlunit.HttpMethod; import com.gargoylesoftware.htmlunit.Page; @@ -614,4 +615,97 @@ public void testCertHttpRequestOnNodeRemote() throws Exception { r.assertLogContains("HTTP Request Plugin Response: ", run); } + ///////////////////////////////////////////////////////////////// + // User/pass credentials tests + ///////////////////////////////////////////////////////////////// + + // Partially from UsernamePasswordCredentialsImplTest setup() + private void prepareUsernamePassword() throws IOException { + UsernamePasswordCredentialsImpl credentials = + new UsernamePasswordCredentialsImpl(null, + "abc123", "Bob’s laptop", + "bob", "s3cr3t"); + SystemCredentialsProvider.getInstance().getCredentials().add(credentials); + SystemCredentialsProvider.getInstance().save(); + } + + String cpsScriptUsernamePasswordCredentialTestHttpRequest(String runnerTag) { + return cpsScriptCredentialTestHttpRequest("abc123", runnerTag); + } + + @Test + @Issue("JENKINS-70101") + public void testUsernamePasswordHttpRequestOnController() throws Exception { + // Check that credentials are usable with pipeline script + // running without a node{} + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = cpsScriptUsernamePasswordCredentialTestHttpRequest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + + @Test + @Issue("JENKINS-70101") + public void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { + // Check that credentials are usable with pipeline script + // running on a node{} (provided by the controller) + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node {\n" + + cpsScriptUsernamePasswordCredentialTestHttpRequest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + + @Test + @Issue("JENKINS-70101") + public void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node(\"worker\") {\n" + + cpsScriptUsernamePasswordCredentialTestHttpRequest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + r.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + r.assertLogContains("HTTP Request Plugin Response: ", run); + } + } From f2b859f83a585285d06e065fb33eee2de77fc1bb Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Sun, 20 Nov 2022 16:56:23 +0100 Subject: [PATCH 13/36] CredentialsInPipelineTest: comment about alias for withCredentials(certificate) --- .../plugins/credentials/CredentialsInPipelineTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 2ec2cf142..809cc4dad 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -414,7 +414,9 @@ String cpsScriptCertCredentialTestWithCredentials(String runnerTag) { } String cpsScriptCertCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { - // Note: for some reason does not pass (env?.)myKeyAlias to closure + // Note: does not pass a(ny) useful (env?.)myKeyAlias to closure + // https://issues.jenkins.io/browse/JENKINS-59331 + // https://github.com/jenkinsci/credentials-binding-plugin/blob/fcd22059ac48b87d0924ef17d5b351a3b7a89a97/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/CertificateMultiBinding.java#L80-L81 return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + "def alias='" + alias + "';\n" + From b3ae79cbb8ba7be8c596b69a4282139886d6bb41 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 09:39:12 +0100 Subject: [PATCH 14/36] CredentialsInPipelineTest: trace SecretBytes "directly" and via httpRequest() in the same pipeline (using same bytes) --- .../CredentialsInPipelineTest.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 809cc4dad..eff13e087 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -523,13 +523,39 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { ///////////////////////////////////////////////////////////////// String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { - return cpsScriptCredentialTestHttpRequest("myCert", runnerTag); + return cpsScriptCredentialTestHttpRequest("myCert", runnerTag, true); } - String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean withLocalCertLookup) { // Note: we accept any outcome (for the plugin, unresolved host is HTTP-404) // but it may not crash making use of the credential + // Note: cases withLocalCertLookup also need cpsScriptCredentialTestImports() return "def authentication='" + id + "';\n" + + "\n" + + "def msg\n" + + (withLocalCertLookup ? ( + "if (true) { // scoping\n" + + " msg = \"Finding credential...\"\n" + + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + + " CredentialsProvider.lookupCredentials(\n" + + " StandardCredentials.class,\n" + + " Jenkins.instance, null, null),\n" + + " CredentialsMatchers.withId(authentication));\n" + + " msg = \"Getting keystore...\"\n" + + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " KeyStore keyStore = credential.getKeyStore();\n" + + " msg = \"Getting keystore source...\"\n" + + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + + " msg = \"Getting keystore source bytes...\"\n" + + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " byte[] kssb = kss.getKeyStoreBytes();\n" + + "}\n" ) + : "" ) + + "\n" + + "msg = \"Querying HTTPS with cert...\"\n" + + "echo msg; System.out.println(msg); System.err.println(msg);\n" + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n" + " httpMode: 'GET',\n" + " authentication: authentication,\n" @@ -551,7 +577,8 @@ public void testCertHttpRequestOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = cpsScriptCertCredentialTestHttpRequest("CONTROLLER BUILT-IN"); + String script = cpsScriptCredentialTestImports() + + cpsScriptCertCredentialTestHttpRequest("CONTROLLER BUILT-IN"); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -573,7 +600,7 @@ public void testCertHttpRequestOnNodeLocal() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = + String script = cpsScriptCredentialTestImports() + "node {\n" + cpsScriptCertCredentialTestHttpRequest("CONTROLLER NODE") + "}\n"; @@ -601,7 +628,7 @@ public void testCertHttpRequestOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); - String script = + String script = cpsScriptCredentialTestImports() + "node(\"worker\") {\n" + cpsScriptCertCredentialTestHttpRequest("REMOTE NODE") + "}\n"; @@ -632,7 +659,7 @@ private void prepareUsernamePassword() throws IOException { } String cpsScriptUsernamePasswordCredentialTestHttpRequest(String runnerTag) { - return cpsScriptCredentialTestHttpRequest("abc123", runnerTag); + return cpsScriptCredentialTestHttpRequest("abc123", runnerTag, false); } @Test From 4a1d34e14a94994142f099185b1712e19661f248 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 11:31:32 +0100 Subject: [PATCH 15/36] CertificateCredentialsImpl: UploadedKeyStoreSource: update comment for getUploadedKeystore() --- .../plugins/credentials/impl/CertificateCredentialsImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index a92d84f48..306df4c4b 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -490,9 +490,9 @@ private Object readResolve() throws ObjectStreamException { } /** - * Returns the private key file name. + * Returns the private key + certificate file bytes. * - * @return the private key file name. + * @return the private key + certificate file bytes. */ public SecretBytes getUploadedKeystore() { return uploadedKeystoreBytes; From f6731fb824772c9ca8975f303fd8b2b03fdb25cf Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 10:57:45 +0100 Subject: [PATCH 16/36] Revive CredentialsSnapshotTaker class --- pom.xml | 4 +- .../impl/CertificateCredentialsImpl.java | 17 ++++ .../CertificateCredentialsSnapshotTaker.java | 95 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java diff --git a/pom.xml b/pom.xml index c91e85623..8d4beaf91 100644 --- a/pom.xml +++ b/pom.xml @@ -182,7 +182,9 @@ org.jenkins-ci.plugins http_request - + 1.16 test diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 306df4c4b..50b5f3eac 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -23,6 +23,7 @@ */ package com.cloudbees.plugins.credentials.impl; +import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SecretBytes; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; @@ -34,6 +35,7 @@ import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Items; +import hudson.remoting.Channel; import hudson.util.FormValidation; import hudson.util.IOUtils; import hudson.util.Secret; @@ -138,6 +140,21 @@ private static char[] toCharArray(@NonNull Secret password) { return plainText == null ? null : plainText.toCharArray(); } + /** + * When serializing over a {@link Channel} ensure that we send a self-contained version. + * + * @return the object instance to write to the stream. + */ + private Object writeReplace() { + if (/* XStream */ Channel.current() == null + || /* already safe to serialize */ keyStoreSource + .isSnapshotSource() + ) { + return this; + } + return CredentialsProvider.snapshot(this); + } + /** * Returns the {@link KeyStore} containing the certificate. * diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java new file mode 100644 index 000000000..dedc8dc44 --- /dev/null +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java @@ -0,0 +1,95 @@ +/* + * The MIT License + * + * Copyright (c) 2011-2016, CloudBees, Inc., Stephen Connolly. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +package com.cloudbees.plugins.credentials.impl; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import com.cloudbees.plugins.credentials.SecretBytes; +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import hudson.Extension; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; + +/** + * The {@link CredentialsSnapshotTaker} for {@link StandardCertificateCredentials}. + * Taking a snapshot of the credential ensures that all the details are captured + * within the credential. + * + * @since 1.14 + * + * Historic note: This code was dropped from {@link CertificateCredentialsImpl} + * codebase along with most of FileOnMasterKeyStoreSource (deprecated and headed + * towards eventual deletion) due to SECURITY-1322, see more details at + * https://www.jenkins.io/security/advisory/2019-05-21/ + * In hind-sight, this snapshot taker was needed to let the + * {@link CertificateCredentialsImpl.UploadedKeyStoreSource} be used + * on remote agents. + */ +@Extension +public class CertificateCredentialsSnapshotTaker extends CredentialsSnapshotTaker { + + /** + * {@inheritDoc} + */ + @Override + public Class type() { + return StandardCertificateCredentials.class; + } + + /** + * {@inheritDoc} + */ + @Override + public StandardCertificateCredentials snapshot(StandardCertificateCredentials credentials) { + if (credentials instanceof CertificateCredentialsImpl) { + final CertificateCredentialsImpl.KeyStoreSource keyStoreSource = ((CertificateCredentialsImpl) credentials).getKeyStoreSource(); + if (keyStoreSource.isSnapshotSource()) { + return credentials; + } + return new CertificateCredentialsImpl(credentials.getScope(), credentials.getId(), + credentials.getDescription(), credentials.getPassword().getEncryptedValue(), + new CertificateCredentialsImpl.UploadedKeyStoreSource(SecretBytes.fromBytes(keyStoreSource.getKeyStoreBytes()))); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final char[] password = credentials.getPassword().getPlainText().toCharArray(); + try { + credentials.getKeyStore().store(bos, password); + bos.close(); + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + return credentials; // as-is + } finally { + Arrays.fill(password, (char) 0); + } + + return new CertificateCredentialsImpl(credentials.getScope(), credentials.getId(), + credentials.getDescription(), credentials.getPassword().getEncryptedValue(), + new CertificateCredentialsImpl.UploadedKeyStoreSource(SecretBytes.fromBytes(bos.toByteArray()))); + } +} From 9d32b42c28f5612dda6b747372f5eb4542eac29e Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 11:39:51 +0100 Subject: [PATCH 17/36] CertificateCredentialsImpl: UploadedKeyStoreSource: let "Secret uploadedKeystore" be used to serialize --- .../impl/CertificateCredentialsImpl.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 50b5f3eac..603579e2a 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -436,17 +436,18 @@ public static class UploadedKeyStoreSource extends KeyStoreSource implements Ser /** * The old uploaded keystore. + * Still used for snapshot taking, with contents independent of Jenkins instance and JVM. */ @CheckForNull @Deprecated - private transient Secret uploadedKeystore; + private Secret uploadedKeystore; /** * The uploaded keystore. * * @since 2.1.5 */ @CheckForNull - private final SecretBytes uploadedKeystoreBytes; + private SecretBytes uploadedKeystoreBytes; /** * Our constructor. @@ -474,6 +475,19 @@ public UploadedKeyStoreSource(@CheckForNull SecretBytes uploadedKeystore) { this.uploadedKeystoreBytes = uploadedKeystore; } + /** + * Our constructor for serialization (e.g. to remote agents, whose SecretBytes + * in another JVM use a different static KEY); would re-encode. + * + * @param uploadedKeystore the keystore content. + * @deprecated + */ + @SuppressWarnings("unused") // by stapler + @Deprecated + public UploadedKeyStoreSource(@CheckForNull Secret uploadedKeystore) { + this.uploadedKeystore = uploadedKeystore; + } + /** * Constructor able to receive file directly * @@ -492,6 +506,18 @@ public UploadedKeyStoreSource(FileItem uploadedCertFile, @CheckForNull SecretByt this.uploadedKeystoreBytes = uploadedKeystore; } + /** + * Request that if the less-efficient but more-portable Secret + * is involved (e.g. to cross the remoting gap to another JVM), + * we use the more secure and efficient SecretBytes. + */ + public void useSecretBytes() { + if (this.uploadedKeystore != null && this.uploadedKeystoreBytes == null) { + this.uploadedKeystoreBytes = SecretBytes.fromBytes(DescriptorImpl.toByteArray(this.uploadedKeystore)); + this.uploadedKeystore = null; + } + } + /** * Migrate to the new field. * @@ -512,6 +538,9 @@ private Object readResolve() throws ObjectStreamException { * @return the private key + certificate file bytes. */ public SecretBytes getUploadedKeystore() { + if (uploadedKeystore != null && uploadedKeystoreBytes == null) { + return SecretBytes.fromBytes(DescriptorImpl.toByteArray(uploadedKeystore)); + } return uploadedKeystoreBytes; } @@ -521,6 +550,9 @@ public SecretBytes getUploadedKeystore() { @NonNull @Override public byte[] getKeyStoreBytes() { + if (uploadedKeystore != null && uploadedKeystoreBytes == null) { + return DescriptorImpl.toByteArray(uploadedKeystore); + } return SecretBytes.getPlainData(uploadedKeystoreBytes); } From 3694a909b9c32d4515dce7f482d067a1e030a7ed Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 13:57:48 +0100 Subject: [PATCH 18/36] CertificateCredentialsSnapshotTaker: for snapshot to be self-contained, must use Secret not SecretBytes --- .../impl/CertificateCredentialsSnapshotTaker.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java index dedc8dc44..90697d7e5 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java @@ -29,6 +29,8 @@ import com.cloudbees.plugins.credentials.SecretBytes; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; import hudson.Extension; +import hudson.util.Secret; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.KeyStoreException; @@ -74,7 +76,7 @@ public StandardCertificateCredentials snapshot(StandardCertificateCredentials cr } return new CertificateCredentialsImpl(credentials.getScope(), credentials.getId(), credentials.getDescription(), credentials.getPassword().getEncryptedValue(), - new CertificateCredentialsImpl.UploadedKeyStoreSource(SecretBytes.fromBytes(keyStoreSource.getKeyStoreBytes()))); + new CertificateCredentialsImpl.UploadedKeyStoreSource(CertificateCredentialsImpl.UploadedKeyStoreSource.DescriptorImpl.toSecret(keyStoreSource.getKeyStoreBytes()))); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -90,6 +92,6 @@ public StandardCertificateCredentials snapshot(StandardCertificateCredentials cr return new CertificateCredentialsImpl(credentials.getScope(), credentials.getId(), credentials.getDescription(), credentials.getPassword().getEncryptedValue(), - new CertificateCredentialsImpl.UploadedKeyStoreSource(SecretBytes.fromBytes(bos.toByteArray()))); + new CertificateCredentialsImpl.UploadedKeyStoreSource(CertificateCredentialsImpl.UploadedKeyStoreSource.DescriptorImpl.toSecret(bos.toByteArray()))); } } From 90e5a218cb4b66af1a3a61294d34fb7b9cc3d8a6 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 13:47:40 +0100 Subject: [PATCH 19/36] CertificateCredentialsImpl: UploadedKeyStoreSource: make isSnapshotSource() depend on Channel.current() == null --- .../credentials/impl/CertificateCredentialsImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 603579e2a..ad9b9dbd6 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -569,7 +569,11 @@ public long getKeyStoreLastModified() { */ @Override public boolean isSnapshotSource() { - return true; + //return this.snapshotSecretBytes; + // If context is local, clone SecretBytes directly (only + // usable in same JVM). Otherwise use Secret for transport + // (see {@link CertificateCredentialsSnapshotTaker}. + return (/* XStream */ Channel.current() == null); } /** From 8e6831f4d1d2121ebde603eae706b9dbf596ae74 Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 15:18:09 +0100 Subject: [PATCH 20/36] CredentialsInPipelineTest: cleanup imports --- .../CredentialsInPipelineTest.java | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index eff13e087..937335b5e 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -24,49 +24,17 @@ package com.cloudbees.plugins.credentials; -import com.cloudbees.hudson.plugins.folder.Folder; -import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider; -import com.cloudbees.plugins.credentials.Credentials; -import com.cloudbees.plugins.credentials.CredentialsNameProvider; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsStore; -import com.cloudbees.plugins.credentials.SecretBytes; -import com.cloudbees.plugins.credentials.SystemCredentialsProvider; -import com.cloudbees.plugins.credentials.common.CertificateCredentials; -import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; -import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; -import com.gargoylesoftware.htmlunit.FormEncodingType; -import com.gargoylesoftware.htmlunit.HttpMethod; -import com.gargoylesoftware.htmlunit.Page; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.html.DomNode; -import com.gargoylesoftware.htmlunit.html.DomNodeList; -import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; -import com.gargoylesoftware.htmlunit.html.HtmlFileInput; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlOption; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput; -import hudson.FilePath; -import hudson.Util; -import hudson.cli.CLICommandInvoker; -import hudson.cli.UpdateJobCommand; import hudson.model.Descriptor; -import hudson.model.ItemGroup; -import hudson.model.Job; import hudson.model.Node; import hudson.model.Result; import hudson.model.Slave; -import hudson.security.ACL; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; import hudson.slaves.DumbSlave; import hudson.slaves.RetentionStrategy; -import hudson.util.Secret; -import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; @@ -79,25 +47,14 @@ import org.junit.rules.TemporaryFolder; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.recipes.LocalData; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.Base64; -import java.util.Collections; -import java.util.List; - -import static hudson.cli.CLICommandInvoker.Matcher.failedWith; -import static hudson.cli.CLICommandInvoker.Matcher.succeeded; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.*; + import static org.hamcrest.CoreMatchers.*; import static org.junit.Assume.assumeThat; From 3213f694ca1c106154305212a8e428231bf989dc Mon Sep 17 00:00:00 2001 From: Evgeny Klimov Date: Mon, 21 Nov 2022 15:19:30 +0100 Subject: [PATCH 21/36] CredentialsInPipelineTest: cleanup verbosePipelines --- .../CredentialsInPipelineTest.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 937335b5e..579318a7a 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -68,6 +68,11 @@ public class CredentialsInPipelineTest { * Initially tied to JENKINS-70101 research. */ + // For developers: set to `true` so that pipeline console logs show + // up in System.out (and/or System.err) of the plugin test run by + // mvn test -Dtest="CredentialsInPipelineTest" + private boolean verbosePipelines = false; + @Rule public JenkinsRule r = new JenkinsRule(); @@ -288,7 +293,7 @@ public void testCertKeyStoreReadableOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -313,7 +318,7 @@ public void testCertKeyStoreReadableOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -341,7 +346,7 @@ public void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -412,7 +417,7 @@ public void testCertWithCredentialsOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -438,7 +443,7 @@ public void testCertWithCredentialsOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -467,7 +472,7 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -493,26 +498,26 @@ String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean w + (withLocalCertLookup ? ( "if (true) { // scoping\n" + " msg = \"Finding credential...\"\n" - + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + " StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + " CredentialsProvider.lookupCredentials(\n" + " StandardCredentials.class,\n" + " Jenkins.instance, null, null),\n" + " CredentialsMatchers.withId(authentication));\n" + " msg = \"Getting keystore...\"\n" - + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + " KeyStore keyStore = credential.getKeyStore();\n" + " msg = \"Getting keystore source...\"\n" - + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + " KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + " msg = \"Getting keystore source bytes...\"\n" - + " echo msg; System.out.println(msg); System.err.println(msg);\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + " byte[] kssb = kss.getKeyStoreBytes();\n" + "}\n" ) : "" ) + "\n" + "msg = \"Querying HTTPS with cert...\"\n" - + "echo msg; System.out.println(msg); System.err.println(msg);\n" + + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n" + " httpMode: 'GET',\n" + " authentication: authentication,\n" @@ -540,7 +545,7 @@ public void testCertHttpRequestOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -565,7 +570,7 @@ public void testCertHttpRequestOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -593,7 +598,7 @@ public void testCertHttpRequestOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -633,7 +638,7 @@ public void testUsernamePasswordHttpRequestOnController() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -658,7 +663,7 @@ public void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); @@ -686,7 +691,7 @@ public void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { // Execute the build WorkflowRun run = proj.scheduleBuild2(0).get(); - System.out.println(getLogAsStringPlaintext(run)); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); // Check expectations r.assertBuildStatus(Result.SUCCESS, run); From 7053c114eaa725033cfd16c400ca7d32198f2466 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 31 Jan 2025 11:39:51 +0100 Subject: [PATCH 22/36] Update CredentialsInPipelineTest.java: declare hudson.model.Descriptor.FormException possible in a test --- .../plugins/credentials/CredentialsInPipelineTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 579318a7a..b1298bc67 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -28,6 +28,7 @@ import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Descriptor; +import hudson.model.Descriptor.FormException; import hudson.model.Node; import hudson.model.Result; import hudson.model.Slave; @@ -611,7 +612,7 @@ public void testCertHttpRequestOnNodeRemote() throws Exception { ///////////////////////////////////////////////////////////////// // Partially from UsernamePasswordCredentialsImplTest setup() - private void prepareUsernamePassword() throws IOException { + private void prepareUsernamePassword() throws IOException, FormException { UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(null, "abc123", "Bob’s laptop", From f787babec7bf854dde06d2e8a692a24e69279b57 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 4 Aug 2025 12:57:19 +0200 Subject: [PATCH 23/36] pom.xml: do not refer to specific versions of dependencies used for testing [JENKINS-70101] Signed-off-by: Jim Klimov --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index 0725fa937..9392bad7f 100644 --- a/pom.xml +++ b/pom.xml @@ -162,19 +162,16 @@ org.jenkins-ci.plugins job-dsl - 1.81 test org.jenkins-ci.plugins command-launcher - 1.6 test org.jenkins-ci.plugins credentials-binding - 523.vd859a_4b_122e6 test @@ -183,7 +180,6 @@ - 1.16 test From f063ec11d3e2353a9b6a81926f9f69ac2cd0ff43 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 13:51:15 +0100 Subject: [PATCH 24/36] CredentialsInPipelineTest: modernize to JUnit 5 Signed-off-by: Jim Klimov --- .../CredentialsInPipelineTest.java | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index b1298bc67..1f19c1039 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -41,13 +41,13 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import java.io.ByteArrayOutputStream; import java.io.File; @@ -56,9 +56,9 @@ import java.net.URL; import java.nio.file.Files; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assume.assumeThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +@WithJenkins public class CredentialsInPipelineTest { /** * The CredentialsInPipelineTest suite prepares pipeline scripts to @@ -74,14 +74,13 @@ public class CredentialsInPipelineTest { // mvn test -Dtest="CredentialsInPipelineTest" private boolean verbosePipelines = false; - @Rule - public JenkinsRule r = new JenkinsRule(); + private JenkinsRule r; // Data for build agent setup - @Rule - public TemporaryFolder tmpAgent = new TemporaryFolder(); - @Rule - public TemporaryFolder tmpWorker = new TemporaryFolder(); + @TempDir + private File tmpAgent; + @TempDir + private File tmpWorker; // Where did we save that file?.. private File agentJar = null; // Can this be reused for many test cases? @@ -90,16 +89,17 @@ public class CredentialsInPipelineTest { private Boolean agentUsable = null; // From CertificateCredentialImplTest - @Rule - public TemporaryFolder tmp = new TemporaryFolder(); + @TempDir + private File tmp; private File p12; - @Before - public void setup() { + @BeforeEach + void setup(JenkinsRule r) throws IOException { + this.r = r; r.jenkins.setCrumbIssuer(null); } - Boolean isAvailableAgent() { + private Boolean isAvailableAgent() { // Can be used to skip optional tests if we know we could not set up an agent if (agentJar == null) return false; @@ -108,7 +108,7 @@ Boolean isAvailableAgent() { return agentUsable; } - Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError { + private Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError, FormException { // Note we anticipate this might fail; it should not block the whole test suite from running // Loosely inspired by // https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/create-agent-node-from-groovy @@ -121,7 +121,7 @@ Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError if (agentJar == null) { try { URL url = new URL(r.jenkins.getRootUrl() + "jnlpJars/agent.jar"); - agentJar = tmpAgent.newFile("agent.jar"); + agentJar = new File(tmpAgent, "agent.jar"); FileOutputStream out = new FileOutputStream(agentJar); out.write(url.openStream().readAllBytes()); out.close(); @@ -140,14 +140,14 @@ Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError // (including spaces in directory names) and Unix/Linux ComputerLauncher launcher = new CommandLauncher( "\"" + System.getProperty("java.home") + File.separator + "bin" + - File.separator + "java\" -jar \"" + agentJar.getAbsolutePath().toString() + "\"" + File.separator + "java\" -jar \"" + agentJar.getAbsolutePath() + "\"" ); try { // Define a "Permanent Agent" agent = new DumbSlave( "worker", - tmpWorker.getRoot().getAbsolutePath().toString(), + tmpWorker.getAbsolutePath(), launcher); agent.setNodeDescription("Worker in another JVM, remoting used"); agent.setNumExecutors(1); @@ -190,7 +190,7 @@ Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError return agentUsable; } - String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException { + private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); f.getLogText().writeLogTo(0, baos); return baos.toString(); @@ -209,7 +209,7 @@ private void prepareUploadedKeystore(String id, String password) throws IOExcept if (p12 == null) { // Contains a private key + openvpn certs, // as alias named "1" (according to keytool) - p12 = tmp.newFile("test.p12"); + p12 = File.createTempFile("test.p12", null, tmp); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("test.p12"), p12); } @@ -220,7 +220,7 @@ private void prepareUploadedKeystore(String id, String password) throws IOExcept SystemCredentialsProvider.getInstance().save(); } - String cpsScriptCredentialTestImports() { + private String cpsScriptCredentialTestImports() { return "import com.cloudbees.plugins.credentials.CredentialsMatchers;\n" + "import com.cloudbees.plugins.credentials.CredentialsProvider;\n" + "import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;\n" + @@ -238,11 +238,11 @@ String cpsScriptCredentialTestImports() { // Certificate credentials retrievability in (trusted) pipeline ///////////////////////////////////////////////////////////////// - String cpsScriptCertCredentialTestScriptedPipeline(String runnerTag) { + private String cpsScriptCertCredentialTestScriptedPipeline(String runnerTag) { return cpsScriptCertCredentialTestScriptedPipeline("myCert", "password", "1", runnerTag); } - String cpsScriptCertCredentialTestScriptedPipeline(String id, String password, String alias, String runnerTag) { + private String cpsScriptCertCredentialTestScriptedPipeline(String id, String password, String alias, String runnerTag) { return "def authentication='" + id + "';\n" + "def password='" + password + "';\n" + "def alias='" + alias + "';\n" + @@ -281,7 +281,7 @@ String cpsScriptCertCredentialTestScriptedPipeline(String id, String password, S @Test @Issue("JENKINS-70101") - public void testCertKeyStoreReadableOnController() throws Exception { + void testCertKeyStoreReadableOnController() throws Exception { // Check that credentials are usable with pipeline script // running without a node{} prepareUploadedKeystore(); @@ -304,7 +304,7 @@ public void testCertKeyStoreReadableOnController() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertKeyStoreReadableOnNodeLocal() throws Exception { + void testCertKeyStoreReadableOnNodeLocal() throws Exception { // Check that credentials are usable with pipeline script // running on a node{} (provided by the controller) prepareUploadedKeystore(); @@ -329,11 +329,11 @@ public void testCertKeyStoreReadableOnNodeLocal() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertKeyStoreReadableOnNodeRemote() throws Exception { + void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Check that credentials are usable with pipeline script // running on a remote node{} with separate JVM (check // that remoting/snapshot work properly) - assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); prepareUploadedKeystore(); @@ -359,7 +359,7 @@ public void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Certificate credentials retrievability by withCredentials() step ///////////////////////////////////////////////////////////////// - String cpsScriptCertCredentialTestGetKeyValue() { + private String cpsScriptCertCredentialTestGetKeyValue() { return "@NonCPS\n" + "def getKeyValue(def keystoreName, def keystoreFormat, def keyPassword, def alias) {\n" + " def p12file = new FileInputStream(keystoreName)\n" + @@ -372,11 +372,11 @@ String cpsScriptCertCredentialTestGetKeyValue() { "\n"; } - String cpsScriptCertCredentialTestWithCredentials(String runnerTag) { + private String cpsScriptCertCredentialTestWithCredentials(String runnerTag) { return cpsScriptCertCredentialTestWithCredentials("myCert", "password", "1", runnerTag); } - String cpsScriptCertCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { + private String cpsScriptCertCredentialTestWithCredentials(String id, String password, String alias, String runnerTag) { // Note: does not pass a(ny) useful (env?.)myKeyAlias to closure // https://issues.jenkins.io/browse/JENKINS-59331 // https://github.com/jenkinsci/credentials-binding-plugin/blob/fcd22059ac48b87d0924ef17d5b351a3b7a89a97/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/CertificateMultiBinding.java#L80-L81 @@ -402,9 +402,9 @@ String cpsScriptCertCredentialTestWithCredentials(String id, String password, St } @Test - @Ignore("Work with keystore file requires a node") + @Disabled("Work with keystore file requires a node") @Issue("JENKINS-70101") - public void testCertWithCredentialsOnController() throws Exception { + void testCertWithCredentialsOnController() throws Exception { // Check that credentials are usable with pipeline script // running without a node{} prepareUploadedKeystore(); @@ -428,7 +428,7 @@ public void testCertWithCredentialsOnController() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertWithCredentialsOnNodeLocal() throws Exception { + void testCertWithCredentialsOnNodeLocal() throws Exception { // Check that credentials are usable with pipeline script // running on a node{} (provided by the controller) prepareUploadedKeystore(); @@ -454,11 +454,11 @@ public void testCertWithCredentialsOnNodeLocal() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertWithCredentialsOnNodeRemote() throws Exception { + void testCertWithCredentialsOnNodeRemote() throws Exception { // Check that credentials are usable with pipeline script // running on a remote node{} with separate JVM (check // that remoting/snapshot work properly) - assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); prepareUploadedKeystore(); @@ -485,11 +485,11 @@ public void testCertWithCredentialsOnNodeRemote() throws Exception { // Certificate credentials retrievability by http-request-plugin ///////////////////////////////////////////////////////////////// - String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { + private String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { return cpsScriptCredentialTestHttpRequest("myCert", runnerTag, true); } - String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean withLocalCertLookup) { + private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean withLocalCertLookup) { // Note: we accept any outcome (for the plugin, unresolved host is HTTP-404) // but it may not crash making use of the credential // Note: cases withLocalCertLookup also need cpsScriptCredentialTestImports() @@ -533,7 +533,7 @@ String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean w @Test @Issue("JENKINS-70101") - public void testCertHttpRequestOnController() throws Exception { + void testCertHttpRequestOnController() throws Exception { // Check that credentials are usable with pipeline script // running without a node{} prepareUploadedKeystore(); @@ -556,7 +556,7 @@ public void testCertHttpRequestOnController() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertHttpRequestOnNodeLocal() throws Exception { + void testCertHttpRequestOnNodeLocal() throws Exception { // Check that credentials are usable with pipeline script // running on a node{} (provided by the controller) prepareUploadedKeystore(); @@ -581,11 +581,11 @@ public void testCertHttpRequestOnNodeLocal() throws Exception { @Test @Issue("JENKINS-70101") - public void testCertHttpRequestOnNodeRemote() throws Exception { + void testCertHttpRequestOnNodeRemote() throws Exception { // Check that credentials are usable with pipeline script // running on a remote node{} with separate JVM (check // that remoting/snapshot work properly) - assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); prepareUploadedKeystore(); @@ -621,13 +621,13 @@ private void prepareUsernamePassword() throws IOException, FormException { SystemCredentialsProvider.getInstance().save(); } - String cpsScriptUsernamePasswordCredentialTestHttpRequest(String runnerTag) { + private String cpsScriptUsernamePasswordCredentialTestHttpRequest(String runnerTag) { return cpsScriptCredentialTestHttpRequest("abc123", runnerTag, false); } @Test @Issue("JENKINS-70101") - public void testUsernamePasswordHttpRequestOnController() throws Exception { + void testUsernamePasswordHttpRequestOnController() throws Exception { // Check that credentials are usable with pipeline script // running without a node{} prepareUsernamePassword(); @@ -649,7 +649,7 @@ public void testUsernamePasswordHttpRequestOnController() throws Exception { @Test @Issue("JENKINS-70101") - public void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { + void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { // Check that credentials are usable with pipeline script // running on a node{} (provided by the controller) prepareUsernamePassword(); @@ -674,11 +674,11 @@ public void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { @Test @Issue("JENKINS-70101") - public void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { + void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { // Check that credentials are usable with pipeline script // running on a remote node{} with separate JVM (check // that remoting/snapshot work properly) - assumeThat("This test needs a separate build agent", this.setupAgent(), is(true)); + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); prepareUsernamePassword(); From 0a1024c0e4051d996898669a3305a2e64143b9f4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 15:00:06 +0100 Subject: [PATCH 25/36] CredentialsInPipelineTest: address a few issues from review comments about the agent setup Signed-off-by: Jim Klimov --- .../plugins/credentials/CredentialsInPipelineTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 1f19c1039..cf1edfceb 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -108,6 +108,11 @@ private Boolean isAvailableAgent() { return agentUsable; } + /** FIXME: Refactor in favor of JenkinsRule.createOnlineSlave - per + * https://github.com/jenkinsci/credentials-plugin/pull/391#discussion_r1049548368 + * this method is more of a historical accident than a clever hack + * that solves something uniquely. + */ private Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError, FormException { // Note we anticipate this might fail; it should not block the whole test suite from running // Loosely inspired by @@ -140,7 +145,7 @@ private Boolean setupAgent() throws IOException, InterruptedException, OutOfMemo // (including spaces in directory names) and Unix/Linux ComputerLauncher launcher = new CommandLauncher( "\"" + System.getProperty("java.home") + File.separator + "bin" + - File.separator + "java\" -jar \"" + agentJar.getAbsolutePath() + "\"" + File.separator + "java\" -Xmx1024m -jar \"" + agentJar.getAbsolutePath() + "\"" ); try { From 2af1914228e637715a15e6fac646fc19d5ca1fbc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 18:00:17 +0100 Subject: [PATCH 26/36] SecretBytes: complete the javadoc comment about the class Signed-off-by: Jim Klimov --- .../java/com/cloudbees/plugins/credentials/SecretBytes.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java index d687c15ed..bc7fa315d 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java +++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java @@ -52,7 +52,9 @@ * salt and padding so no two invocations of {@link #getEncryptedData()} will return the same result, but all will * decrypt to the same {@link #getPlainData()}. XStream serialization and Stapler form-binding will assume that * the {@link #toString()} representation is used (i.e. the Base64 encoded secret bytes wrapped with { - * and }. If the string representation fails to decrypt (and is not wrapped + * and }). If the string representation fails to decrypt (and is not wrapped) the {@link #fromString} + * method returns a {@link SecretBytes} representation of an empty array, and {@link #isSecretBytes} method + * returns {@code false}. * * @since 2.1.5 */ From 6e808d11691e110cedf95beb21c8162e3f6ed886 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 18:01:06 +0100 Subject: [PATCH 27/36] SecretBytes: revise the javadoc comments about the class based on PR #391 discussion Specifically the https://github.com/jenkinsci/credentials-plugin/pull/391#issuecomment-1341628264 note that the original comment about `values[]` seems wrong, and comments before that. Signed-off-by: Jim Klimov --- .../com/cloudbees/plugins/credentials/SecretBytes.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java index bc7fa315d..9463fc6ac 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java +++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java @@ -72,7 +72,10 @@ public class SecretBytes implements Serializable { */ private static final long serialVersionUID = 1L; /** - * The key that encrypts the data on disk. + * The key that encrypts the data on disk.
+ * + * NOTE: Unique per JVM run-time, so different value on a + * Jenkins controller and remote agents or across restarts! */ private static final CredentialsConfidentialKey KEY = new CredentialsConfidentialKey(SecretBytes.class, "KEY"); /** @@ -80,7 +83,7 @@ public class SecretBytes implements Serializable { */ private static final Logger LOGGER = Logger.getLogger(SecretBytes.class.getName()); /** - * The unencrypted bytes. + * The encrypted bytes. */ @NonNull private final byte[] value; From c3d693f0c1be763321392e0409ff93d1546cad84 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 18:44:19 +0100 Subject: [PATCH 28/36] CredentialsInPipelineTest: refactor to use Jenkins.createOnlineSlave() instead of custom code [JENKINS-70101] Signed-off-by: Jim Klimov --- .../CredentialsInPipelineTest.java | 105 +++++------------- 1 file changed, 30 insertions(+), 75 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index cf1edfceb..fdb1c4a43 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -29,12 +29,10 @@ import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; +import hudson.model.Label; import hudson.model.Node; import hudson.model.Result; import hudson.model.Slave; -import hudson.slaves.CommandLauncher; -import hudson.slaves.ComputerLauncher; -import hudson.slaves.DumbSlave; import hudson.slaves.RetentionStrategy; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; @@ -51,41 +49,38 @@ import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.net.URL; import java.nio.file.Files; import static org.junit.jupiter.api.Assumptions.assumeTrue; +/** + * The CredentialsInPipelineTest suite prepares pipeline scripts to + * retrieve some previously saved credentials, on the controller, + * on a node provided by it, and on a worker agent in separate JVM. + * This picks known-working test cases and their setup from other + * test classes which address those credential types in more detail. + * Initially tied to JENKINS-70101 research. + */ @WithJenkins public class CredentialsInPipelineTest { - /** - * The CredentialsInPipelineTest suite prepares pipeline scripts to - * retrieve some previously saved credentials, on the controller, - * on a node provided by it, and on a worker agent in separate JVM. - * This picks known-working test cases and their setup from other - * test classes which address those credential types in more detail. - * Initially tied to JENKINS-70101 research. + /** For developers: set to `true` so that pipeline console logs show + * up in System.out (and/or System.err) of the plugin test run by + *
+     *   mvn test -Dtest="CredentialsInPipelineTest"
+     * 
*/ - - // For developers: set to `true` so that pipeline console logs show - // up in System.out (and/or System.err) of the plugin test run by - // mvn test -Dtest="CredentialsInPipelineTest" private boolean verbosePipelines = false; private JenkinsRule r; // Data for build agent setup - @TempDir - private File tmpAgent; - @TempDir - private File tmpWorker; - // Where did we save that file?.. - private File agentJar = null; + /** Build agent label expected by test cases for remote logic execution + * and data transfer */ + private final static String agentLabelString = "cred-test-worker"; // Can this be reused for many test cases? private Slave agent = null; - // Unknown/started/not usable + /** Tri-state Unknown/started/not usable */ private Boolean agentUsable = null; // From CertificateCredentialImplTest @@ -101,62 +96,24 @@ void setup(JenkinsRule r) throws IOException { private Boolean isAvailableAgent() { // Can be used to skip optional tests if we know we could not set up an agent - if (agentJar == null) - return false; if (agent == null) return false; return agentUsable; } - /** FIXME: Refactor in favor of JenkinsRule.createOnlineSlave - per - * https://github.com/jenkinsci/credentials-plugin/pull/391#discussion_r1049548368 - * this method is more of a historical accident than a clever hack - * that solves something uniquely. - */ - private Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError, FormException { - // Note we anticipate this might fail; it should not block the whole test suite from running - // Loosely inspired by - // https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/create-agent-node-from-groovy - - // Is it known-impossible to start the agent? - if (agentUsable != null && agentUsable == false) - return agentUsable; // quickly for re-runs - - // Did we download this file for earlier test cases? - if (agentJar == null) { - try { - URL url = new URL(r.jenkins.getRootUrl() + "jnlpJars/agent.jar"); - agentJar = new File(tmpAgent, "agent.jar"); - FileOutputStream out = new FileOutputStream(agentJar); - out.write(url.openStream().readAllBytes()); - out.close(); - } catch (IOException | OutOfMemoryError e) { - agentJar = null; - agentUsable = false; - - System.out.println("Failed to download agent.jar from test instance: " + - e.toString()); - - return agentUsable; - } - } - - // This CLI spelling and quoting should play well with both Windows - // (including spaces in directory names) and Unix/Linux - ComputerLauncher launcher = new CommandLauncher( - "\"" + System.getProperty("java.home") + File.separator + "bin" + - File.separator + "java\" -Xmx1024m -jar \"" + agentJar.getAbsolutePath() + "\"" - ); + private Boolean setupAgent() throws OutOfMemoryError, Exception { + if (isAvailableAgent()) + return true; + // Note we anticipate this might fail e.g. due to system resources; + // it should not block the whole test suite from running + // (we would just dynamically skip certain test cases) try { // Define a "Permanent Agent" - agent = new DumbSlave( - "worker", - tmpWorker.getAbsolutePath(), - launcher); + Label agentLabel = Label.get(agentLabelString); + agent = r.createOnlineSlave(agentLabel); agent.setNodeDescription("Worker in another JVM, remoting used"); agent.setNumExecutors(1); - agent.setLabelString("worker"); agent.setMode(Node.Mode.EXCLUSIVE); agent.setRetentionStrategy(new RetentionStrategy.Always()); @@ -169,8 +126,6 @@ private Boolean setupAgent() throws IOException, InterruptedException, OutOfMemo agent.getNodeProperties().add(envPro); */ - r.jenkins.addNode(agent); - String agentLog = null; agentUsable = false; for (long i = 0; i < 5; i++) { @@ -345,7 +300,7 @@ void testCertKeyStoreReadableOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - "node(\"worker\") {\n" + + "node(\"" + agentLabelString + "\") {\n" + cpsScriptCertCredentialTestScriptedPipeline("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -471,7 +426,7 @@ void testCertWithCredentialsOnNodeRemote() throws Exception { WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + cpsScriptCertCredentialTestGetKeyValue() + - "node(\"worker\") {\n" + + "node(\"" + agentLabelString + "\") {\n" + cpsScriptCertCredentialTestWithCredentials("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -597,7 +552,7 @@ void testCertHttpRequestOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = cpsScriptCredentialTestImports() + - "node(\"worker\") {\n" + + "node(\"" + agentLabelString + "\") {\n" + cpsScriptCertCredentialTestHttpRequest("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -690,7 +645,7 @@ void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { // Configure the build to use the credential WorkflowJob proj = r.jenkins.createProject(WorkflowJob.class, "proj"); String script = - "node(\"worker\") {\n" + + "node(\"" + agentLabelString + "\") {\n" + cpsScriptUsernamePasswordCredentialTestHttpRequest("REMOTE NODE") + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); From 648e77d5c971b5005f340334802dfe1bb1426975 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 18:45:06 +0100 Subject: [PATCH 29/36] CertificateCredentialsImplTest: import useCertificateCredentialsImplOnAgent() proposed in PR #391 discussion Rearranged from commit 4ee16f3393421e6dae85d42445e56beff8ff9541 posted in https://github.com/dwnusbaum/credentials-plugin/tree/JENKINS-70101 (initially as a simpler bug repro case than CredentialsInPipelineTest). Co-authored-by: Devin Nusbaum Signed-off-by: Jim Klimov --- .../impl/CertificateCredentialsImplTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 658fa3711..f48b574d0 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -26,6 +26,7 @@ import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SecretBytes; import com.cloudbees.plugins.credentials.common.CertificateCredentials; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; @@ -45,6 +46,7 @@ import org.htmlunit.html.HtmlRadioButtonInput; import hudson.Util; +import hudson.model.Node; import hudson.security.ACL; import hudson.util.Secret; import org.apache.commons.io.FileUtils; @@ -66,6 +68,8 @@ import java.util.Base64; import java.util.List; +import jenkins.security.MasterToSlaveCallable; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -310,6 +314,33 @@ void fullSubmitOfUploadedPEM() throws Exception { assertEquals(EXPECTED_DISPLAY_NAME_PEM, displayName); } + /** Helper for {@link #useCertificateCredentialsImplOnAgent} test case */ + private static class ReadCertificateCredentialsOnAgent extends MasterToSlaveCallable { + private final CertificateCredentialsImpl credentials; + + public ReadCertificateCredentialsOnAgent(CertificateCredentialsImpl credentials) { + this.credentials = credentials; + } + + @Override + public String call() throws Throwable { + KeyStore keyStore = credentials.getKeyStore(); + // KeyStore is not Serializable, so we just return the DN. + return StandardCertificateCredentials.NameProvider.getSubjectDN(keyStore); + } + } + + @Test + @Issue("JENKINS-70101") + public void useCertificateCredentialsImplOnAgent() throws Throwable { + SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); + CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore); + CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "my-credentials", "description", VALID_PASSWORD, storeSource); + + Node node = r.createOnlineSlave(); + assertEquals(EXPECTED_DISPLAY_NAME, node.getChannel().call(new ReadCertificateCredentialsOnAgent(credentials))); + } + private String getValidP12_base64() throws Exception { return Base64.getEncoder().encodeToString(Files.readAllBytes(p12.toPath())); } From 571bba6c9a927874560277812fac8aaae4637fb4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 2 Jan 2026 19:17:36 +0100 Subject: [PATCH 30/36] CredentialsInPipelineTest: setupAgent(): do not require to retain the agent after test case [JENKINS-70101] Signed-off-by: Jim Klimov --- .../plugins/credentials/CredentialsInPipelineTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index fdb1c4a43..f9eb7b056 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -115,7 +115,7 @@ private Boolean setupAgent() throws OutOfMemoryError, Exception { agent.setNodeDescription("Worker in another JVM, remoting used"); agent.setNumExecutors(1); agent.setMode(Node.Mode.EXCLUSIVE); - agent.setRetentionStrategy(new RetentionStrategy.Always()); + ///agent.setRetentionStrategy(new RetentionStrategy.Always()); /* // Add node envvars From 76657e8a29d9631aa89dfd6a4289a4813a4e24c5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 3 Jan 2026 14:30:13 +0100 Subject: [PATCH 31/36] CertificateCredentialsImplTest: separately test useCertificateCredentialsImplOnRemoteAgent() vs useCertificateCredentialsImplOnBuiltinAgent() Signed-off-by: Jim Klimov --- .../impl/CertificateCredentialsImplTest.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index f48b574d0..c48fb5a38 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -314,7 +314,8 @@ void fullSubmitOfUploadedPEM() throws Exception { assertEquals(EXPECTED_DISPLAY_NAME_PEM, displayName); } - /** Helper for {@link #useCertificateCredentialsImplOnAgent} test case */ + /** Helper for {@link #useCertificateCredentialsImplOnBuiltinAgent} + * and {@link #useCertificateCredentialsImplOnRemoteAgent} test cases */ private static class ReadCertificateCredentialsOnAgent extends MasterToSlaveCallable { private final CertificateCredentialsImpl credentials; @@ -332,11 +333,23 @@ public String call() throws Throwable { @Test @Issue("JENKINS-70101") - public void useCertificateCredentialsImplOnAgent() throws Throwable { + public void useCertificateCredentialsImplOnBuiltinAgent() throws Throwable { SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore); CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "my-credentials", "description", VALID_PASSWORD, storeSource); + // There should be no trouble without transfer to another JVM: + assertEquals(EXPECTED_DISPLAY_NAME, r.jenkins.getChannel().call(new ReadCertificateCredentialsOnAgent(credentials))); + } + + @Test + @Issue("JENKINS-70101") + public void useCertificateCredentialsImplOnRemoteAgent() throws Throwable { + SecretBytes uploadedKeystore = SecretBytes.fromBytes(Files.readAllBytes(p12.toPath())); + CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore); + CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "my-credentials", "description", VALID_PASSWORD, storeSource); + + // Check for trouble with transfer to another JVM (should be fixed by a solution to JENKINS-70101): Node node = r.createOnlineSlave(); assertEquals(EXPECTED_DISPLAY_NAME, node.getChannel().call(new ReadCertificateCredentialsOnAgent(credentials))); } From 87fbb66ec95be406e5f9a2f8d883c9dd8d9729ff Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 3 Jan 2026 14:47:52 +0100 Subject: [PATCH 32/36] CredentialsInPipelineTest, CertificateCredentialsImplTest: use prettier file names for test keystores Note: `createTempFile()` deals with patterns like shell `mktemp`, not with exact file names. Signed-off-by: Jim Klimov --- .../plugins/credentials/CredentialsInPipelineTest.java | 2 +- .../credentials/impl/CertificateCredentialsImplTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index f9eb7b056..4f92ea6a3 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -169,7 +169,7 @@ private void prepareUploadedKeystore(String id, String password) throws IOExcept if (p12 == null) { // Contains a private key + openvpn certs, // as alias named "1" (according to keytool) - p12 = File.createTempFile("test.p12", null, tmp); + p12 = File.createTempFile("test-keystore-", ".p12", tmp); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("test.p12"), p12); } diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index c48fb5a38..f06b4c47a 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -97,9 +97,9 @@ public class CertificateCredentialsImplTest { @BeforeEach void setup(JenkinsRule r) throws IOException { this.r = r; - p12 = File.createTempFile("test.p12", null, tmp); + p12 = File.createTempFile("test-keystore-", ".p12", tmp); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("test.p12"), p12); - p12Invalid = File.createTempFile("invalid.p12", null, tmp); + p12Invalid = File.createTempFile("invalid-keystore-", ".p12", tmp); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("invalid.p12"), p12Invalid); pemCert = IOUtils.toString(CertificateCredentialsImplTest.class.getResource("certs.pem"), StandardCharsets.UTF_8); From de6854d6fec2cf50ec604d844cd80cf4f51e62d3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 4 Jan 2026 21:18:24 +0100 Subject: [PATCH 33/36] CertificateCredentialsImpl: fix comment Signed-off-by: Jim Klimov --- .../plugins/credentials/impl/CertificateCredentialsImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 58e215148..51bc66b16 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -539,7 +539,7 @@ public KeyStore toKeyStore(char[] password) throws NoSuchAlgorithmException, Cer throw new IllegalStateException(className + " is not FIPS compliant and can not be used when Jenkins is in FIPS mode. " + "An issue should be filed against the plugin " + pluginName + " to ensure it is adapted to be able to work in this mode"); } - // legacy behaviour that assumed all KeyStoreSources where in the non compliant PKCS12 format + // legacy behaviour that assumed all KeyStoreSources were in the non FIPS compliant PKCS12 format KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new ByteArrayInputStream(getKeyStoreBytes()), password); return keyStore; From 00f26696ff7928d35c8b874c3a49225c57793772 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 4 Jan 2026 22:18:28 +0100 Subject: [PATCH 34/36] SecretBytes: getPlainData(): if we failed decoding by a copy of the plugin on a remote agent JVM, note that in the thrown Error Signed-off-by: Jim Klimov --- .../com/cloudbees/plugins/credentials/SecretBytes.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java index 9463fc6ac..9f79470a1 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java +++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytes.java @@ -35,6 +35,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Util; +import hudson.remoting.Channel; import hudson.util.Secret; import java.io.Serializable; import java.security.GeneralSecurityException; @@ -143,7 +144,12 @@ public byte[] getPlainData() { Cipher cipher = KEY.decrypt(salt); return cipher.doFinal(encryptedBytes); } catch (GeneralSecurityException e) { - throw new Error(e); + if (Channel.current() == null) { + // Local JVM of the Jenkins controller + throw new Error(e); + } else { + throw new Error("Failed to access secret data by remote agent", e); + } } } From b8bb5a8bdbac9459f333a9236551f7eae77bb1b6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 17:02:49 +0100 Subject: [PATCH 35/36] CredentialsInPipelineTest, pom.xml: comment away dependencies and test code for use of HTTP Request plugin with Credentials [JENKINS-70101] Tests to make sure the complex call stack works properly are offloaded into that plugin, see https://github.com/jenkinsci/http-request-plugin/pull/231 Signed-off-by: Jim Klimov --- pom.xml | 17 ++++++++++------- .../credentials/CredentialsInPipelineTest.java | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index d500fbb1d..fb27565d5 100644 --- a/pom.xml +++ b/pom.xml @@ -169,28 +169,31 @@ test - org.jenkins-ci.plugins - job-dsl - test + + org.jenkins-ci.plugins + credentials-binding + test + + - - > test +--> diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java index 4f92ea6a3..127fcf97b 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java @@ -444,7 +444,7 @@ void testCertWithCredentialsOnNodeRemote() throws Exception { ///////////////////////////////////////////////////////////////// // Certificate credentials retrievability by http-request-plugin ///////////////////////////////////////////////////////////////// - +/* private String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { return cpsScriptCredentialTestHttpRequest("myCert", runnerTag, true); } @@ -660,4 +660,5 @@ void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { r.assertLogContains("HTTP Request Plugin Response: ", run); } +*/ } From c2ca4f469cb891ea7ffcaeeaefe7c4a8d284bd4c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 17:13:21 +0100 Subject: [PATCH 36/36] CredentialsProvider: formally annotate findCredentialById() documented as deprecated ...to avoid a maven warning Signed-off-by: Jim Klimov --- .../com/cloudbees/plugins/credentials/CredentialsProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/cloudbees/plugins/credentials/CredentialsProvider.java b/src/main/java/com/cloudbees/plugins/credentials/CredentialsProvider.java index 221172e9a..d94790b57 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/CredentialsProvider.java +++ b/src/main/java/com/cloudbees/plugins/credentials/CredentialsProvider.java @@ -867,6 +867,7 @@ public static C findCredentialById(@NonNull String id, /** * @deprecated Use {@link #findCredentialById(String, Class, Run, List)} instead. */ + @Deprecated public static C findCredentialById(@NonNull String id, @NonNull Class type, @NonNull Run run, DomainRequirement... domainRequirements) {