Skip to content

Commit

Permalink
Merge pull request #11 from christiangalsterer/JENKINS-25306
Browse files Browse the repository at this point in the history
[JENKINS-25306] Build status badge for specific build of a job
  • Loading branch information
christiangalsterer committed Nov 29, 2014
2 parents 508911b + 6c23f30 commit 769640c
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 11 deletions.
40 changes: 35 additions & 5 deletions src/main/java/org/jenkinsci/plugins/badge/PublicBadgeAction.java
Expand Up @@ -25,6 +25,7 @@

import hudson.Extension;
import hudson.model.Item;
import hudson.model.Run;
import hudson.model.UnprotectedRootAction;
import hudson.model.AbstractProject;
import hudson.security.ACL;
Expand Down Expand Up @@ -52,6 +53,12 @@
*
* <li>http://localhost:8080/buildstatus/icon?job=[JOBNAME] <li>e.g. http://localhost:8080/buildstatus/icon?job=free1 <br/>
* <br/>
*
* The status of a particular build can be checked like this:
*
* <li>http://localhost:8080/buildstatus/icon?job=[JOBNAME]&build=[BUILDNUMBER] <li>e.g. http://localhost:8080/buildstatus/icon?job=free1&build=5<br/>
* <br/>
*
* Even though the URL is unprotected, the user does still need the 'ViewStatus' permission on the given Job. If you want the status icons to be public readable/accessible, just grant the 'ViewStatus'
* permission globally to 'anonymous'.
*
Expand All @@ -60,7 +67,7 @@
@Extension
public class PublicBadgeAction implements UnprotectedRootAction {

final public static Permission VIEW_STATUS = new Permission(Item.PERMISSIONS, "ViewStatus", Messages._ViewStatus_Permission(), Item.READ, PermissionScope.ITEM);
public final static Permission VIEW_STATUS = new Permission(Item.PERMISSIONS, "ViewStatus", Messages._ViewStatus_Permission(), Item.READ, PermissionScope.ITEM);

private final ImageResolver iconResolver;

Expand All @@ -83,12 +90,17 @@ public String getDisplayName() {
/**
* Serves the badge image.
*/
public HttpResponse doIcon(StaplerRequest req, StaplerResponse rsp, @QueryParameter String job) throws IOException, ServletException {
AbstractProject<?, ?> project = getProject(job, req, rsp);
return iconResolver.getImage(project.getIconColor());
public HttpResponse doIcon(StaplerRequest req, StaplerResponse rsp, @QueryParameter String job, @QueryParameter String build) {
if(build != null) {
Run run = getRun(job, build);
return iconResolver.getImage(run.getIconColor());
} else {
AbstractProject<?, ?> project = getProject(job);
return iconResolver.getImage(project.getIconColor());
}
}

private AbstractProject<?, ?> getProject(String job, StaplerRequest req, StaplerResponse rsp) throws IOException, HttpResponses.HttpResponseException {
private AbstractProject<?, ?> getProject(String job) {
AbstractProject<?, ?> p;

// as the user might have ViewStatus permission only (e.g. as anonymous) we get get the project impersonate and check for permission after getting the project
Expand All @@ -107,4 +119,22 @@ public HttpResponse doIcon(StaplerRequest req, StaplerResponse rsp, @QueryParame
return p;
}

private Run<?, ?> getRun(String job, String build) {
Run<?, ?> run;

// as the user might have ViewStatus permission only (e.g. as anonymous) we get get the project impersonate and check for permission after getting the project
SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
try {
run = Jenkins.getInstance().getItemByFullName(job, AbstractProject.class).getBuildByNumber(Integer.parseInt(build));
} finally {
SecurityContextHolder.setContext(orig);
}

// check if user has permission to view the status
if(run == null || !(run.hasPermission(VIEW_STATUS))){
throw HttpResponses.notFound();
}

return run;
}
}
38 changes: 38 additions & 0 deletions src/main/java/org/jenkinsci/plugins/badge/RunBadgeAction.java
@@ -0,0 +1,38 @@
package org.jenkinsci.plugins.badge;

import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Run;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;

public class RunBadgeAction implements Action {
private final RunBadgeActionFactory factory;
public final Run run;
public final AbstractProject project;

public RunBadgeAction(RunBadgeActionFactory factory, Run run) {
this.factory = factory;
this.run = run;
this.project = (AbstractProject)run.getParent();
}

public String getIconFileName() {
return Jenkins.RESOURCE_PATH+"/plugin/embeddable-build-status/images/24x24/shield.png";
}

public String getDisplayName() {
return Messages.RunBadgeAction_DisplayName();
}

public String getUrlName() {
return "badge";
}

/**
* Serves the badge image.
*/
public HttpResponse doIcon() {
return factory.getImage(run.getIconColor());
}
}
@@ -0,0 +1,27 @@
package org.jenkinsci.plugins.badge;

import hudson.Extension;
import hudson.model.*;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;

@Extension
public class RunBadgeActionFactory extends TransientBuildActionFactory {

private final ImageResolver iconResolver;

public RunBadgeActionFactory() throws IOException {
iconResolver = new ImageResolver();
}

@Override
public Collection<? extends Action> createFor(Run target) {
return Collections.singleton(new RunBadgeAction(this, target));
}

public StatusImage getImage(BallColor color) {
return iconResolver.getImage(color);
}
}
@@ -1,2 +1,3 @@
BadgeAction.DisplayName=Embeddable Build Status
ViewStatus.Permission=This permission grants the ability to view the build status via embeddable build status plugin.
RunBadgeAction.DisplayName=Embeddable Build Status
ViewStatus.Permission=This permission grants the ability to view the build status via embeddable build status plugin.
@@ -1 +1,2 @@
BadgeAction.DisplayName=\u57CB\u3081\u8FBC\u307F\u7528\u30B9\u30C6\u30FC\u30BF\u30B9\u30A2\u30A4\u30B3\u30F3
BadgeAction.DisplayName=\u57CB\u3081\u8FBC\u307F\u7528\u30B9\u30C6\u30FC\u30BF\u30B9\u30A2\u30A4\u30B3\u30F3
RunBadgeAction.DisplayName=\u57CB\u3081\u8FBC\u307F\u7528\u30B9\u30C6\u30FC\u30BF\u30B9\u30A2\u30A4\u30B3\u30F3
Expand Up @@ -25,6 +25,8 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import hudson.model.FreeStyleProject;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.SecurityRealm;

Expand All @@ -48,7 +50,7 @@ public class PublicBadgeActionTest {
@PresetData(PresetData.DataSet.NO_ANONYMOUS_READACCESS)
@Test
public void authenticatedAccess() throws Exception {
j.createFreeStyleProject("free");
final FreeStyleProject project = j.createFreeStyleProject("free");
JenkinsRule.WebClient wc = j.createWebClient();
wc.login("alice", "alice");
try {
Expand All @@ -59,12 +61,14 @@ public void authenticatedAccess() throws Exception {
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}
wc.goTo("buildStatus/icon?job=free", "image/svg+xml");
j.buildAndAssertSuccess(project);
wc.goTo("buildStatus/icon?job=free&build=1", "image/svg+xml");
}

@PresetData(PresetData.DataSet.NO_ANONYMOUS_READACCESS)
@Test
public void invalidAnonymousAccess() throws Exception {
j.createFreeStyleProject("free");
final FreeStyleProject project = j.createFreeStyleProject("free");
JenkinsRule.WebClient wc = j.createWebClient();
try {
// try with wrong job name
Expand All @@ -82,6 +86,17 @@ public void invalidAnonymousAccess() throws Exception {
// make sure return code does not leak security relevant information (must 404)
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}

j.buildAndAssertSuccess(project);

try {
// try with correct job name
wc.goTo("buildStatus/icon?job=free&build=1", "image/svg+xml");
fail("should fail, because there is no job with this name");
} catch (FailingHttpStatusCodeException x) {
// make sure return code does not leak security relevant information (must 404)
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}
}

@Test
Expand All @@ -94,7 +109,7 @@ public void validAnonymousViewStatusAccess() throws Exception {
j.getInstance().setSecurityRealm(realm);
j.getInstance().setAuthorizationStrategy(auth);

j.createFreeStyleProject("free");
final FreeStyleProject project = j.createFreeStyleProject("free");

JenkinsRule.WebClient wc = j.createWebClient();
try {
Expand All @@ -104,14 +119,24 @@ public void validAnonymousViewStatusAccess() throws Exception {
} catch (FailingHttpStatusCodeException x) {
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}

try {
// try with wrong job name
wc.goTo("buildStatus/icon?job=free&build=1");
fail("should fail, because there is no job with this name");
} catch (FailingHttpStatusCodeException x) {
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}

wc.goTo("buildStatus/icon?job=free", "image/svg+xml");
j.buildAndAssertSuccess(project);
wc.goTo("buildStatus/icon?job=free&build=1", "image/svg+xml");
}

@PresetData(PresetData.DataSet.ANONYMOUS_READONLY)
@Test
public void validAnonymousAccess() throws Exception {
j.createFreeStyleProject("free");
final FreeStyleProject project = j.createFreeStyleProject("free");
JenkinsRule.WebClient wc = j.createWebClient();
try {
// try with wrong job name
Expand All @@ -121,7 +146,17 @@ public void validAnonymousAccess() throws Exception {
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}

try {
// try with wrong job name
wc.goTo("buildStatus/icon?job=free&build=1");
fail("should fail, because there is no job with this name");
} catch (FailingHttpStatusCodeException x) {
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, x.getStatusCode());
}

// try with correct job name
wc.goTo("buildStatus/icon?job=free", "image/svg+xml");
j.buildAndAssertSuccess(project);
wc.goTo("buildStatus/icon?job=free&build=1", "image/svg+xml");
}
}

0 comments on commit 769640c

Please sign in to comment.