diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java
index 124d8bd78f8c861e95406932a795245fedfbd88e..324449542ee6864a57a12dcba8b141994ca89b34 100644
--- a/core/src/main/java/jenkins/security/ApiTokenProperty.java
+++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java
@@ -29,6 +29,7 @@ import hudson.model.Descriptor.FormException;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
+import hudson.security.ACL;
import hudson.util.HttpResponses;
import hudson.util.Secret;
import jenkins.model.Jenkins;
@@ -41,6 +42,10 @@ import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.security.SecureRandom;
+import javax.annotation.Nonnull;
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Remembers the API token for this user, that can be used like a password to login.
@@ -53,6 +58,16 @@ import java.security.SecureRandom;
public class ApiTokenProperty extends UserProperty {
private volatile Secret apiToken;
+ /**
+ * If enabled, shows API tokens to users with {@link Jenkins#ADMINISTER) permissions.
+ * Disabled by default due to the security reasons.
+ * If enabled, it restores the original Jenkins behavior (SECURITY-200).
+ * @since TODO
+ */
+ private static final boolean SHOW_TOKEN_TO_ADMINS =
+ Boolean.getBoolean(ApiTokenProperty.class.getName() + ".showTokenToAdmins");
+
+
@DataBoundConstructor
public ApiTokenProperty() {
_changeApiToken();
@@ -66,7 +81,24 @@ public class ApiTokenProperty extends UserProperty {
apiToken = Secret.fromString(seed);
}
+ /**
+ * Gets the API token.
+ * The method performs security checks. Only the current user and SYSTEM may see it.
+ * Users with {@link Jenkins#ADMINISTER} may be allowed to do it using {@link #SHOW_TOKEN_TO_ADMINS}.
+ *
+ * @return API Token. Never null, but may be {@link Messages#ApiTokenProperty_ChangeToken_TokenIsHidden()}
+ * if the user has no appropriate permissions.
+ * @since TODO: the method performs security checks
+ */
+ @Nonnull
public String getApiToken() {
+ return hasPermissionToSeeToken() ? getApiTokenInsecure()
+ : Messages.ApiTokenProperty_ChangeToken_TokenIsHidden();
+ }
+
+ @Nonnull
+ @Restricted(NoExternalUse.class)
+ /*package*/ String getApiTokenInsecure() {
String p = apiToken.getPlainText();
if (p.equals(Util.getDigestOf(Jenkins.getInstance().getSecretKey()+":"+user.getId()))) {
// if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that.
@@ -77,7 +109,34 @@ public class ApiTokenProperty extends UserProperty {
}
public boolean matchesPassword(String password) {
- return getApiToken().equals(password);
+ return getApiTokenInsecure().equals(password);
+ }
+
+ private boolean hasPermissionToSeeToken() {
+ final Jenkins jenkins = Jenkins.getInstance();
+ if (jenkins == null) {
+ return false; // Should not happen - we don't display UIs in this stage
+ }
+
+ // Administrators can do whatever they want
+ if (SHOW_TOKEN_TO_ADMINS && jenkins.hasPermission(Jenkins.ADMINISTER)) {
+ return true;
+ }
+
+
+ final User current = User.current();
+ if (current == null) { // Anonymous
+ return false;
+ }
+
+ // SYSTEM user is always eligible to see tokens
+ if (Jenkins.getAuthentication() == ACL.SYSTEM) {
+ return true;
+ }
+
+ //TODO: replace by IdStrategy in newer Jenkins versions
+ //return User.idStrategy().equals(user.getId(), current.getId());
+ return StringUtils.equals(user.getId(), current.getId());
}
public void changeApiToken() throws IOException {
@@ -125,7 +184,9 @@ public class ApiTokenProperty extends UserProperty {
p.changeApiToken();
}
rsp.setHeader("script","document.getElementById('apiToken').value='"+p.getApiToken()+"'");
- return HttpResponses.html(Messages.ApiTokenProperty_ChangeToken_Success());
+ return HttpResponses.html(p.hasPermissionToSeeToken()
+ ? Messages.ApiTokenProperty_ChangeToken_Success()
+ : Messages.ApiTokenProperty_ChangeToken_SuccessHidden());
}
}
diff --git a/core/src/main/resources/jenkins/security/Messages.properties b/core/src/main/resources/jenkins/security/Messages.properties
index fc71097d03319ea6664c5325a8535c65bd252c24..e09dca3e79758277e4fc1be3e1a943366d9053c2 100644
--- a/core/src/main/resources/jenkins/security/Messages.properties
+++ b/core/src/main/resources/jenkins/security/Messages.properties
@@ -21,5 +21,7 @@
# THE SOFTWARE.
ApiTokenProperty.DisplayName=API Token
-ApiTokenProperty.ChangeToken.Success=
Updated
+ApiTokenProperty.ChangeToken.TokenIsHidden=Token is hidden
+ApiTokenProperty.ChangeToken.Success=Updated. See the new token in the field above
+ApiTokenProperty.ChangeToken.SuccessHidden=Updated. You need to login as the user to see the token
RekeySecretAdminMonitor.DisplayName=Re-keying
\ No newline at end of file
diff --git a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java
index cf4e85dcfe4314820e2768ab0f3b440d5febb225..06b88c09eef48c8573f40a0a31004cf35dc72fd6 100644
--- a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java
+++ b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java
@@ -5,6 +5,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.Util;
import hudson.model.User;
+import hudson.security.ACL;
import jenkins.model.Jenkins;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
@@ -16,6 +17,8 @@ import org.apache.commons.httpclient.auth.CredentialsProvider;
import org.jvnet.hudson.test.HudsonTestCase;
import java.util.concurrent.Callable;
+import javax.annotation.Nonnull;
+import org.jvnet.hudson.test.Issue;
/**
* @author Kohsuke Kawaguchi
@@ -27,40 +30,33 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
public void testBasics() throws Exception {
jenkins.setSecurityRealm(createDummySecurityRealm());
User u = User.get("foo");
- ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
+ final ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
final String token = t.getApiToken();
- // make sure the UI shows the token
- HtmlPage config = createWebClient().goTo(u.getUrl() + "/configure");
- HtmlForm form = config.getFormByName("config");
- assertEquals(token, form.getInputByName("_.apiToken").getValueAttribute());
-
- // round-trip shouldn't change the API token
- submit(form);
- assertSame(t, u.getProperty(ApiTokenProperty.class));
-
- WebClient wc = createWebClient();
- wc.setCredentialsProvider(new CredentialsProvider() {
- public Credentials getCredentials(AuthScheme scheme, String host, int port, boolean proxy) throws CredentialsNotAvailableException {
- return new UsernamePasswordCredentials("foo", token);
- }
- });
- wc.setWebConnection(new HttpWebConnection(wc) {
+ // Make sure that user is able to get the token via the interface
+ ACL.impersonate(u.impersonate(), new Runnable() {
@Override
- protected HttpClient getHttpClient() {
- HttpClient c = super.getHttpClient();
- c.getParams().setAuthenticationPreemptive(true);
- c.getState().setCredentials(new AuthScope("localhost", localPort, AuthScope.ANY_REALM), new UsernamePasswordCredentials("foo", token));
- return c;
+ public void run() {
+ assertEquals("User is unable to get its own token", token, t.getApiToken());
}
});
- // test the authentication
+ // test the authentication via Token
+ WebClient wc = createClientForUser("foo");
assertEquals(u,wc.executeOnServer(new Callable() {
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
+ submit(form);
+ assertSame(t, u.getProperty(ApiTokenProperty.class));
}
public void testSecurity49Upgrade() throws Exception {
@@ -85,4 +81,61 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
assertTrue(t.getApiToken().equals(Util.getDigestOf(historicalInitialValue+"somethingElse")));
}
+
+ @Issue("SECURITY-200")
+ public void testAdminsShouldBeUnableToSeeTokensByDefault() throws Exception {
+ jenkins.setSecurityRealm(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")
+ public void testAdminsShouldBeUnableToChangeTokensByDefault() throws Exception {
+ jenkins.setSecurityRealm(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(), "" + res.getBody().asText() + "
");
+ }
+
+ @Nonnull
+ private WebClient createClientForUser(final String username) {
+ 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 = createWebClient();
+ wc.setCredentialsProvider(new CredentialsProvider() {
+ @Override
+ public Credentials getCredentials(AuthScheme scheme, String host, int port, boolean proxy)
+ throws CredentialsNotAvailableException {
+ return new UsernamePasswordCredentials(username, token);
+ }
+ });
+ wc.setWebConnection(new HttpWebConnection(wc) {
+ @Override
+ protected HttpClient getHttpClient() {
+ HttpClient c = super.getHttpClient();
+ c.getParams().setAuthenticationPreemptive(true);
+ c.getState().setCredentials(new AuthScope("localhost", localPort, AuthScope.ANY_REALM),
+ new UsernamePasswordCredentials(username, token));
+ return c;
+ }
+ });
+ return wc;
+ }
}