Merge "Audit support for new RESTFul API."

This commit is contained in:
Shawn Pearce
2013-01-30 01:07:30 +00:00
committed by Gerrit Code Review
9 changed files with 323 additions and 131 deletions

View File

@@ -39,6 +39,8 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.HttpAuditEvent;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
@@ -140,14 +142,17 @@ public class RestApiServlet extends HttpServlet {
final Provider<CurrentUser> currentUser;
final Provider<WebSession> webSession;
final Provider<ParameterParser> paramParser;
final AuditService auditService;
@Inject
Globals(Provider<CurrentUser> currentUser,
Provider<WebSession> webSession,
Provider<ParameterParser> paramParser) {
Provider<ParameterParser> paramParser,
AuditService auditService) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
this.auditService = auditService;
}
}
@@ -171,12 +176,16 @@ public class RestApiServlet extends HttpServlet {
@Override
protected final void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
long auditStartTs = System.currentTimeMillis();
CacheHeaders.setNotCacheable(res);
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
Object result = null;
Multimap<String, String> params = LinkedHashMultimap.create();
Object inputRequestBody = null;
try {
int status = SC_OK;
checkUserSession(req);
List<IdString> path = splitPath(req);
@@ -260,19 +269,18 @@ public class RestApiServlet extends HttpServlet {
}
Multimap<String, String> config = LinkedHashMultimap.create();
Multimap<String, String> params = LinkedHashMultimap.create();
ParameterParser.splitQueryString(req.getQueryString(), config, params);
if (!globals.paramParser.get().parse(view, params, req, res)) {
return;
}
Object result;
if (view instanceof RestModifyView<?, ?>) {
@SuppressWarnings("unchecked")
RestModifyView<RestResource, Object> m =
(RestModifyView<RestResource, Object>) view;
result = m.apply(rsrc, parseRequest(req, inputType(m)));
inputRequestBody = parseRequest(req, inputType(m));
result = m.apply(rsrc, inputRequestBody);
} else if (view instanceof RestReadView<?>) {
result = ((RestReadView<RestResource>) view).apply(rsrc);
} else {
@@ -298,24 +306,30 @@ public class RestApiServlet extends HttpServlet {
}
}
} catch (AuthException e) {
replyError(res, SC_FORBIDDEN, e.getMessage());
replyError(res, status = SC_FORBIDDEN, e.getMessage());
} catch (BadRequestException e) {
replyError(res, SC_BAD_REQUEST, e.getMessage());
replyError(res, status = SC_BAD_REQUEST, e.getMessage());
} catch (MethodNotAllowedException e) {
replyError(res, SC_METHOD_NOT_ALLOWED, "Method not allowed");
replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
} catch (ResourceConflictException e) {
replyError(res, SC_CONFLICT, e.getMessage());
replyError(res, status = SC_CONFLICT, e.getMessage());
} catch (PreconditionFailedException e) {
replyError(res, SC_PRECONDITION_FAILED,
replyError(res, status = SC_PRECONDITION_FAILED,
Objects.firstNonNull(e.getMessage(), "Precondition failed"));
} catch (ResourceNotFoundException e) {
replyError(res, SC_NOT_FOUND, "Not found");
replyError(res, status = SC_NOT_FOUND, "Not found");
} catch (AmbiguousViewException e) {
replyError(res, SC_NOT_FOUND, e.getMessage());
replyError(res, status = SC_NOT_FOUND, e.getMessage());
} catch (JsonParseException e) {
replyError(res, SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
handleException(e, req, res);
} finally {
globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
.getSessionId(), globals.currentUser.get(), req.getRequestURI(),
auditStartTs, params, req.getMethod(), inputRequestBody, status,
result));
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (C) 2013 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 java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
class AuditedHttpServletResponse
extends HttpServletResponseWrapper
implements HttpServletResponse {
private int status;
AuditedHttpServletResponse(HttpServletResponse response) {
super(response);
}
int getStatus() {
return status;
}
@Override
public void setStatus(int sc) {
super.setStatus(sc);
this.status = sc;
}
@Override
public void setStatus(int sc, String sm) {
super.setStatus(sc, sm);
this.status = sc;
}
@Override
public void sendError(int sc) throws IOException {
super.sendError(sc);
this.status = sc;
}
@Override
public void sendError(int sc, String msg) throws IOException {
super.sendError(sc, msg);
this.status = sc;
}
@Override
public void sendRedirect(String location) throws IOException {
super.sendRedirect(location);
this.status = SC_MOVED_TEMPORARILY;
}
}

View File

@@ -14,9 +14,10 @@
package com.google.gerrit.httpd.rpc;
import com.google.common.collect.Lists;
import com.google.gerrit.audit.AuditEvent;
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;
@@ -37,11 +38,11 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
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.
*/
@@ -68,7 +69,7 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
@Override
protected GerritCall createActiveCall(final HttpServletRequest req,
final HttpServletResponse rsp) {
final GerritCall call = new GerritCall(session.get(), req, rsp);
final GerritCall call = new GerritCall(session.get(), req, new AuditedHttpServletResponse(rsp));
currentCall.set(call);
return call;
}
@@ -134,62 +135,86 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
if (note != null) {
final String sid = call.getWebSession().getSessionId();
final CurrentUser username = call.getWebSession().getCurrentUser();
final List<Object> args =
final Multimap<String, ?> args =
extractParams(note, call);
final String what = extractWhat(note, method.getName());
final String what = extractWhat(note, call);
final Object result = call.getResult();
audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
result));
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 List<Object> extractParams(final Audit note, final GerritCall call) {
List<Object> args = Lists.newArrayList(Arrays.asList(call.getParams()));
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.set(idx, "*****");
args.removeAll("$" + idx);
args.put("$" + idx, "*****");
}
return args;
}
private String extractWhat(final Audit note, final String methodName) {
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) {
boolean ccase = Character.isLowerCase(methodName.charAt(0));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < methodName.length(); i++) {
char c = methodName.charAt(i);
if (ccase && !Character.isLowerCase(c)) {
sb.append(' ');
}
sb.append(Character.toLowerCase(c));
}
what = sb.toString();
what = call.getMethod().getName();
}
return what;
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 = ActiveCall.class.getDeclaredField("result");
declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
} catch (Exception e) {
log.error("Unable to expose RPS/JSON result field");
}
return declaredField;
}
resultField = 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

View File

@@ -14,22 +14,22 @@
package com.google.gerrit.audit;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.gerrit.server.CurrentUser;
import java.util.Collections;
import java.util.List;
public class AuditEvent {
public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000";
private static final Object UNKNOWN_RESULT = "N/A";
protected static final Multimap<String, ?> EMPTY_PARAMS = HashMultimap.create();
public final String sessionId;
public final CurrentUser who;
public final long when;
public final String what;
public final List<?> params;
public final Multimap<String, ?> params;
public final Object result;
public final long timeAtStart;
public final long elapsed;
@@ -72,19 +72,6 @@ public class AuditEvent {
}
}
/**
* Creates a new audit event.
*
* @param sessionId session id the event belongs to
* @param who principal that has generated the event
* @param what object of the event
* @param params parameters of the event
*/
public AuditEvent(String sessionId, CurrentUser who, String what, List<?> params) {
this(sessionId, who, what, System.currentTimeMillis(), params,
UNKNOWN_RESULT);
}
/**
* Creates a new audit event with results
*
@@ -96,28 +83,20 @@ public class AuditEvent {
* @param result result of the event
*/
public AuditEvent(String sessionId, CurrentUser who, String what, long when,
List<?> params, Object result) {
Multimap<String, ?> params, Object result) {
Preconditions.checkNotNull(what, "what is a mandatory not null param !");
this.sessionId = getValueWithDefault(sessionId, UNKNOWN_SESSION_ID);
this.sessionId = Objects.firstNonNull(sessionId, UNKNOWN_SESSION_ID);
this.who = who;
this.what = what;
this.when = when;
this.timeAtStart = this.when;
this.params = getValueWithDefault(params, Collections.emptyList());
this.params = Objects.firstNonNull(params, EMPTY_PARAMS);
this.uuid = new UUID();
this.result = result;
this.elapsed = System.currentTimeMillis() - timeAtStart;
}
private <T> T getValueWithDefault(T value, T defaultValueIfNull) {
if (value == null) {
return defaultValueIfNull;
} else {
return value;
}
}
@Override
public int hashCode() {
return uuid.hashCode();
@@ -135,38 +114,7 @@ public class AuditEvent {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(uuid.toString());
sb.append("|");
sb.append(sessionId);
sb.append('|');
sb.append(who);
sb.append('|');
sb.append(when);
sb.append('|');
sb.append(what);
sb.append('|');
sb.append(elapsed);
sb.append('|');
if (params != null) {
sb.append('[');
for (int i = 0; i < params.size(); i++) {
if (i > 0) sb.append(',');
Object param = params.get(i);
if (param == null) {
sb.append("null");
} else {
sb.append(param);
}
}
sb.append(']');
}
sb.append('|');
if (result != null) {
sb.append(result);
}
return sb.toString();
return String.format("AuditEvent UUID:%s, SID:%s, TS:%d, who:%s, what:%s",
uuid.get(), sessionId, when, who, what);
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (C) 2013 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.audit;
import com.google.common.collect.Multimap;
import com.google.gerrit.server.CurrentUser;
public class HttpAuditEvent extends AuditEvent {
public final String httpMethod;
public final int httpStatus;
public final Object input;
/**
* Creates a new audit event with results
*
* @param sessionId session id the event belongs to
* @param who principal that has generated the event
* @param what object of the event
* @param when time-stamp of when the event started
* @param params parameters of the event
* @param result result of the event
*/
public HttpAuditEvent(String sessionId, CurrentUser who, String what, long when,
Multimap<String, ?> params, String httpMethod, Object input, int status, Object result) {
super(sessionId, who, what, when, params, result);
this.httpMethod = httpMethod;
this.input = input;
this.httpStatus = status;
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (C) 2013 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.audit;
import com.google.common.collect.Multimap;
import com.google.gerrit.server.CurrentUser;
public class RpcAuditEvent extends HttpAuditEvent {
/**
* Creates a new audit event with results
*
* @param sessionId session id the event belongs to
* @param who principal that has generated the event
* @param what object of the event
* @param when time-stamp of when the event started
* @param params parameters of the event
* @param result result of the event
*/
public RpcAuditEvent(String sessionId, CurrentUser who, String what,
long when, Multimap<String, ?> params, String httpMethod, Object input,
int status, Object result) {
super(sessionId, who, what, when, params, httpMethod, input, status, result);
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2013 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.audit;
import com.google.common.collect.Multimap;
import com.google.gerrit.server.CurrentUser;
public class SshAuditEvent extends AuditEvent {
public SshAuditEvent(String sessionId, CurrentUser who, String what,
long when, Multimap<String, ?> params, Object result) {
super(sessionId, who, what, when, params, result);
}
}

View File

@@ -196,7 +196,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
private void log(final int rc) {
if (logged.compareAndSet(false, true)) {
log.onExecute(rc);
log.onExecute(cmd, rc);
}
}

View File

@@ -14,9 +14,11 @@
package com.google.gerrit.sshd;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.audit.AuditEvent;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.SshAuditEvent;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
@@ -42,7 +44,6 @@ import org.eclipse.jgit.util.QuotedString;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
@@ -101,7 +102,7 @@ class SshLog implements LifecycleListener {
void onLogin() {
async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
audit(context.get(), "0", "LOGIN", new String[] {});
audit(context.get(), "0", "LOGIN");
}
void onAuthFail(final SshSession sd) {
@@ -127,18 +128,14 @@ class SshLog implements LifecycleListener {
}
async.append(event);
audit(null, "FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
audit(null, "FAIL", "AUTH");
}
void onExecute(int exitValue) {
void onExecute(DispatchCommand dcmd, int exitValue) {
final Context ctx = context.get();
ctx.finished = System.currentTimeMillis();
final String commandLine = ctx.getCommandLine();
String cmd = QuotedString.BOURNE.quote(commandLine);
if (cmd == commandLine) {
cmd = "'" + commandLine + "'";
}
String cmd = extractWhat(dcmd);
final LoggingEvent event = log(cmd);
event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
@@ -165,19 +162,54 @@ class SshLog implements LifecycleListener {
event.setProperty(P_STATUS, status);
async.append(event);
audit(context.get(), status, getCommand(commandLine),
CommandFactoryProvider.split(commandLine));
audit(context.get(), status, dcmd);
}
private String getCommand(String commandLine) {
commandLine = commandLine.trim();
int spacePos = commandLine.indexOf(' ');
return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
private Multimap<String, ?> extractParameters(DispatchCommand dcmd) {
String[] cmdArgs = dcmd.getArguments();
String paramName = null;
int argPos = 0;
Multimap<String, String> parms = ArrayListMultimap.create();
for (int i = 2; i < cmdArgs.length; i++) {
String arg = cmdArgs[i];
// -- stop parameters parsing
if (arg.equals("--")) {
for (i++; i < cmdArgs.length; i++) {
parms.put("$" + argPos++, cmdArgs[i]);
}
break;
}
// --param=value
int eqPos = arg.indexOf('=');
if (arg.startsWith("--") && eqPos > 0) {
parms.put(arg.substring(0, eqPos), arg.substring(eqPos + 1));
continue;
}
// -p value or --param value
if (arg.startsWith("-")) {
if (paramName != null) {
parms.put(paramName, null);
}
paramName = arg;
continue;
}
// value
if (paramName == null) {
parms.put("$" + argPos++, arg);
} else {
parms.put(paramName, arg);
paramName = null;
}
}
if (paramName != null) {
parms.put(paramName, null);
}
return parms;
}
void onLogout() {
async.append(log("LOGOUT"));
audit(context.get(), "0", "LOGOUT", new String[] {});
audit(context.get(), "0", "LOGOUT");
}
private LoggingEvent log(final String msg) {
@@ -416,21 +448,28 @@ class SshLog implements LifecycleListener {
}
}
void audit(Context ctx, Object result, String commandName, String[] args) {
void audit(Context ctx, Object result, String cmd) {
final String sid = extractSessionId(ctx);
final long created = extractCreated(ctx);
final String what = extractWhat(commandName, args);
auditService.dispatch(new AuditEvent(sid, extractCurrentUser(ctx), "ssh:"
+ what, created, Arrays.asList(args), result));
auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx), cmd,
created, null, result));
}
private String extractWhat(String commandName, String[] args) {
String result = commandName;
if ("gerrit".equals(commandName)) {
if (args.length > 1)
result = "gerrit"+"."+args[1];
void audit(Context ctx, Object result, DispatchCommand cmd) {
final String sid = extractSessionId(ctx);
final long created = extractCreated(ctx);
auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx),
extractWhat(cmd), created, extractParameters(cmd), result));
}
private String extractWhat(DispatchCommand dcmd) {
String commandName = dcmd.getCommandName();
String[] args = dcmd.getArguments();
if (args.length > 1) {
return commandName + "." + args[1];
} else {
return commandName;
}
return result;
}
private long extractCreated(final Context ctx) {