提交 578a2f5b 编写于 作者: K Kohsuke Kawaguchi

[FIXED JENKINS-9363] added API token for REST API.

上级 29038528
......@@ -60,6 +60,9 @@ Upcoming changes</a>
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-10556">issue 10556</a>)
<li class=rfe>
Record and display who aborted builds.
<li class=rfe>
Added API token support.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-9363">issue 9363</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -23,8 +23,10 @@
*/
package hudson.security;
import hudson.model.User;
import jenkins.model.Jenkins;
import hudson.util.Scrambler;
import jenkins.security.ApiTokenProperty;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -46,9 +48,9 @@ import java.net.URLEncoder;
* Implements the dual authentcation mechanism.
*
* <p>
* Hudson supports both the HTTP basic authentication and the form-based authentication.
* Jenkins supports both the HTTP basic authentication and the form-based authentication.
* The former is for scripted clients, and the latter is for humans. Unfortunately,
* becase the servlet spec does not allow us to programatically authenticate users,
* because the servlet spec does not allow us to programatically authenticate users,
* we need to rely on some hack to make it work, and this is the class that implements
* that hack.
*
......@@ -131,6 +133,21 @@ public class BasicAuthenticationFilter implements Filter {
return;
}
{// attempt to authenticate as API token
User u = User.get(username);
ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
if (t!=null && t.matchesPassword(password)) {
SecurityContextHolder.getContext().setAuthentication(u.impersonate());
try {
chain.doFilter(request,response);
} finally {
SecurityContextHolder.clearContext();
}
return;
}
}
path = req.getContextPath()+"/secured"+path;
String q = req.getQueryString();
if(q!=null)
......
package jenkins.security;
import hudson.model.User;
import hudson.util.Scrambler;
import org.acegisecurity.context.SecurityContextHolder;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* {@link Filter} that performs HTTP basic authentication based on API token.
*
* <p>
* Normally the filter chain would also contain another filter that handles BASIC
* auth with the real password. Care must be taken to ensure that this doesn't
* interfere with the other.
*
* @author Kohsuke Kawaguchi
*/
public class ApiTokenFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse rsp = (HttpServletResponse) response;
String authorization = req.getHeader("Authorization");
if (authorization!=null) {
// authenticate the user
String uidpassword = Scrambler.descramble(authorization.substring(6));
int idx = uidpassword.indexOf(':');
if (idx >= 0) {
String username = uidpassword.substring(0, idx);
String password = uidpassword.substring(idx+1);
// attempt to authenticate as API token
User u = User.get(username);
ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
if (t!=null && t.matchesPassword(password)) {
// even if we fail to match the password, we aren't rejecting it.
// as the user might be passing in a real password.
SecurityContextHolder.getContext().setAuthentication(u.impersonate());
try {
chain.doFilter(request,response);
return;
} finally {
SecurityContextHolder.clearContext();
}
}
}
}
chain.doFilter(request,response);
}
public void destroy() {
}
}
/*
* The MIT License
*
* Copyright (c) 2011, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security;
import hudson.Extension;
import hudson.Functions;
import hudson.Util;
import hudson.model.Descriptor.FormException;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.security.SecureRandom;
/**
* Remembers the API token for this user, that can be used like a password to login.
*
*
* @author Kohsuke Kawaguchi
* @see ApiTokenFilter
* @since 1.426
*/
public class ApiTokenProperty extends UserProperty {
private volatile Secret apiToken;
@DataBoundConstructor
public ApiTokenProperty() {
_changeApiToken();
}
/**
* We don't let the external code set the API token,
* but for the initial value of the token we need to compute the seed by ourselves.
*/
private ApiTokenProperty(String seed) {
apiToken = Secret.fromString(seed);
}
public String getApiToken() {
return Util.getDigestOf(apiToken.getPlainText());
}
public boolean matchesPassword(String password) {
return getApiToken().equals(password);
}
public void changeApiToken() throws IOException {
_changeApiToken();
if (user!=null)
user.save();
}
private void _changeApiToken() {
byte[] random = new byte[16]; // 16x8=128bit worth of randomness, since we use md5 digest as the API token
RANDOM.nextBytes(random);
apiToken = Secret.fromString(Util.toHexString(random));
}
@Override
public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return this;
}
@Extension
public static final class DescriptorImpl extends UserPropertyDescriptor {
public String getDisplayName() {
return "API Token";
}
/**
* When we are creating a default {@link ApiTokenProperty} for User,
* we need to make sure it yields the same value for the same user,
* because there's no guarantee that the property is saved.
*
* But we also need to make sure that an attacker won't be able to guess
* the initial API token value. So we take the seed by hasing the instance secret key + user ID.
*/
public ApiTokenProperty newInstance(User user) {
return new ApiTokenProperty(Util.getDigestOf(Jenkins.getInstance().getSecretKey() + ":" + user.getId()));
}
public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) throws IOException {
ApiTokenProperty p = u.getProperty(ApiTokenProperty.class);
if (p==null) {
p = newInstance(u);
u.addProperty(p);
} else {
p.changeApiToken();
}
rsp.setHeader("script","document.getElementById('apiToken').value='"+p.getApiToken()+"'");
return HttpResponses.html("<div>Updated</div>");
}
}
private static final SecureRandom RANDOM = new SecureRandom();
}
/*
* The MIT License
*
* Copyright (c) 2011, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.ApiTokenProperty;
f=namespace(lib.FormTagLib)
f.advanced(title:"Show API Token", align:"left") {
f.entry(title:_("API Token"), field:"apiToken") {
f.readOnlyTextbox(id:"apiToken") // TODO: need to figure out the way to do this without using ID.
}
f.validateButton(title:"Change API Token",method:"changeToken")
}
//f.entry(title:_("API Token"),field:"apiToken") {
//raw("""
//<a href="#" class='showDetails'>${_("Show API token")}</a><div style="display:none">
//""")
// f.readOnlyTextbox()
//raw("""
//</div>
//""")
//}
//
//f.validateButton(title:"") {
//
//}
//
<div>
This API token can be used for authenticating yourself in the REST API call.
See <a href="https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API">our wiki</a> for more details.
The API token should be protected like your password, as it allows other people to access Jenkins as you.
</div>
\ No newline at end of file
package jenkins.security;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.model.User;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
import org.apache.commons.httpclient.auth.CredentialsProvider;
import org.jvnet.hudson.test.HudsonTestCase;
import java.util.concurrent.Callable;
/**
* @author Kohsuke Kawaguchi
*/
public class ApiTokenPropertyTest extends HudsonTestCase {
/**
* Tests the UI interaction and authentication.
*/
public void testBasics() throws Exception {
jenkins.setSecurityRealm(createDummySecurityRealm());
User u = User.get("foo");
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) {
@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;
}
});
// test the authentication
assertEquals(u,wc.executeOnServer(new Callable<User>() {
public User call() throws Exception {
return User.current();
}
}));
}
}
......@@ -40,6 +40,7 @@ import org.acegisecurity.ui.rememberme.RememberMeProcessingFilter
import hudson.security.HttpSessionContextIntegrationFilter2
import hudson.security.SecurityRealm
import hudson.security.NoopFilter
import jenkins.security.ApiTokenFilter
// providers that apply to both patterns
def commonProviders() {
......@@ -63,6 +64,7 @@ filter(ChainedServletFilter) {
// this persists the authentication across requests by using session
bean(HttpSessionContextIntegrationFilter2) {
},
bean(ApiTokenFilter),
// allow clients to submit basic authentication credential
// but allow that to be skipped since it can interfere with reverse proxy setup
Boolean.getBoolean("jenkins.security.ignoreBasicAuth") ? bean(NoopFilter) :
......@@ -73,7 +75,7 @@ filter(ChainedServletFilter) {
// since users of basic auth tends to be a program and won't see the redirection to the form
// page as a failure
authenticationEntryPoint = bean(BasicProcessingFilterEntryPoint) {
realmName = "Hudson"
realmName = "Jenkins"
}
},
bean(AuthenticationProcessingFilter2) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册