Audit support for new RESTFul API.

Allow generating Audit events related to RESTFul
API execution. Structure of the AuditEvent
has been extended to support the new name-multivalue
pairs used in the new API.

This is breaking compatibility with the 2.5 API
as it changes the params data type, this is needed
anyway IMHO as the previous list of Objects was
not providing all the necessary information of
"what relates to what" in terms of parameters info.

Existing support for SSH and JSON-RPC events have
been adapted in order to fit into the new name-multivalue
syntax: this allow a generic audit plug-in to capture
all parameters regardless of where they have been
generated.

Change-Id: Ifb50dbff4eef1326b8199ea3c7324a981579547c
Signed-off-by: Luca Milanesio <luca.milanesio@gmail.com>
This commit is contained in:
Luca Milanesio
2012-11-16 19:28:22 -08:00
committed by Shawn Pearce
parent 9dd764a380
commit 7a0ba5b558
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) {