// Copyright (C) 2008 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.rpc; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.gerrit.audit.AuditService; import com.google.gerrit.audit.RpcAuditEvent; import com.google.gerrit.common.audit.Audit; import com.google.gerrit.common.auth.SignInRequired; import com.google.gerrit.common.errors.NotSignedInException; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.util.TimeUtil; import com.google.gson.GsonBuilder; import com.google.gwtjsonrpc.common.RemoteJsonService; import com.google.gwtjsonrpc.server.ActiveCall; import com.google.gwtjsonrpc.server.JsonServlet; import com.google.gwtjsonrpc.server.MethodHandle; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Base JSON servlet to ensure the current user is not forged. */ @SuppressWarnings("serial") final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> { private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class); private static final ThreadLocal<GerritCall> currentCall = new ThreadLocal<>(); private static final ThreadLocal<MethodHandle> currentMethod = new ThreadLocal<>(); private final DynamicItem<WebSession> session; private final RemoteJsonService service; private final AuditService audit; @Inject GerritJsonServlet(final DynamicItem<WebSession> w, final RemoteJsonService s, final AuditService a) { session = w; service = s; audit = a; } @Override protected GerritCall createActiveCall(final HttpServletRequest req, final HttpServletResponse rsp) { final GerritCall call = new GerritCall(session.get(), req, new AuditedHttpServletResponse(rsp)); currentCall.set(call); return call; } @Override protected GsonBuilder createGsonBuilder() { return gerritDefaultGsonBuilder(); } private static GsonBuilder gerritDefaultGsonBuilder() { final GsonBuilder g = defaultGsonBuilder(); g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class, new org.eclipse.jgit.diff.EditDeserializer()); return g; } @Override protected void preInvoke(final GerritCall call) { super.preInvoke(call); if (call.isComplete()) { return; } if (call.getMethod().getAnnotation(SignInRequired.class) != null) { // If SignInRequired is set on this method we must have both a // valid XSRF token *and* have the user signed in. Doing these // checks also validates that they agree on the user identity. // if (!call.requireXsrfValid() || !session.get().isSignedIn()) { call.onFailure(new NotSignedInException()); } } } @Override protected Object createServiceHandle() { return service; } @Override protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws IOException { try { super.service(req, resp); } finally { audit(); currentCall.set(null); } } private void audit() { try { GerritCall call = currentCall.get(); MethodHandle method = call.getMethod(); if (method == null) { return; } Audit note = (Audit) method.getAnnotation(Audit.class); if (note != null) { final String sid = call.getWebSession().getSessionId(); final CurrentUser username = call.getWebSession().getCurrentUser(); final Multimap<String, ?> args = extractParams(note, call); final String what = extractWhat(note, call); final Object result = call.getResult(); audit.dispatch(new RpcAuditEvent(sid, username, what, call.getWhen(), args, call.getHttpServletRequest().getMethod(), call.getHttpServletRequest().getMethod(), ((AuditedHttpServletResponse) (call.getHttpServletResponse())) .getStatus(), result)); } } catch (Throwable all) { log.error("Unable to log the call", all); } } private Multimap<String, ?> extractParams(final Audit note, final GerritCall call) { Multimap<String, Object> args = ArrayListMultimap.create(); Object[] params = call.getParams(); for (int i = 0; i < params.length; i++) { args.put("$" + i, params[i]); } for (int idx : note.obfuscate()) { args.removeAll("$" + idx); args.put("$" + idx, "*****"); } return args; } private String extractWhat(final Audit note, final GerritCall call) { String methodClass = call.getMethodClass().getName(); methodClass = methodClass.substring(methodClass.lastIndexOf(".")+1); String what = note.action(); if (what.length() == 0) { what = call.getMethod().getName(); } return methodClass + "." + what; } static class GerritCall extends ActiveCall { private final WebSession session; private final long when; private static final Field resultField; private static final Field methodField; // Needed to allow access to non-public result field in GWT/JSON-RPC static { resultField = getPrivateField(ActiveCall.class, "result"); methodField = getPrivateField(MethodHandle.class, "method"); } private static Field getPrivateField(Class<?> clazz, String fieldName) { Field declaredField = null; try { declaredField = clazz.getDeclaredField(fieldName); declaredField.setAccessible(true); } catch (Exception e) { log.error("Unable to expose RPS/JSON result field"); } return declaredField; } // Surrogate of the missing getMethodClass() in GWT/JSON-RPC public Class<?> getMethodClass() { if (methodField == null) { return null; } try { Method method = (Method) methodField.get(this.getMethod()); return method.getDeclaringClass(); } catch (IllegalArgumentException e) { log.error("Cannot access result field"); } catch (IllegalAccessException e) { log.error("No permissions to access result field"); } return null; } // Surrogate of the missing getResult() in GWT/JSON-RPC public Object getResult() { if (resultField == null) { return null; } try { return resultField.get(this); } catch (IllegalArgumentException e) { log.error("Cannot access result field"); } catch (IllegalAccessException e) { log.error("No permissions to access result field"); } return null; } GerritCall(final WebSession session, final HttpServletRequest i, final HttpServletResponse o) { super(i, o); this.session = session; this.when = TimeUtil.nowMs(); } @Override public MethodHandle getMethod() { if (currentMethod.get() == null) { return super.getMethod(); } else { return currentMethod.get(); } } @Override public void onFailure(final Throwable error) { if (error instanceof IllegalArgumentException || error instanceof IllegalStateException) { super.onFailure(error); } else if (error instanceof OrmException || error instanceof RuntimeException) { onInternalFailure(error); } else { super.onFailure(error); } } @Override public boolean xsrfValidate() { final String keyIn = getXsrfKeyIn(); if (keyIn == null || "".equals(keyIn)) { // Anonymous requests don't need XSRF protection, they shouldn't // be able to cause critical state changes. // return !session.isSignedIn(); } else if (session.isSignedIn() && session.isValidXGerritAuth(keyIn)) { // The session must exist, and must be using this token. // session.getCurrentUser().setAccessPath(AccessPath.JSON_RPC); return true; } return false; } public WebSession getWebSession() { return session; } public long getWhen() { return when; } public long getElapsed() { return TimeUtil.nowMs() - when; } } }