diff options
author | Brad Larson <bklarson@gmail.com> | 2012-07-25 11:41:22 -0500 |
---|---|---|
committer | Shawn O. Pearce <sop@google.com> | 2012-08-14 19:05:21 -0700 |
commit | 3a6f0779325eb457594185c093a9ca8fea0af789 (patch) | |
tree | d3833fa07ce56c7b3107bb7e5873433b675a197f | |
parent | 513e86debec00d556e360c1296773d5815c20f52 (diff) |
Tokenized REST API POST handler
POST requests typically modify server state, and are often vulnerable
to XSRF attacks. For example, a site admin could be fooled into
clicking a button on a rogue website which causes his browser to run
REST commands against the server.
To prevent against this, REST POST requests should include a token
which is first retrieved by making a GET request to the same URL.
This token allows us to verify that the user visited the Gerrit site
and helps protect against XSRF.
An example use-case of this new API:
token = $(curl --anyauth -u [user] http://review/a/rest-api | tail -n 1)
curl --anyauth -u [user] -d $token http://review/a/rest-api
Signed-off-by: Brad Larson <bklarson@gmail.com>
Change-Id: I18f3ad2b6be4df2e5a6fa3262de5a2f4601fccea
12 files changed, 575 insertions, 71 deletions
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 5606768a62..cf32d88183 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -2431,6 +2431,7 @@ Sample `etc/secure.config`: ---- [auth] registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6 + restTokenPrivateKey = 7e40PzCjlUKOnXATvcBNXH6oyiu+r0dFk2c= [database] username = webuser diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java index 4e9488c236..650cacdabb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java @@ -42,6 +42,73 @@ public class RestApi { */ private static final String JSON_MAGIC = ")]}'\n"; + private class MyRequestCallback<T extends JavaScriptObject> implements + RequestCallback { + private final boolean wasGet; + private final AsyncCallback<T> cb; + + public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) { + this.wasGet = wasGet; + this.cb = cb; + } + + @Override + public void onResponseReceived(Request req, Response res) { + int status = res.getStatusCode(); + if (status != 200) { + RpcStatus.INSTANCE.onRpcComplete(); + if ((400 <= status && status < 600) && isTextBody(res)) { + cb.onFailure(new RemoteJsonException(res.getText(), status, null)); + } else { + cb.onFailure(new StatusCodeException(status, res.getStatusText())); + } + return; + } + + if (!isJsonBody(res)) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(new RemoteJsonException("Invalid JSON")); + return; + } + + String json = res.getText(); + if (!json.startsWith(JSON_MAGIC)) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(new RemoteJsonException("Invalid JSON")); + return; + } + json = json.substring(JSON_MAGIC.length()); + + if (wasGet && json.startsWith("{\"_authkey\":")) { + RestApi.this.resendPost(cb, json); + return; + } + + T data; + try { + // javac generics bug + data = Natives.<T> parseJSON(json); + } catch (RuntimeException e) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(new RemoteJsonException("Invalid JSON")); + return; + } + + cb.onSuccess(data); + RpcStatus.INSTANCE.onRpcComplete(); + } + + @Override + public void onError(Request req, Throwable err) { + RpcStatus.INSTANCE.onRpcComplete(); + if (err.getMessage().contains("XmlHttpRequest.status")) { + cb.onFailure(new ServerUnavailableException()); + } else { + cb.onFailure(err); + } + } + } + private StringBuilder url; private boolean hasQueryParams; @@ -101,53 +168,7 @@ public class RestApi { public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) { RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString()); req.setHeader("Accept", JsonConstants.JSON_TYPE); - req.setCallback(new RequestCallback() { - @Override - public void onResponseReceived(Request req, Response res) { - RpcStatus.INSTANCE.onRpcComplete(); - int status = res.getStatusCode(); - if (status != 200) { - if ((400 <= status && status < 500) && isTextBody(res)) { - cb.onFailure(new RemoteJsonException(res.getText(), status, null)); - } else { - cb.onFailure(new StatusCodeException(status, res.getStatusText())); - } - return; - } - - if (!isJsonBody(res)) { - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; - } - - String json = res.getText(); - if (!json.startsWith(JSON_MAGIC)) { - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; - } - - T data; - try { - // javac generics bug - data = Natives.<T>parseJSON(json.substring(JSON_MAGIC.length())); - } catch (RuntimeException e) { - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; - } - - cb.onSuccess(data); - } - - @Override - public void onError(Request req, Throwable err) { - RpcStatus.INSTANCE.onRpcComplete(); - if (err.getMessage().contains("XmlHttpRequest.status")) { - cb.onFailure(new ServerUnavailableException()); - } else { - cb.onFailure(err); - } - } - }); + req.setCallback(new MyRequestCallback<T>(true, cb)); try { RpcStatus.INSTANCE.onRpcStart(); req.send(); @@ -157,6 +178,21 @@ public class RestApi { } } + private <T extends JavaScriptObject> void resendPost( + final AsyncCallback<T> cb, String token) { + RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString()); + req.setHeader("Accept", JsonConstants.JSON_TYPE); + req.setHeader("Content-Type", JsonConstants.JSON_TYPE); + req.setRequestData(token); + req.setCallback(new MyRequestCallback<T>(false, cb)); + try { + req.send(); + } catch (RequestException e) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(e); + } + } + private static boolean isJsonBody(Response res) { return isContentType(res, JsonConstants.JSON_TYPE); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java index a4217cf02e..99db2f046d 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java @@ -14,6 +14,9 @@ package com.google.gerrit.httpd; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.gerrit.extensions.annotations.RequiresCapability; @@ -38,6 +41,7 @@ import java.util.Collections; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -80,18 +84,20 @@ public abstract class RestApiServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { - noCache(res); + res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Cache-Control", "no-cache, must-revalidate"); + res.setHeader("Content-Disposition", "attachment"); + try { checkRequiresCapability(); super.service(req, res); } catch (RequireCapabilityException err) { - res.setStatus(HttpServletResponse.SC_FORBIDDEN); - noCache(res); - sendText(req, res, err.getMessage()); + sendError(res, SC_FORBIDDEN, err.getMessage()); } catch (Error err) { - handleError(err, req, res); + handleException(err, req, res); } catch (RuntimeException err) { - handleError(err, req, res); + handleException(err, req, res); } } @@ -114,16 +120,8 @@ public abstract class RestApiServlet extends HttpServlet { } } - private static void noCache(HttpServletResponse res) { - res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); - res.setHeader("Pragma", "no-cache"); - res.setHeader("Cache-Control", "no-cache, must-revalidate"); - res.setHeader("Content-Disposition", "attachment"); - } - - private static void handleError( - Throwable err, HttpServletRequest req, HttpServletResponse res) - throws IOException { + private static void handleException(Throwable err, HttpServletRequest req, + HttpServletResponse res) throws IOException { String uri = req.getRequestURI(); if (!Strings.isNullOrEmpty(req.getQueryString())) { uri += "?" + req.getQueryString(); @@ -132,12 +130,16 @@ public abstract class RestApiServlet extends HttpServlet { if (!res.isCommitted()) { res.reset(); - res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - noCache(res); - sendText(req, res, "Internal Server Error"); + sendError(res, SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); } } + protected static void sendError(HttpServletResponse res, + int statusCode, String msg) throws IOException { + res.setStatus(statusCode); + sendText(null, res, msg); + } + protected static boolean acceptsJson(HttpServletRequest req) { String accept = req.getHeader("Accept"); if (accept == null) { @@ -155,16 +157,17 @@ public abstract class RestApiServlet extends HttpServlet { return false; } - protected static void sendText(HttpServletRequest req, + protected static void sendText(@Nullable HttpServletRequest req, HttpServletResponse res, String data) throws IOException { res.setContentType("text/plain"); res.setCharacterEncoding("UTF-8"); send(req, res, data.getBytes("UTF-8")); } - protected static void send(HttpServletRequest req, HttpServletResponse res, - byte[] data) throws IOException { - if (data.length > 256 && RPCServletUtils.acceptsGzipEncoding(req)) { + protected static void send(@Nullable HttpServletRequest req, + HttpServletResponse res, byte[] data) throws IOException { + if (data.length > 256 && req != null + && RPCServletUtils.acceptsGzipEncoding(req)) { res.setHeader("Content-Encoding", "gzip"); data = HtmlDomUtil.compress(data); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java new file mode 100644 index 0000000000..783ebc70a9 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java @@ -0,0 +1,58 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.mail.RegisterNewEmailSender; + +/** Verifies the token sent by {@link RegisterNewEmailSender}. */ +public interface RestTokenVerifier { + /** + * Construct a token to verify a REST PUT request. + * + * @param user the caller that wants to make a PUT request + * @param url the URL being requested + * @return an unforgeable string to send to the user as the body of a GET + * request. Presenting the string in a follow-up POST request provides + * proof the user has the ability to read messages sent to thier + * browser and they likely aren't making the request via XSRF. + */ + public String sign(Account.Id user, String url); + + /** + * Decode a token previously created. + * + * @param user the user making the verify request. + * @param url the url user is attempting to access. + * @param token the string created by sign. + * @throws InvalidTokenException the token is invalid, expired, malformed, + * etc. + */ + public void verify(Account.Id user, String url, String token) + throws InvalidTokenException; + + /** Exception thrown when a token does not parse correctly. */ + public static class InvalidTokenException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidTokenException() { + super("Invalid token"); + } + + public InvalidTokenException(Throwable cause) { + super("Invalid token", cause); + } + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java new file mode 100644 index 0000000000..83d6caa36b --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java @@ -0,0 +1,97 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gwtjsonrpc.server.SignedToken; +import com.google.gwtjsonrpc.server.ValidToken; +import com.google.gwtjsonrpc.server.XsrfException; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; + +import org.eclipse.jgit.util.Base64; + +import java.io.UnsupportedEncodingException; + +/** Verifies the token sent by {@link RestApiServlet}. */ +public class SignedTokenRestTokenVerifier implements RestTokenVerifier { + private final SignedToken restToken; + + public static class Module extends AbstractModule { + @Override + protected void configure() { + bind(RestTokenVerifier.class).to(SignedTokenRestTokenVerifier.class); + } + } + + @Inject + SignedTokenRestTokenVerifier(AuthConfig config) { + restToken = config.getRestToken(); + } + + @Override + public String sign(Account.Id user, String url) { + try { + String payload = String.format("%s:%s", user, url); + byte[] utf8 = payload.getBytes("UTF-8"); + String base64 = Base64.encodeBytes(utf8); + return restToken.newToken(base64); + } catch (XsrfException e) { + throw new IllegalArgumentException(e); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public void verify(Account.Id user, String url, String tokenString) + throws InvalidTokenException { + ValidToken token; + try { + token = restToken.checkToken(tokenString, null); + } catch (XsrfException err) { + throw new InvalidTokenException(err); + } + if (token == null || token.getData() == null || token.getData().isEmpty()) { + throw new InvalidTokenException(); + } + + String payload; + try { + payload = new String(Base64.decode(token.getData()), "UTF-8"); + } catch (UnsupportedEncodingException err) { + throw new InvalidTokenException(err); + } + + int colonPos = payload.indexOf(':'); + if (colonPos == -1) { + throw new InvalidTokenException(); + } + + Account.Id tokenUser; + try { + tokenUser = Account.Id.parse(payload.substring(0, colonPos)); + } catch (IllegalArgumentException err) { + throw new InvalidTokenException(err); + } + + String tokenUrl = payload.substring(colonPos+1); + + if (!tokenUser.equals(user) || !tokenUrl.equals(url)) { + throw new InvalidTokenException(); + } + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java new file mode 100644 index 0000000000..98a1b57463 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java @@ -0,0 +1,263 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import com.google.common.base.Strings; +import com.google.common.collect.Iterators; +import com.google.common.collect.Maps; +import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.OutputFormat; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Enumeration; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +public abstract class TokenVerifiedRestApiServlet extends RestApiServlet { + private static final long serialVersionUID = 1L; + private static final String FORM_ENCODED = "application/x-www-form-urlencoded"; + private static final String UTF_8 = "UTF-8"; + private static final String AUTHKEY_NAME = "_authkey"; + private static final String AUTHKEY_HEADER = "X-authkey"; + + private final Gson gson; + private final Provider<CurrentUser> userProvider; + private final RestTokenVerifier verifier; + + @Inject + protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider, + RestTokenVerifier verifier) { + super(userProvider); + this.gson = OutputFormat.JSON_COMPACT.newGson(); + this.userProvider = userProvider; + this.verifier = verifier; + } + + /** + * Process the (possibly state changing) request. + * + * @param req incoming HTTP request. + * @param res outgoing response. + * @param requestData JSON object representing the HTTP request parameters. + * Null if the request body was not supplied in JSON format. + * @throws IOException + * @throws ServletException + */ + protected abstract void doRequest(HttpServletRequest req, + HttpServletResponse res, + @Nullable JsonObject requestData) throws IOException, ServletException; + + @Override + protected final void doGet(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + CurrentUser user = userProvider.get(); + if (!(user instanceof IdentifiedUser)) { + sendError(res, SC_UNAUTHORIZED, "API requires authentication"); + return; + } + + TokenInfo info = new TokenInfo(); + info._authkey = verifier.sign( + ((IdentifiedUser) user).getAccountId(), + computeUrl(req)); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + String type; + buf.write(JSON_MAGIC); + if (acceptsJson(req)) { + type = JSON_TYPE; + buf.write(gson.toJson(info).getBytes(UTF_8)); + } else { + type = FORM_ENCODED; + buf.write(String.format("%s=%s", + AUTHKEY_NAME, + URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8)); + } + + res.setContentType(type); + res.setCharacterEncoding(UTF_8); + res.setHeader("Content-Disposition", "attachment"); + send(req, res, buf.toByteArray()); + } + + @Override + protected final void doPost(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + CurrentUser user = userProvider.get(); + if (!(user instanceof IdentifiedUser)) { + sendError(res, SC_UNAUTHORIZED, "API requires authentication"); + return; + } + + ParsedBody body; + if (JSON_TYPE.equals(req.getContentType())) { + body = parseJson(req, res); + } else if (FORM_ENCODED.equals(req.getContentType())) { + body = parseForm(req, res); + } else { + sendError(res, SC_BAD_REQUEST, String.format( + "Expected Content-Type: %s or %s", + JSON_TYPE, FORM_ENCODED)); + return; + } + + if (body == null) { + return; + } + + if (Strings.isNullOrEmpty(body._authkey)) { + String h = req.getHeader(AUTHKEY_HEADER); + if (Strings.isNullOrEmpty(h)) { + sendError(res, SC_BAD_REQUEST, String.format( + "Expected %s in request body or %s in HTTP headers", + AUTHKEY_NAME, AUTHKEY_HEADER)); + return; + } + body._authkey = URLDecoder.decode(h, UTF_8); + } + + try { + verifier.verify( + ((IdentifiedUser) user).getAccountId(), + computeUrl(req), + body._authkey); + } catch (InvalidTokenException err) { + sendError(res, SC_BAD_REQUEST, + String.format("Invalid or expired %s", AUTHKEY_NAME)); + return; + } + + doRequest(body.req, res, body.json); + } + + private static ParsedBody parseJson(HttpServletRequest req, + HttpServletResponse res) throws IOException { + try { + JsonElement element = new JsonParser().parse(req.getReader()); + if (!element.isJsonObject()) { + sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body"); + return null; + } + + ParsedBody body = new ParsedBody(); + body.req = req; + body.json = (JsonObject) element; + JsonElement authKey = body.json.remove(AUTHKEY_NAME); + if (authKey != null + && authKey.isJsonPrimitive() + && authKey.getAsJsonPrimitive().isString()) { + body._authkey = authKey.getAsString(); + } + return body; + } catch (JsonParseException e) { + sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body"); + return null; + } + } + + private static ParsedBody parseForm(HttpServletRequest req, + HttpServletResponse res) throws IOException { + ParsedBody body = new ParsedBody(); + body.req = new WrappedRequest(req); + body._authkey = req.getParameter(AUTHKEY_NAME); + return body; + } + + private static String computeUrl(HttpServletRequest req) { + StringBuffer url = req.getRequestURL(); + String qs = req.getQueryString(); + if (!Strings.isNullOrEmpty(qs)) { + url.append('?').append(qs); + } + return url.toString(); + } + + private static class TokenInfo { + String _authkey; + } + + private static class ParsedBody { + HttpServletRequest req; + String _authkey; + JsonObject json; + } + + private static class WrappedRequest extends HttpServletRequestWrapper { + @SuppressWarnings("rawtypes") + private Map parameters; + + WrappedRequest(HttpServletRequest req) { + super(req); + } + + @Override + public String getParameter(String name) { + if (AUTHKEY_NAME.equals(name)) { + return null; + } + return super.getParameter(name); + } + + @Override + public String[] getParameterValues(String name) { + if (AUTHKEY_NAME.equals(name)) { + return null; + } + return super.getParameterValues(name); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Map getParameterMap() { + Map m = parameters; + if (m == null) { + m = super.getParameterMap(); + if (m.containsKey(AUTHKEY_NAME)) { + m = Maps.newHashMap(m); + m.remove(AUTHKEY_NAME); + } + parameters = m; + } + return m; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Enumeration getParameterNames() { + return Iterators.asEnumeration(getParameterMap().keySet().iterator()); + } + } +} + diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index 7d27482a29..c164d48483 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -22,6 +22,7 @@ import com.google.gerrit.httpd.CacheBasedWebSession; import com.google.gerrit.httpd.GitOverHttpModule; import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider; import com.google.gerrit.httpd.RequestContextFilter; +import com.google.gerrit.httpd.SignedTokenRestTokenVerifier; import com.google.gerrit.httpd.WebModule; import com.google.gerrit.httpd.WebSshGlueModule; import com.google.gerrit.httpd.auth.openid.OpenIdModule; @@ -294,6 +295,7 @@ public class Daemon extends SiteProgram { modules.add(new DefaultCacheFactory.Module()); modules.add(new SmtpEmailSender.Module()); modules.add(new SignedTokenEmailTokenVerifier.Module()); + modules.add(new SignedTokenRestTokenVerifier.Module()); modules.add(new PluginModule()); if (httpd) { modules.add(new CanonicalWebUrlModule() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java index f809c73308..fa4dc142b3 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java @@ -85,5 +85,9 @@ class InitAuth implements InitStep { if (auth.getSecure("registerEmailPrivateKey") == null) { auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey()); } + + if (auth.getSecure("restTokenPrivateKey") == null) { + auth.setSecure("restTokenPrivateKey", SignedToken.generateRandomKey()); + } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java index dc36988fe9..9916257ad7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java @@ -45,6 +45,7 @@ public class AuthConfig { private final String cookiePath; private final boolean cookieSecure; private final SignedToken emailReg; + private final SignedToken restToken; private final boolean allowGoogleAccountUpgrade; @@ -75,6 +76,15 @@ public class AuthConfig { emailReg = null; } + key = cfg.getString("auth", null, "restTokenPrivateKey"); + if (key != null && !key.isEmpty()) { + int age = (int) ConfigUtil.getTimeUnit(cfg, + "auth", null, "maxRestTokenAge", 60, TimeUnit.SECONDS); + restToken = new SignedToken(age, key); + } else { + restToken = null; + } + if (authType == AuthType.OPENID) { allowGoogleAccountUpgrade = cfg.getBoolean("auth", "allowgoogleaccountupgrade", false); @@ -129,6 +139,10 @@ public class AuthConfig { return emailReg; } + public SignedToken getRestToken() { + return restToken; + } + public boolean isAllowGoogleAccountUpgrade() { return allowGoogleAccountUpgrade; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index eeb0937423..5ab7b6d2b5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java @@ -32,7 +32,7 @@ import java.util.List; /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - public static final Class<Schema_71> C = Schema_71.class; + public static final Class<Schema_72> C = Schema_72.class; public static class Module extends AbstractModule { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java new file mode 100644 index 0000000000..748837b985 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java @@ -0,0 +1,25 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_72 extends SchemaVersion { + @Inject + Schema_72(Provider<Schema_71> prior) { + super(prior); + } +} diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index cc85e1e99a..1a556c2afd 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java @@ -203,6 +203,7 @@ public class WebAppInitializer extends GuiceServletContextListener { modules.add(new DefaultCacheFactory.Module()); modules.add(new SmtpEmailSender.Module()); modules.add(new SignedTokenEmailTokenVerifier.Module()); + modules.add(new SignedTokenRestTokenVerifier.Module()); modules.add(new PluginModule()); modules.add(new CanonicalWebUrlModule() { @Override |