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; + } }