Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-69081] Track credential usage #113

Merged
merged 4 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,17 @@ You can handle response

[source,groovy]
----
httpRequest responseHandle: 'LEAVE_OPEN', url:'https://api.github.com/orgs/${orgName}'
def response = httpRequest responseHandle: 'LEAVE_OPEN',
url: "https://api.github.com/orgs/${orgName}"
response.close() // must call response.close() after a LEAVE_OPEN
----

You can use a Jenkins credential to authenticate the request

[source,groovy]
----
def response = httpRequest authenticate: 'my-jenkins-credential-id',
url: 'https://api.github.com/user/jenkinsci'
----

For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.Item;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.remoting.RemoteOutputStream;
import hudson.security.ACL;
Expand Down Expand Up @@ -118,6 +119,7 @@ static HttpRequestExecution from(HttpRequest http,
FilePath outputFile = http.resolveOutputFile(envVars, build);
FilePath uploadFile = http.resolveUploadFile(envVars, build);
Item project = build.getProject();
Run<?, ?> run = build;

List<HttpRequestFormDataPart> formData = http.resolveFormDataParts(envVars, build);

Expand All @@ -134,6 +136,7 @@ static HttpRequestExecution from(HttpRequest http,
ResponseHandle.NONE,

project,
run,
taskListener.getLogger());
} catch (IOException e) {
throw new IllegalStateException(e);
Expand All @@ -149,6 +152,8 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener

// TODO: resolveFormDataParts missing
Item project = execution.getProject();
Run<?, ?> run = execution.getContext().get(Run.class);

return new HttpRequestExecution(
step.getUrl(), step.getHttpMode(), step.isIgnoreSslErrors(),
step.getHttpProxy(), step.getProxyAuthentication(),
Expand All @@ -160,7 +165,7 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener
step.getValidResponseCodes(), step.getValidResponseContent(),
step.getConsoleLogResponseBody(), outputFile,
step.getResponseHandle(),
project, taskListener.getLogger());
project, run, taskListener.getLogger());
}

private HttpRequestExecution(
Expand All @@ -175,7 +180,7 @@ private HttpRequestExecution(
Boolean consoleLogResponseBody, FilePath outputFile,
ResponseHandle responseHandle,

Item project, PrintStream logger
Item project, Run<?, ?> run, PrintStream logger
) {
this.url = url;
this.httpMode = httpMode;
Expand All @@ -191,6 +196,9 @@ private HttpRequestExecution(
project, ACL.SYSTEM,
URIRequirementBuilder.fromUri(url).build()),
CredentialsMatchers.withId(proxyAuthentication));

CredentialsProvider.trackAll(run, credential);

if (credential instanceof StandardUsernamePasswordCredentials) {
this.proxyCredentials = (StandardUsernamePasswordCredentials) credential;
} else {
Expand Down Expand Up @@ -220,6 +228,9 @@ private HttpRequestExecution(
project, ACL.SYSTEM,
URIRequirementBuilder.fromUri(url).build()),
CredentialsMatchers.withId(authentication));

CredentialsProvider.trackAll(run, credential);

if (credential != null) {
if (credential instanceof StandardUsernamePasswordCredentials) {
if (this.useNtlm) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package jenkins.plugins.http_request;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import hudson.model.Fingerprint;
import hudson.model.Result;
import java.util.Collections;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;

import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.junit.Before;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

/**
* @author Mark Waite
*/
public class HttpRequestStepCredentialsTest extends HttpRequestTestBase {

private StandardCredentials getInvalidCredential() {
String username = "bad-user";
String password = "bad-password";
CredentialsScope scope = CredentialsScope.GLOBAL;
String id = "username-" + username + "-password-" + password;
return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password);
}

private CredentialsStore store = null;

@Before
public void enableSystemCredentialsProvider() throws Exception {
SystemCredentialsProvider.getInstance()
.setDomainCredentialsMap(
Collections.singletonMap(Domain.global(), Collections.emptyList()));
for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) {
if (s.getProvider() instanceof SystemCredentialsProvider.ProviderImpl) {
store = s;
break;
}
}
assertThat("The system credentials provider is enabled", store, notNullValue());
}

@Test
public void trackCredentials() throws Exception {
StandardCredentials credential = getInvalidCredential();
store.addCredentials(Domain.global(), credential);

Fingerprint fingerprint = CredentialsProvider.getFingerprintOf(credential);
assertThat("Fingerprint should not be set before job definition", fingerprint, nullValue());

JenkinsRule.WebClient wc = j.createWebClient();
HtmlPage page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
assertThat("Have usage tracking reported", page.getElementById("usage"), notNullValue());
assertThat(
"No fingerprint created until first use on missing page",
page.getElementById("usage-missing"),
notNullValue());
assertThat(
"No fingerprint created until first use on present page",
page.getElementById("usage-present"),
nullValue());

// Configure the build to use the credential
WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
proj.setDefinition(
new CpsFlowDefinition(
"def response = httpRequest(url: 'https://api.github.com/users/jenkinsci',\n"
+ " authentication: '" + credential.getId() + "')\n"
+ "println('Status: '+response.getStatus())\n"
+ "println('Response: '+response.getContent())\n",
true));

fingerprint = CredentialsProvider.getFingerprintOf(credential);
assertThat("Fingerprint should not be set before first build", fingerprint, nullValue());

// Execute the build
WorkflowRun run = proj.scheduleBuild2(0).get();

// Check expectations
j.assertBuildStatus(Result.SUCCESS, run);
j.assertLogContains("https://api.github.com/users/jenkinsci/followers", run);

// Check the credential use was correctly tracked
fingerprint = CredentialsProvider.getFingerprintOf(credential);
assertThat("Fingerprint should be set after first build", fingerprint, notNullValue());
assertThat(fingerprint.getJobs(), hasItem(is(proj.getFullName())));
Fingerprint.RangeSet rangeSet = fingerprint.getRangeSet(proj);
assertThat(rangeSet, notNullValue());
assertThat(rangeSet.includes(proj.getLastBuild().getNumber()), is(true));

page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
assertThat(page.getElementById("usage-missing"), nullValue());
assertThat(page.getElementById("usage-present"), notNullValue());
assertThat(page.getAnchorByText(proj.getFullDisplayName()), notNullValue());
}
}