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:

committed by
Shawn Pearce

parent
9dd764a380
commit
7a0ba5b558
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user