package jenkins.security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import com.gargoylesoftware.htmlunit.DownloadedContent;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.WebConnection;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebResponseData;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import com.gargoylesoftware.htmlunit.util.UrlUtils;
import hidden.jth.org.apache.http.Header;
import hidden.jth.org.apache.http.HttpEntity;
import hidden.jth.org.apache.http.HttpHost;
import hidden.jth.org.apache.http.auth.AuthScheme;
import hidden.jth.org.apache.http.auth.AuthScope;
import hidden.jth.org.apache.http.auth.Credentials;
import hidden.jth.org.apache.http.auth.UsernamePasswordCredentials;
import hidden.jth.org.apache.http.client.AuthCache;
import hidden.jth.org.apache.http.client.CredentialsProvider;
import hidden.jth.org.apache.http.client.methods.CloseableHttpResponse;
import hidden.jth.org.apache.http.client.methods.HttpGet;
import hidden.jth.org.apache.http.client.protocol.HttpClientContext;
import hidden.jth.org.apache.http.impl.auth.BasicScheme;
import hidden.jth.org.apache.http.impl.client.BasicAuthCache;
import hidden.jth.org.apache.http.impl.client.BasicCredentialsProvider;
import hidden.jth.org.apache.http.impl.client.CloseableHttpClient;
import hidden.jth.org.apache.http.impl.client.HttpClientBuilder;
import hudson.Util;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.ACLContext;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import javax.annotation.Nonnull;
import org.jvnet.hudson.test.Issue;
/**
* @author Kohsuke Kawaguchi
*/
public class ApiTokenPropertyTest {
@Rule
public JenkinsRule j = new JenkinsRule();
/**
* Tests the UI interaction and authentication.
*/
@Test
public void basics() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
User u = User.get("foo");
final ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
final String token = t.getApiToken();
// Make sure that user is able to get the token via the interface
try (ACLContext _ = ACL.as(u)) {
assertEquals("User is unable to get its own token", token, t.getApiToken());
}
// test the authentication via Token
WebClient wc = createClientForUser("foo");
assertEquals(u, wc.executeOnServer(new Callable<User>() {
public User call() throws Exception {
return User.current();
}
}));
// Make sure the UI shows the token to the user
HtmlPage config = wc.goTo(u.getUrl() + "/configure");
HtmlForm form = config.getFormByName("config");
assertEquals(token, form.getInputByName("_.apiToken").getValueAttribute());
// round-trip shouldn't change the API token
j.submit(form);
assertSame(t, u.getProperty(ApiTokenProperty.class));
}
@Test
public void security49Upgrade() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
User u = User.get("foo");
String historicalInitialValue = Util.getDigestOf(Jenkins.getInstance().getSecretKey() + ":" + u.getId());
// we won't accept historically used initial value as it may be compromised
ApiTokenProperty t = new ApiTokenProperty(historicalInitialValue);
u.addProperty(t);
String apiToken1 = t.getApiToken();
assertFalse(apiToken1.equals(Util.getDigestOf(historicalInitialValue)));
// the replacement for the compromised value must be consistent and cannot be random
ApiTokenProperty t2 = new ApiTokenProperty(historicalInitialValue);
u.addProperty(t2);
assertEquals(apiToken1,t2.getApiToken());
// any other value is OK. those are changed values
t = new ApiTokenProperty(historicalInitialValue+"somethingElse");
u.addProperty(t);
assertTrue(t.getApiToken().equals(Util.getDigestOf(historicalInitialValue+"somethingElse")));
}
@Issue("SECURITY-200")
@Test
public void adminsShouldBeUnableToSeeTokensByDefault() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
User u = User.get("foo");
final ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
final String token = t.getApiToken();
// Make sure the UI does not show the token to another user
WebClient wc = createClientForUser("bar");
HtmlPage config = wc.goTo(u.getUrl() + "/configure");
HtmlForm form = config.getFormByName("config");
assertEquals(Messages.ApiTokenProperty_ChangeToken_TokenIsHidden(), form.getInputByName("_.apiToken").getValueAttribute());
}
@Issue("SECURITY-200")
@Test
public void adminsShouldBeUnableToChangeTokensByDefault() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
User foo = User.get("foo");
User bar = User.get("bar");
final ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class);
final ApiTokenProperty.DescriptorImpl descriptor = (ApiTokenProperty.DescriptorImpl) t.getDescriptor();
// Make sure that Admin can reset a token of another user
WebClient wc = createClientForUser("bar");
HtmlPage res = wc.goTo(foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken");
assertEquals("Update token response is incorrect",
Messages.ApiTokenProperty_ChangeToken_SuccessHidden(), "<div>" + res.getBody().asText() + "</div>");
}
@Nonnull
private WebClient createClientForUser(final String username) throws Exception {
User u = User.get(username);
final ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
// Yes, we use the insecure call in the test stuff
final String token = t.getApiTokenInsecure();
WebClient wc = j.createWebClient();
wc.setCredentialsProvider(new CredentialsProvider() {
@Override
public void clear() {
// Do nothing
}
@Override
public Credentials getCredentials(AuthScope as) {
return new UsernamePasswordCredentials(username, token);
}
@Override
public void setCredentials(AuthScope as, Credentials c) {
// Ignore
}
});
CredentialsProvider provider = new BasicCredentialsProvider();
provider.setCredentials(new AuthScope("localhost", AuthScope.ANY_PORT, AuthScope.ANY_REALM),
new UsernamePasswordCredentials(username, token));
wc.setCredentialsProvider(provider);
wc.login(username);
return wc;
}
private void configureWebConnection(final WebClient wc, final String username, final String token) throws IOException {
// See https://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html
final UsernamePasswordCredentials fooCreds = new UsernamePasswordCredentials(username, token);
URL hostUrl = j.getURL();
final HttpHost targetHost = new HttpHost(hostUrl.getHost(), hostUrl.getPort(), hostUrl.getProtocol());
CredentialsProvider credsProvider = new BasicCredentialsProvider() {
@Override
public Credentials getCredentials(AuthScope authscope) {
return fooCreds;
}
};
credsProvider.setCredentials(
new AuthScope("localhost", AuthScope.ANY_PORT, AuthScope.ANY_REALM),
fooCreds);
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local auth cache
AuthScheme authScheme = new BasicScheme();
authCache.put(targetHost, authScheme);
// Add AuthCache to the execution context
final HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthCache(authCache);
wc.setCredentialsProvider(credsProvider);
// Need to create our own WebConnection that gives us control of HttpClient execution,
// allowing us to pass our own HttpClientContext etc. HttpWebConnection has its own
// private HttpClientContext instance, which means we can't authenticate properly.
wc.setWebConnection(new WebConnection() {
@Override
public WebResponse getResponse(WebRequest request) throws IOException {
try {
long startTime = System.currentTimeMillis();
HttpClientBuilder builder = HttpClientBuilder.create();
CloseableHttpClient httpClient = builder.build();
URL url = UrlUtils.encodeUrl(request.getUrl(), false, request.getCharset());
HttpGet method = new HttpGet(url.toURI());
CloseableHttpResponse response = httpClient.execute(targetHost, method, context);
HttpEntity httpEntity = response.getEntity();
DownloadedContent responseBody = HttpWebConnection.downloadContent(httpEntity.getContent(), wc.getOptions().getMaxInMemory());
String statusMessage = response.getStatusLine().getReasonPhrase();
if (statusMessage == null) {
statusMessage = "Unknown status message";
}
int statusCode = response.getStatusLine().getStatusCode();
List<NameValuePair> headers = new ArrayList<>();
for (final Header header : response.getAllHeaders()) {
headers.add(new NameValuePair(header.getName(), header.getValue()));
}
WebResponseData responseData = new WebResponseData(responseBody, statusCode, statusMessage, headers);
return new WebResponse(responseData, request, (System.currentTimeMillis() - startTime));
} catch (Exception e) {
throw new AssertionError("Failed to execute WebRequest.", e);
}
}
});
}
}