diff --git a/src/main/java/hudson/plugins/mercurial/MercurialStatus.java b/src/main/java/hudson/plugins/mercurial/MercurialStatus.java index a58998d6..a3c5d678 100644 --- a/src/main/java/hudson/plugins/mercurial/MercurialStatus.java +++ b/src/main/java/hudson/plugins/mercurial/MercurialStatus.java @@ -33,6 +33,7 @@ import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.springframework.security.core.Authentication; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_OK; @@ -108,6 +109,8 @@ public HttpResponse doNotifyCommit(@QueryParameter(required=true) final String u String origin = SCMEvent.originOf(Stapler.getCurrentRequest()); // run in high privilege to see all the projects anonymous users don't see. // this is safe because we only initiate polling. + // But we shouldn't disclose the item names to users that is not supposed to see them + final Authentication origAuth = Jenkins.getAuthentication2(); try (ACLContext context = ACL.as(ACL.SYSTEM)) { if (StringUtils.isNotBlank(branch) && StringUtils.isNotBlank(changesetId)) { SCMHeadEvent.fireNow(new MercurialSCMHeadEvent( @@ -115,13 +118,13 @@ SCMEvent.Type.UPDATED, new MercurialCommitPayload(new URI(url), branch, changese origin)); return HttpResponses.ok(); } - return handleNotifyCommit(origin, new URI(url)); + return handleNotifyCommit(origin, new URI(url), origAuth); } catch ( URISyntaxException ex ) { throw HttpResponses.error(SC_BAD_REQUEST, ex); } } - private HttpResponse handleNotifyCommit(String origin, URI url) throws ServletException, IOException { + private HttpResponse handleNotifyCommit(String origin, URI url, final Authentication origAuth) throws ServletException, IOException { final List projects = Lists.newArrayList(); boolean scmFound = false, urlFound = false; @@ -203,11 +206,19 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod rsp.setStatus(SC_OK); rsp.setContentType("text/plain"); for (Item p : projects) { - rsp.addHeader("Triggered", p.getAbsoluteUrl()); + if (p.hasPermission2(origAuth, Item.READ)) { + rsp.addHeader("Triggered", p.getAbsoluteUrl()); + } else { + rsp.addHeader("Triggered", "Something"); + } } PrintWriter w = rsp.getWriter(); for (Item p : projects) { - w.println("Scheduled polling of " + p.getFullName()); + if (p.hasPermission2(origAuth, Item.READ)) { + w.println("Scheduled polling of " + p.getFullName()); + } else { + w.println("Scheduled polling of a job"); + } } if (msg!=null) w.println(msg); diff --git a/src/test/java/hudson/plugins/mercurial/MercurialStatusPermissionTest.java b/src/test/java/hudson/plugins/mercurial/MercurialStatusPermissionTest.java new file mode 100644 index 00000000..7f4c7d80 --- /dev/null +++ b/src/test/java/hudson/plugins/mercurial/MercurialStatusPermissionTest.java @@ -0,0 +1,172 @@ +package hudson.plugins.mercurial; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.util.NameValuePair; +import hudson.FilePath; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.Slave; +import hudson.triggers.SCMTrigger; +import jenkins.model.Jenkins; +import org.hamcrest.Matcher; +import org.jenkinsci.test.acceptance.docker.DockerClassRule; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.SleepBuilder; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.ArrayMatching.hasItemInArray; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.StringContains.containsString; +import static org.hamcrest.core.StringEndsWith.endsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class MercurialStatusPermissionTest { + + @Rule public JenkinsRule j = new JenkinsRule(); + @Rule public MercurialRule m = new MercurialRule(j); + @ClassRule + public static DockerClassRule docker = new DockerClassRule<>(MercurialContainer.class); + + private final String[] names = new String[]{"one1", "two2", "three3"}; + private final String carls = names[1]; + private String source; + + @Before + public void setup() throws Exception { + m.hg("version"); // test environment needs to be able to run Mercurial + MercurialContainer container = docker.create(); + Slave slave = container.createSlave(j); + m.withNode(slave); + MercurialInstallation inst = container.createInstallation(j, MercurialContainer.Version.HG5, false, false, false, "", slave); + assertNotNull(inst); + m.withInstallation(inst); + FilePath sampleRepo = slave.getRootPath().child("sampleRepo"); + sampleRepo.mkdirs(); + m.hg(sampleRepo, "init"); + sampleRepo.child("a").write("a", "UTF-8"); + m.hg(sampleRepo, "commit", "--addremove", "--message=a-file"); + + source = "ssh://test@" + container.ipBound(22) + ":" + container.port(22) + "/" + sampleRepo; + + FreeStyleProject p = j.createFreeStyleProject(names[0]); + p.setScm(new MercurialSCM( + inst.getName(), + source, + null, null, null, null, false)); + p.addTrigger(new SCMTrigger("")); + p.getBuildersList().add(new SleepBuilder(1000)); + p = j.createFreeStyleProject(names[1]); + FreeStyleProject carls = p; + p.setScm(new MercurialSCM( + inst.getName(), + source, + null, null, null, null, false)); + p.addTrigger(new SCMTrigger("")); + p.getBuildersList().add(new SleepBuilder(1000)); + p = j.createFreeStyleProject(names[2]); + p.setScm(new MercurialSCM( + inst.getName(), + source, + null, null, null, null, false)); + p.addTrigger(new SCMTrigger("")); + p.getBuildersList().add(new SleepBuilder(1000)); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Item.READ).everywhere().to("bob") + .grant(Item.READ).onItems(carls).to("carl") + .grant(Jenkins.ADMINISTER).everywhere().to("alice") + .grant(Jenkins.READ).onRoot().toAuthenticated()); + } + + @Test + public void testTriggeredWithAnonymous() throws Exception { + final Page page = j.createWebClient().goTo("mercurial/notifyCommit?url=" + source, "text/plain"); + final WebResponse response = page.getWebResponse(); + assertEquals(200, response.getStatusCode()); + final List headers = response.getResponseHeaders(); + final List triggered = headers.stream().filter(nvp -> nvp.getName().equals("Triggered")).collect(Collectors.toList()); + assertEquals("No headers!", 3, triggered.size()); + List headerValues = triggered.stream().map(NameValuePair::getValue).collect(Collectors.toList()); + final String content = response.getContentAsString(); + assertThat(content, containsString("Scheduled polling of a job")); + assertThat(headerValues.toArray(new String[headerValues.size()]), hasItemInArray(containsString("Something"))); + + List> testHeaders = new ArrayList<>(); + for (String name : names) { + assertThat(content, not(containsString(name))); + testHeaders.add(not(endsWith(name + "/"))); + } + + assertThat(headerValues, containsInAnyOrder(testHeaders)); + } + + @Test + public void testTriggeredWithAllReadable() throws Exception { + final Page page = j.createWebClient().login("bob").goTo("mercurial/notifyCommit?url=" + source, "text/plain"); + final WebResponse response = page.getWebResponse(); + assertEquals(200, response.getStatusCode()); + final List headers = response.getResponseHeaders(); + final List triggered = headers.stream().filter(nvp -> nvp.getName().equals("Triggered")).collect(Collectors.toList()); + assertEquals("No headers!", 3, triggered.size()); + List headerValues = triggered.stream().map(NameValuePair::getValue).collect(Collectors.toList()); + final String content = response.getContentAsString(); + assertThat(content, containsString("Scheduled polling of")); + assertThat(content, not(containsString("Scheduled polling of a job"))); + assertThat(headerValues.toArray(new String[headerValues.size()]), not(hasItemInArray(containsString("Something")))); + + List> testHeaders = new ArrayList<>(); + for (String name : names) { + assertThat(content, containsString(name)); + testHeaders.add(endsWith(name + "/")); + } + + assertThat(headerValues, containsInAnyOrder(testHeaders)); + } + + @Test + public void testTriggeredWithOneReadable() throws Exception { + final Page page = j.createWebClient().login("carl").goTo("mercurial/notifyCommit?url=" + source, "text/plain"); + final WebResponse response = page.getWebResponse(); + assertEquals(200, response.getStatusCode()); + final List headers = response.getResponseHeaders(); + final List triggered = headers.stream().filter(nvp -> nvp.getName().equals("Triggered")).collect(Collectors.toList()); + assertEquals("No headers!", 3, triggered.size()); + List headerValues = triggered.stream().map(NameValuePair::getValue).collect(Collectors.toList()); + final String content = response.getContentAsString(); + assertThat(content, containsString("Scheduled polling of")); + assertThat(content, containsString("Scheduled polling of a job")); + assertThat(headerValues.toArray(new String[headerValues.size()]), hasItemInArray(containsString("Something"))); + + List> testHeaders = new ArrayList<>(); + for (String name : names) { + if (name.equals(carls)) { + assertThat(content, containsString(name)); + testHeaders.add(endsWith(name + "/")); + } else { + assertThat(content, not(containsString(name))); + testHeaders.add(not(endsWith(name + "/"))); + } + } + + assertThat(headerValues, containsInAnyOrder(testHeaders)); + } +}