提交 b3f16489 编写于 作者: O Oleg Nenashev

[FIXED SECURITY-200] - Do not expose Api tokens to other users by default

System property can be used to restore the original behavior
上级 f53802bb
......@@ -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());
}
}
......
......@@ -21,5 +21,7 @@
# THE SOFTWARE.
ApiTokenProperty.DisplayName=API Token
ApiTokenProperty.ChangeToken.Success=<div>Updated</div>
ApiTokenProperty.ChangeToken.TokenIsHidden=Token is hidden
ApiTokenProperty.ChangeToken.Success=<div>Updated. See the new token in the field above</div>
ApiTokenProperty.ChangeToken.SuccessHidden=<div>Updated. You need to login as the user to see the token</div>
RekeySecretAdminMonitor.DisplayName=Re-keying
\ No newline at end of file
......@@ -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<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
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(), "<div>" + res.getBody().asText() + "</div>");
}
@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;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册