Skip to content

Commit

Permalink
[SECURITY-1029]
Browse files Browse the repository at this point in the history
Co-authored-by: Wadeck Follonier <wadeck.follonier@gmail.com>
  • Loading branch information
2 people authored and daniel-beck committed Sep 12, 2018
1 parent f7a8dae commit 612a6ef
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 8 deletions.
19 changes: 18 additions & 1 deletion src/main/java/hudson/plugins/jira/JiraSite.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import hudson.plugins.jira.model.JiraIssue;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.AccessDeniedException2;
import hudson.security.Permission;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
Expand All @@ -33,6 +35,7 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
Expand All @@ -57,6 +60,7 @@
import java.util.logging.Logger;
import java.util.regex.Pattern;

import static hudson.model.Item.CONFIGURE;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.isNotEmpty;

Expand Down Expand Up @@ -701,13 +705,26 @@ public String getDisplayName() {
/**
* Checks if the user name and password are valid.
*/
@RequirePOST
public FormValidation doValidate(@QueryParameter String url,
@QueryParameter String credentialsId,
@QueryParameter String groupVisibility,
@QueryParameter String roleVisibility,
@QueryParameter boolean useHTTPAuth,
@QueryParameter String alternativeUrl,
@QueryParameter Integer timeout) {
@QueryParameter Integer timeout,
@AncestorInPath Item item) {
boolean ok = Jenkins.getInstance().hasPermission( Jenkins.ADMINISTER);
if(!ok){
// not administer we check configure for the item if any
if (item != null) {
ok = item.hasPermission( CONFIGURE);
}
if(!ok){
throw new AccessDeniedException2( Jenkins.getAuthentication(), CONFIGURE);
}
}

url = Util.fixEmpty(url);
alternativeUrl = Util.fixEmpty(alternativeUrl);
URL mainURL, alternativeURL = null;
Expand Down
21 changes: 19 additions & 2 deletions src/test/java/hudson/plugins/jira/DescriptorImpl2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@

import com.atlassian.jira.rest.client.api.RestClientException;
import com.atlassian.jira.rest.client.api.domain.Permissions;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.plugins.jira.JiraSite.JiraSiteBuilder;
import hudson.search.Search;
import hudson.search.SearchIndex;
import hudson.security.ACL;
import hudson.security.Permission;
import hudson.util.FormValidation;
import org.acegisecurity.AccessDeniedException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.jvnet.hudson.test.JenkinsRule;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.util.Collection;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
Expand All @@ -28,6 +42,9 @@ public class DescriptorImpl2Test {

JiraSession jiraSession = mock(JiraSession.class);

@Rule
public JenkinsRule r = new JenkinsRule();

@Rule
public final ExpectedException exception = ExpectedException.none();

Expand All @@ -41,7 +58,7 @@ public void prepareMocks() {
@Test
public void testValidateConnectionError() throws Exception {
when(jiraSession.getMyPermissions()).thenThrow(RestClientException.class);
FormValidation validation = descriptor.doValidate("http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT);
FormValidation validation = descriptor.doValidate("http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, r.createFreeStyleProject());

verify(descriptor).getJiraSiteBuilder();
verify(builder).build();
Expand All @@ -52,7 +69,7 @@ public void testValidateConnectionError() throws Exception {
@Test
public void testValidateConnectionOK() throws Exception {
when(jiraSession.getMyPermissions()).thenReturn(mock(Permissions.class));
FormValidation validation = descriptor.doValidate("http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT);
FormValidation validation = descriptor.doValidate("http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, r.createFreeStyleProject());

verify(descriptor).getJiraSiteBuilder();
verify(builder).build();
Expand Down
9 changes: 4 additions & 5 deletions src/test/java/hudson/plugins/jira/DescriptorImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,18 @@ public class DescriptorImplTest {

JiraSite.DescriptorImpl descriptor = new JiraSite.DescriptorImpl();

@WithoutJenkins
@Test
public void testDoValidate() throws Exception {
FormValidation validation = descriptor.doValidate(null, null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT);
FormValidation validation = descriptor.doValidate(null, null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT,r.createFreeStyleProject());
assertEquals(FormValidation.Kind.ERROR, validation.kind);

validation = descriptor.doValidate("invalid", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT);
validation = descriptor.doValidate("invalid", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT,r.createFreeStyleProject());
assertEquals(FormValidation.Kind.ERROR, validation.kind);

validation = descriptor.doValidate("http://valid/", null, null, null, false, "invalid", JiraSite.DEFAULT_TIMEOUT);
validation = descriptor.doValidate("http://valid/", null, null, null, false, "invalid", JiraSite.DEFAULT_TIMEOUT,r.createFreeStyleProject());
assertEquals(FormValidation.Kind.ERROR, validation.kind);

validation = descriptor.doValidate("http://valid/", null, null, null, false, " ", JiraSite.DEFAULT_TIMEOUT);
validation = descriptor.doValidate("http://valid/", null, null, null, false, " ", JiraSite.DEFAULT_TIMEOUT,r.createFreeStyleProject());
assertEquals(FormValidation.Kind.ERROR, validation.kind);
}

Expand Down
265 changes: 265 additions & 0 deletions src/test/java/hudson/plugins/jira/JiraSiteSecurity1029Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package hudson.plugins.jira;

import com.cloudbees.hudson.plugins.folder.Folder;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import hudson.model.Item;
import hudson.model.TopLevelItem;
import hudson.model.User;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

public class JiraSiteSecurity1029Test {

@Rule
public JenkinsRule j = new JenkinsRule();
{j.timeout = 0;}

private Server server;
private URI serverUri;
private FakeJiraServlet servlet;

@Test
public void cannotLeakCredentials() throws Exception {
setupServer();

final String ADMIN = "admin";
final String USER = "user";
final String USER_FOLDER_CONFIGURE = "folder_configure";

j.jenkins.setCrumbIssuer(null);
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER).everywhere().to(ADMIN)
.grant(Jenkins.READ, Item.READ).everywhere().to(USER)
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE).everywhere().to(USER_FOLDER_CONFIGURE)
);

String credId_1 = "cred-1-id";
String credId_2 = "cred-2-id";

String pwd1 = "pwd1";
String pwd2 = "pwd2";

UsernamePasswordCredentialsImpl cred1 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_1, null, "user1", pwd1);
UsernamePasswordCredentialsImpl cred2 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_2, null, "user2", pwd2);

SystemCredentialsProvider systemProvider = SystemCredentialsProvider.getInstance();
systemProvider.getCredentials().add(cred1);
systemProvider.getCredentials().add(cred2);
systemProvider.save();

User admin = User.getById(ADMIN, true);
User user = User.getById(USER, true);
User userFolderConfigure = User.getById(USER_FOLDER_CONFIGURE, true);

{ // as an admin I should be able to validate my url / credentials
JenkinsRule.WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
wc.withBasicApiToken(admin);

String jiraSiteValidateUrl = j.getURL() + "descriptorByName/" + JiraSite.class.getName() + "/validate";
WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST);
request.setRequestParameters(Arrays.asList(
new NameValuePair("url", serverUri.toString()),
new NameValuePair("credentialsId", credId_1),
new NameValuePair("useHTTPAuth", "true")
));

Page page = wc.getPage(request);
assertThat(page.getWebResponse().getStatusCode(), equalTo(200));
assertThat(servlet.getPasswordAndReset(), equalTo(pwd1));
}
{ // as an user with just read access, I may not be able to leak any credentials
JenkinsRule.WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
wc.withBasicApiToken(user);

String jiraSiteValidateUrl = j.getURL() + "descriptorByName/" + JiraSite.class.getName() + "/validate";
WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST);
request.setRequestParameters(Arrays.asList(
new NameValuePair("url", serverUri.toString()),
new NameValuePair("credentialsId", credId_2),
new NameValuePair("useHTTPAuth", "true")
));

Page page = wc.getPage(request);
// to avoid trouble, we always validate when the user has not the good permission
assertThat(page.getWebResponse().getStatusCode(), equalTo(403));
assertThat(servlet.getPasswordAndReset(), nullValue());

}

{ // as an user with just read access, I may not be able to leak any credentials in folder
Folder folder = j.jenkins.createProject(Folder.class, "folder" + j.jenkins.getItems().size());

JenkinsRule.WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
wc.withBasicApiToken(user);

String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl()
+ "descriptorByName/" + JiraSite.class.getName() + "/validate";

WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST);
request.setRequestParameters(Arrays.asList(
new NameValuePair("url", serverUri.toString()),
new NameValuePair("credentialsId", credId_2),
new NameValuePair("useHTTPAuth", "true")
));

Page page = wc.getPage(request);
// to avoid trouble, we always validate when the user has not the good permission
assertThat(page.getWebResponse().getStatusCode(), equalTo(403));
assertThat(servlet.getPasswordAndReset(), nullValue());
}

{ // as an user with configure access, I can access
Folder folder = j.jenkins.createProject(Folder.class, "folder" + j.jenkins.getItems().size());

JenkinsRule.WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
wc.withBasicApiToken(userFolderConfigure);

String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl()
+ "descriptorByName/" + JiraSite.class.getName() + "/validate";

WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST);
request.setRequestParameters(Arrays.asList(
new NameValuePair("url", serverUri.toString()),
new NameValuePair("credentialsId", credId_2),
new NameValuePair("useHTTPAuth", "true")
));

Page page = wc.getPage(request);
// to avoid trouble, we always validate when the user has the good permission
assertThat(page.getWebResponse().getStatusCode(), equalTo(200));
assertThat(servlet.getPasswordAndReset(), equalTo(pwd2));
}
}

public void setupServer() throws Exception {
server = new Server();
ServerConnector connector = new ServerConnector(server);
// auto-bind to available port
connector.setPort(0);
server.addConnector(connector);

servlet = new FakeJiraServlet(j);

ServletContextHandler context = new ServletContextHandler();
ServletHolder servletHolder = new ServletHolder("default", servlet);
context.addServlet(servletHolder, "/*");
server.setHandler(context);

server.start();

String host = connector.getHost();
if (host == null) {
host = "localhost";
}

int port = connector.getLocalPort();
serverUri = new URI(String.format("http://%s:%d/", host, port));
servlet.setServerUrl(serverUri);
}

@After
public void stopEmbeddedJettyServer() {
try {
server.stop();
} catch (Exception e) {
e.printStackTrace();
}
}

private static class FakeJiraServlet extends DefaultServlet {

private JenkinsRule jenkinsRule;
private URI serverUri;

private String pwdCollected;

public FakeJiraServlet(JenkinsRule jenkinsRule) {
this.jenkinsRule = jenkinsRule;
}

public void setServerUrl(URI serverUri) {
this.serverUri = serverUri;
}

public String getPasswordAndReset() {
String result = pwdCollected;
this.pwdCollected = null;
return result;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getRequestURL().toString();
String relativePath = path.substring(this.serverUri.toString().length());

String authBasicBase64 = req.getHeader("Authorization");
String authBase64 = authBasicBase64.substring("Basic ".length());
String auth = new String(Base64.getDecoder().decode(authBase64), StandardCharsets.UTF_8);
String[] authArray = auth.split(":");
String user = authArray[0];
String pwd = authArray[1];

this.pwdCollected = pwd;

switch (relativePath) {
case "rest/api/latest/mypermissions":
myPermissions(req, resp);
break;
}
}

private void myPermissions(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Object body = new HashMap<String, Object>() {{
put("permissions", new HashMap<String, Object>() {{
put("perm_1", new HashMap<String, Object>() {{
put("id", 1);
put("key", "perm_key");
put("name", "perm_name");
put("description", null);
put("havePermission", "true");
}});
}}
);
}};

resp.getWriter().write(JSONObject.fromObject(body).toString());
}
}

}

0 comments on commit 612a6ef

Please sign in to comment.