| // 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.ListMultimap; |
| import com.google.common.collect.MultimapBuilder; |
| import com.google.gerrit.audit.AuditService; |
| import com.google.gerrit.audit.RpcAuditEvent; |
| import com.google.gerrit.common.TimeUtil; |
| 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.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 java.io.IOException; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** 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 = method.getAnnotation(Audit.class); |
| if (note != null) { |
| String sid = call.getWebSession().getSessionId(); |
| CurrentUser username = call.getWebSession().getUser(); |
| ListMultimap<String, ?> args = extractParams(note, call); |
| String what = extractWhat(note, call); |
| 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 ListMultimap<String, ?> extractParams(Audit note, GerritCall call) { |
| ListMultimap<String, Object> args = MultimapBuilder.hashKeys().arrayListValues().build(); |
| |
| 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) { |
| Class<?> methodClass = call.getMethodClass(); |
| String methodClassName = methodClass != null ? methodClass.getName() : "<UNKNOWN_CLASS>"; |
| methodClassName = methodClassName.substring(methodClassName.lastIndexOf(".") + 1); |
| String what = note.action(); |
| if (what.length() == 0) { |
| what = call.getMethod().getName(); |
| } |
| |
| return methodClassName + "." + 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(); |
| } |
| 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.getUser().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; |
| } |
| } |
| } |