diff --git a/pom.xml b/pom.xml
index b73040a64..8d4beaf91 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,33 @@
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
+ credentials-binding
+ 523.vd859a_4b_122e6
+ test
+
+
+ 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 a92d84f48..ad9b9dbd6 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.
*
@@ -419,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.
@@ -457,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
*
@@ -475,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.
*
@@ -490,11 +533,14 @@ 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() {
+ if (uploadedKeystore != null && uploadedKeystoreBytes == null) {
+ return SecretBytes.fromBytes(DescriptorImpl.toByteArray(uploadedKeystore));
+ }
return uploadedKeystoreBytes;
}
@@ -504,6 +550,9 @@ public SecretBytes getUploadedKeystore() {
@NonNull
@Override
public byte[] getKeyStoreBytes() {
+ if (uploadedKeystore != null && uploadedKeystoreBytes == null) {
+ return DescriptorImpl.toByteArray(uploadedKeystore);
+ }
return SecretBytes.getPlainData(uploadedKeystoreBytes);
}
@@ -520,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);
}
/**
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..90697d7e5
--- /dev/null
+++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsSnapshotTaker.java
@@ -0,0 +1,97 @@
+/*
+ * 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 hudson.util.Secret;
+
+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(CertificateCredentialsImpl.UploadedKeyStoreSource.DescriptorImpl.toSecret(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(CertificateCredentialsImpl.UploadedKeyStoreSource.DescriptorImpl.toSecret(bos.toByteArray())));
+ }
+}
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..579318a7a
--- /dev/null
+++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsInPipelineTest.java
@@ -0,0 +1,702 @@
+/*
+ * 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.plugins.credentials.impl.CertificateCredentialsImpl;
+import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest;
+import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
+import hudson.model.Descriptor;
+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;
+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.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+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.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.
+ */
+
+ // 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();
+
+ // 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;
+ }
+
+ String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ f.getLogText().writeLogTo(0, baos);
+ return baos.toString();
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // 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) {
+ // 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);
+ }
+
+ 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";
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // Certificate credentials retrievability in (trusted) pipeline
+ /////////////////////////////////////////////////////////////////
+
+ String cpsScriptCertCredentialTestScriptedPipeline(String runnerTag) {
+ return cpsScriptCertCredentialTestScriptedPipeline("myCert", "password", "1", runnerTag);
+ }
+
+ String cpsScriptCertCredentialTestScriptedPipeline(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" +
+ " 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" +
+ "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" +
+ "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" +
+ "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";
+ }
+
+ @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() +
+ cpsScriptCertCredentialTestScriptedPipeline("CONTROLLER BUILT-IN");
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // 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" +
+ cpsScriptCertCredentialTestScriptedPipeline("CONTROLLER NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // 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" +
+ cpsScriptCertCredentialTestScriptedPipeline("REMOTE NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ r.assertLogContains("KSS-SNAP bytes", run);
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // Certificate credentials retrievability by withCredentials() step
+ /////////////////////////////////////////////////////////////////
+
+ String cpsScriptCertCredentialTestGetKeyValue() {
+ 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, keyPassword.toCharArray())\n" +
+ " return key.getEncoded().encodeBase64().toString()\n" +
+ "}\n" +
+ "\n";
+ }
+
+ String cpsScriptCertCredentialTestWithCredentials(String runnerTag) {
+ return cpsScriptCertCredentialTestWithCredentials("myCert", "password", "1", runnerTag);
+ }
+
+ 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
+ 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" +
+ " keystoreVariable: 'keystoreName',\n" +
+ " passwordVariable: 'keyPassword',\n" +
+ " aliasVariable: 'myKeyAlias')\n" +
+ "]) {\n" +
+ " echo \"Keystore bytes (len): \" + (new File(keystoreName)).length()\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" +
+ " println keyValue\n" +
+ " println \"-----END PRIVATE KEY-----\"\n" +
+ "}\n" +
+ "\n";
+ }
+
+ @Test
+ @Ignore("Work with keystore file requires a node")
+ @Issue("JENKINS-70101")
+ public void testCertWithCredentialsOnController() 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() +
+ cpsScriptCertCredentialTestGetKeyValue() +
+ cpsScriptCertCredentialTestWithCredentials("CONTROLLER BUILT-IN");
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ r.assertLogContains("END PRIVATE KEY", run);
+ }
+
+ @Test
+ @Issue("JENKINS-70101")
+ public void testCertWithCredentialsOnNodeLocal() 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() +
+ cpsScriptCertCredentialTestGetKeyValue() +
+ "node {\n" +
+ cpsScriptCertCredentialTestWithCredentials("CONTROLLER NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ r.assertLogContains("END PRIVATE KEY", run);
+ }
+
+ @Test
+ @Issue("JENKINS-70101")
+ 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)
+ 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() +
+ cpsScriptCertCredentialTestGetKeyValue() +
+ "node(\"worker\") {\n" +
+ cpsScriptCertCredentialTestWithCredentials("REMOTE NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ r.assertLogContains("END PRIVATE KEY", run);
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // Certificate credentials retrievability by http-request-plugin
+ /////////////////////////////////////////////////////////////////
+
+ String cpsScriptCertCredentialTestHttpRequest(String runnerTag) {
+ return cpsScriptCredentialTestHttpRequest("myCert", runnerTag, true);
+ }
+
+ 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;" + (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;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n"
+ + " KeyStore keyStore = credential.getKeyStore();\n"
+ + " msg = \"Getting keystore source...\"\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;" + (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;" + (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"
+ + " 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 = cpsScriptCredentialTestImports() +
+ cpsScriptCertCredentialTestHttpRequest("CONTROLLER BUILT-IN");
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) 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 = cpsScriptCredentialTestImports() +
+ "node {\n" +
+ cpsScriptCertCredentialTestHttpRequest("CONTROLLER NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) 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 = cpsScriptCredentialTestImports() +
+ "node(\"worker\") {\n" +
+ cpsScriptCertCredentialTestHttpRequest("REMOTE NODE") +
+ "}\n";
+ proj.setDefinition(new CpsFlowDefinition(script, false));
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ 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, false);
+ }
+
+ @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();
+ if (verbosePipelines) 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();
+ if (verbosePipelines) 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();
+ if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
+
+ // Check expectations
+ r.assertBuildStatus(Result.SUCCESS, run);
+ // Got to the end?
+ r.assertLogContains("HTTP Request Plugin Response: ", run);
+ }
+
+}