/* * The MIT License * * Copyright 2015 Jesse Glick. * * 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 org.jenkinsci.plugins.credentialsbinding.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.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Fingerprint; import hudson.model.User; import jenkins.security.QueueItemAuthenticatorConfiguration; import hudson.FilePath; import hudson.model.Node; import hudson.model.Result; import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; import hudson.slaves.DumbSlave; import hudson.slaves.NodeProperty; import hudson.slaves.RetentionStrategy; import hudson.slaves.WorkspaceList; import hudson.util.Secret; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.inject.Inject; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.authorizeproject.AuthorizeProjectProperty; import org.jenkinsci.plugins.authorizeproject.ProjectQueueItemAuthenticator; import org.jenkinsci.plugins.authorizeproject.strategy.SpecificUsersAuthorizationStrategy; import org.jenkinsci.plugins.credentialsbinding.MultiBinding; import org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl; import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.BlanketWhitelist; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.SnippetizerTester; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; import org.jenkinsci.plugins.workflow.steps.StepConfigTester; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.*; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runners.model.Statement; import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.RestartableJenkinsRule; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.DataBoundConstructor; public class BindingStepTest { @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); @Rule public TemporaryFolder tmp = new TemporaryFolder(); @Test public void configRoundTrip() throws Exception { story.addStep(new Statement() { @SuppressWarnings("rawtypes") @Override public void evaluate() throws Throwable { UsernamePasswordCredentialsImpl c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", "bob", "s3cr3t"); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); BindingStep s = new StepConfigTester(story.j).configRoundTrip(new BindingStep(Collections.<MultiBinding>singletonList(new UsernamePasswordBinding("userpass", "creds")))); story.j.assertEqualDataBoundBeans(s.getBindings(), Collections.singletonList(new UsernamePasswordBinding("userpass", "creds"))); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), new FileCredentialsImpl(CredentialsScope.GLOBAL, "secrets", "sample", "secrets.zip", SecretBytes.fromBytes(new byte[] {0x50,0x4B,0x05,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}))); // https://en.wikipedia.org/wiki/Zip_(file_format)#Limits new SnippetizerTester(story.j).assertRoundTrip(new BindingStep(Collections.<MultiBinding>singletonList(new ZipFileBinding("file", "secrets"))), "withCredentials([[$class: 'ZipFileBinding', credentialsId: 'secrets', variable: 'file']]) {\n // some block\n}"); } }); } public static class ZipStep extends AbstractStepImpl { @DataBoundConstructor public ZipStep() {} @TestExtension("configRoundTrip") public static class DescriptorImpl extends AbstractStepDescriptorImpl { public DescriptorImpl() {super(Execution.class);} @Override public String getFunctionName() {return "zip";} } public static class Execution extends AbstractSynchronousStepExecution<Void> { @Override protected Void run() throws Exception {return null;} } } @Test public void basics() throws Exception { final String credentialsId = "creds"; final String username = "bob"; final String password = "s3cr3t"; story.addStep(new Statement() { @Override public void evaluate() throws Throwable { UsernamePasswordCredentialsImpl c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", username, password); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "node {\n" + " withCredentials([usernamePassword(usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD', credentialsId: '" + credentialsId + "')]) {\n" + " semaphore 'basics'\n" + " sh '''\n" + " set +x\n" + " echo curl -u $USERNAME:$PASSWORD server > script.sh\n" + " '''\n" + " }\n" + "}", true)); WorkflowRun b = p.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("basics/1", b); } }); story.addStep(new Statement() { @Override public void evaluate() throws Throwable { WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); assertNotNull(p); WorkflowRun b = p.getBuildByNumber(1); assertNotNull(b); assertEquals(Collections.<String>emptySet(), grep(b.getRootDir(), password)); SemaphoreStep.success("basics/1", null); story.j.waitForCompletion(b); story.j.assertBuildStatusSuccess(b); story.j.assertLogNotContains(password, b); FilePath script = story.j.jenkins.getWorkspaceFor(p).child("script.sh"); assertTrue(script.exists()); assertEquals("curl -u " + username + ":" + password + " server", script.readToString().trim()); assertEquals(Collections.<String>emptySet(), grep(b.getRootDir(), password)); } }); } @Issue("JENKINS-42999") @Test public void limitedRequiredContext() throws Exception { final String credentialsId = "creds"; final String username = "bob"; final String password = "s3cr3t"; story.addStep(new Statement() { @Override public void evaluate() throws Throwable { UsernamePasswordCredentialsImpl c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", username, password); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "withCredentials([usernameColonPassword(variable: 'USERPASS', credentialsId: '" + credentialsId + "')]) {\n" + " semaphore 'basics'\n" + " echo USERPASS.toUpperCase()\n" + "}", true)); WorkflowRun b = p.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("basics/1", b); } }); story.addStep(new Statement() { @Override public void evaluate() throws Throwable { WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); assertNotNull(p); WorkflowRun b = p.getBuildByNumber(1); assertNotNull(b); assertEquals(Collections.<String>emptySet(), grep(b.getRootDir(), password)); SemaphoreStep.success("basics/1", null); story.j.waitForCompletion(b); story.j.assertBuildStatusSuccess(b); story.j.assertLogContains((username + ":" + password).toUpperCase(), b); story.j.assertLogNotContains(password, b); } }); } @Issue("JENKINS-42999") @Test public void widerRequiredContext() throws Exception { final String credentialsId = "creds"; final String credsFile = "credsFile"; final String credsContent = "s3cr3t"; story.addStep(new Statement() { @Override public void evaluate() throws Throwable { FileCredentialsImpl c = new FileCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", credsFile, SecretBytes.fromBytes(credsContent.getBytes())); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "withCredentials([file(variable: 'targetFile', credentialsId: '" + credentialsId + "')]) {\n" + " echo 'We should fail before getting here'\n" + "}", true)); WorkflowRun b = p.scheduleBuild2(0).waitForStart(); story.j.assertBuildStatus(Result.FAILURE, story.j.waitForCompletion(b)); story.j.assertLogNotContains("We should fail before getting here", b); story.j.assertLogContains("Required context class hudson.FilePath is missing", b); story.j.assertLogContains("Perhaps you forgot to surround the code with a step that provides this, such as: node", b); } }); } @Inject StringCredentialsImpl.DescriptorImpl stringCredentialsDescriptor; @Test public void incorrectType() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { StringCredentialsImpl c = new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", Secret.fromString("s3cr3t")); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "node {\n" + " withCredentials([usernamePassword(usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD', credentialsId: 'creds')]) {\n" + " }\n" + "}", true)); WorkflowRun r = story.j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); // make sure error message contains information about the actual type and the expected type story.j.assertLogNotContains("s3cr3t", r); story.j.assertLogContains(CredentialNotFoundException.class.getName(), r); story.j.assertLogContains(StandardUsernamePasswordCredentials.class.getName(), r); story.j.assertLogContains(stringCredentialsDescriptor.getDisplayName(), r); } }); } @Test public void cleanupAfterRestart() throws Exception { final String secret = "s3cr3t"; story.addStep(new Statement() { @Override public void evaluate() throws Throwable { FileCredentialsImpl c = new FileCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", "secret.txt", SecretBytes.fromBytes(secret.getBytes())); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c); // TODO JENKINS-26398: story.j.createSlave("myslave", null, null) does not work since the slave root is deleted after restart. story.j.jenkins.addNode(new DumbSlave("myslave", "", tmp.newFolder().getAbsolutePath(), "1", Node.Mode.NORMAL, "", story.j.createComputerLauncher(null), RetentionStrategy.NOOP, Collections.<NodeProperty<?>>emptyList())); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "node('myslave') {" + " withCredentials([file(variable: 'SECRET', credentialsId: 'creds')]) {\n" + " semaphore 'cleanupAfterRestart'\n" + " sh 'cp $SECRET key'\n" + " }\n" + "}", true)); WorkflowRun b = p.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("cleanupAfterRestart/1", b); } }); story.addStep(new Statement() { @Override public void evaluate() throws Throwable { WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); assertNotNull(p); WorkflowRun b = p.getBuildByNumber(1); assertNotNull(b); assertEquals(Collections.<String>emptySet(), grep(b.getRootDir(), secret)); SemaphoreStep.success("cleanupAfterRestart/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b)); story.j.assertLogNotContains(secret, b); FilePath ws = story.j.jenkins.getNode("myslave").getWorkspaceFor(p); FilePath key = ws.child("key"); assertTrue(key.exists()); assertEquals(secret, key.readToString()); FilePath secretFiles = tempDir(ws).child("secretFiles"); assertTrue(secretFiles.isDirectory()); assertEquals(Collections.emptyList(), secretFiles.list()); assertEquals(Collections.<String>emptySet(), grep(b.getRootDir(), secret)); } }); } // TODO 1.652 use WorkspaceList.tempDir private static FilePath tempDir(FilePath ws) { return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); } @Issue("JENKINS-27389") @Test public void grabEnv() { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { String credentialsId = "creds"; String secret = "s3cr3t"; CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", Secret.fromString(secret))); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "def extract(id) {\n" + " def v\n" + " withCredentials([string(credentialsId: id, variable: 'temp')]) {\n" + " v = env.temp\n" + " }\n" + " v\n" + "}\n" + "node {\n" + " echo \"got: ${extract('" + credentialsId + "')}\"\n" + "}", true)); story.j.assertLogContains("got: " + secret, story.j.assertBuildStatusSuccess(p.scheduleBuild2(0).get())); } }); } @Issue("JENKINS-27486") @Test public void masking() { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { String credentialsId = "creds"; String secret = "s3cr3t"; CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", Secret.fromString(secret))); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "node {\n" + " withCredentials([string(credentialsId: '" + credentialsId + "', variable: 'SECRET')]) {\n" // forgot set +x, ran /usr/bin/env, etc. + " sh 'echo $SECRET > oops'\n" + " }\n" + "}", true)); WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); story.j.assertLogNotContains(secret, b); story.j.assertLogContains("echo ****", b); } }); } @Issue("JENKINS-30326") @Test public void testGlobalBindingWithAuthorization() { story.addStep(new Statement() { @SuppressWarnings("deprecation") // using TestExtension would be better, as would calling ScriptApproval.preapprove @Override public void evaluate() throws Throwable { // configure security story.j.jenkins.setSecurityRealm(story.j.createDummySecurityRealm()); story.j.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); // create the user. User.get("dummy", true); // enable the run as user strategy for the AuthorizeProject plugin Map<String, Boolean> strategies = new HashMap<String, Boolean>(); strategies.put(story.j.jenkins.getDescriptor(SpecificUsersAuthorizationStrategy.class).getId(), true); QueueItemAuthenticatorConfiguration.get().getAuthenticators().add(new ProjectQueueItemAuthenticator(strategies)); // blanket whitelist all methods (easier than whitelisting Jenkins.getAuthentication) story.j.jenkins.getExtensionList(Whitelist.class).add(new BlanketWhitelist()); String credentialsId = "creds"; String secret = "s3cr3t"; CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", Secret.fromString(secret))); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "node {\n" + " def authentication = Jenkins.getAuthentication()\n" + " echo \"running as user: $authentication.principal\"\n" + " withCredentials([string(credentialsId: '" + credentialsId + "', variable: 'SECRET')]) {\n" + " writeFile file:'test', text: \"$env.SECRET\"\n" + " def content = readFile 'test'\n" + " if (\"$content\" != \"" + secret + "\") {\n" + " error 'The credential was not bound into the workflow correctly'\n" + " }\n" + " }\n" + "}", true)); // run the job as a specific user p.addProperty(new AuthorizeProjectProperty(new SpecificUsersAuthorizationStrategy("dummy", true))); // the build will fail if we can not locate the credentials WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); // make sure this was actually run as a user and not system story.j.assertLogContains("running as user: dummy", b); } }); } @Issue("JENKINS-38831") @Test public void testTrackingOfCredential() { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { String credentialsId = "creds"; String secret = "s3cr3t"; StringCredentialsImpl credentials = new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, "sample", Secret.fromString(secret)); Fingerprint fingerprint = CredentialsProvider.getFingerprintOf(credentials); CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), credentials); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition("" + "def extract(id) {\n" + " def v\n" + " withCredentials([[$class: 'StringBinding', credentialsId: id, variable: 'temp']]) {\n" + " v = env.temp\n" + " }\n" + " v\n" + "}\n" + "node {\n" + " echo \"got: ${extract('" + credentialsId + "')}\"\n" + "}", true)); assertThat("No fingerprint created until first use", fingerprint, nullValue()); story.j.assertLogContains("got: " + secret, story.j.assertBuildStatusSuccess(p.scheduleBuild2(0).get())); fingerprint = CredentialsProvider.getFingerprintOf(credentials); assertThat(fingerprint, notNullValue()); assertThat(fingerprint.getJobs(), hasItem(is(p.getFullName()))); } }); } private static Set<String> grep(File dir, String text) throws IOException { Set<String> matches = new TreeSet<String>(); grep(dir, text, "", matches); return matches; } private static void grep(File dir, String text, String prefix, Set<String> matches) throws IOException { File[] kids = dir.listFiles(); if (kids == null) { return; } for (File kid : kids) { String qualifiedName = prefix + kid.getName(); if (kid.isDirectory()) { grep(kid, text, qualifiedName + "/", matches); } else if (kid.isFile() && FileUtils.readFileToString(kid).contains(text)) { matches.add(qualifiedName); } } } }