summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrad Larson <bklarson@gmail.com>2012-07-25 11:41:22 -0500
committerShawn O. Pearce <sop@google.com>2012-08-14 19:05:21 -0700
commit3a6f0779325eb457594185c093a9ca8fea0af789 (patch)
treed3833fa07ce56c7b3107bb7e5873433b675a197f
parent513e86debec00d556e360c1296773d5815c20f52 (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
-rw-r--r--Documentation/config-gerrit.txt1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java130
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java49
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java58
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java97
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java263
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java2
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java14
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java25
-rw-r--r--gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java1
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