Merge "Audit support for new RESTFul API."
This commit is contained in:
@@ -39,6 +39,8 @@ import com.google.common.collect.Maps;
|
|||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.net.HttpHeaders;
|
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.annotations.RequiresCapability;
|
||||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
import com.google.gerrit.extensions.restapi.AcceptsCreate;
|
import com.google.gerrit.extensions.restapi.AcceptsCreate;
|
||||||
@@ -140,14 +142,17 @@ public class RestApiServlet extends HttpServlet {
|
|||||||
final Provider<CurrentUser> currentUser;
|
final Provider<CurrentUser> currentUser;
|
||||||
final Provider<WebSession> webSession;
|
final Provider<WebSession> webSession;
|
||||||
final Provider<ParameterParser> paramParser;
|
final Provider<ParameterParser> paramParser;
|
||||||
|
final AuditService auditService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Globals(Provider<CurrentUser> currentUser,
|
Globals(Provider<CurrentUser> currentUser,
|
||||||
Provider<WebSession> webSession,
|
Provider<WebSession> webSession,
|
||||||
Provider<ParameterParser> paramParser) {
|
Provider<ParameterParser> paramParser,
|
||||||
|
AuditService auditService) {
|
||||||
this.currentUser = currentUser;
|
this.currentUser = currentUser;
|
||||||
this.webSession = webSession;
|
this.webSession = webSession;
|
||||||
this.paramParser = paramParser;
|
this.paramParser = paramParser;
|
||||||
|
this.auditService = auditService;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +176,16 @@ public class RestApiServlet extends HttpServlet {
|
|||||||
@Override
|
@Override
|
||||||
protected final void service(HttpServletRequest req, HttpServletResponse res)
|
protected final void service(HttpServletRequest req, HttpServletResponse res)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
long auditStartTs = System.currentTimeMillis();
|
||||||
CacheHeaders.setNotCacheable(res);
|
CacheHeaders.setNotCacheable(res);
|
||||||
res.setHeader("Content-Disposition", "attachment");
|
res.setHeader("Content-Disposition", "attachment");
|
||||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
|
int status = SC_OK;
|
||||||
|
Object result = null;
|
||||||
|
Multimap<String, String> params = LinkedHashMultimap.create();
|
||||||
|
Object inputRequestBody = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int status = SC_OK;
|
|
||||||
checkUserSession(req);
|
checkUserSession(req);
|
||||||
|
|
||||||
List<IdString> path = splitPath(req);
|
List<IdString> path = splitPath(req);
|
||||||
@@ -260,19 +269,18 @@ public class RestApiServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Multimap<String, String> config = LinkedHashMultimap.create();
|
Multimap<String, String> config = LinkedHashMultimap.create();
|
||||||
Multimap<String, String> params = LinkedHashMultimap.create();
|
|
||||||
ParameterParser.splitQueryString(req.getQueryString(), config, params);
|
ParameterParser.splitQueryString(req.getQueryString(), config, params);
|
||||||
if (!globals.paramParser.get().parse(view, params, req, res)) {
|
if (!globals.paramParser.get().parse(view, params, req, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object result;
|
|
||||||
if (view instanceof RestModifyView<?, ?>) {
|
if (view instanceof RestModifyView<?, ?>) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
RestModifyView<RestResource, Object> m =
|
RestModifyView<RestResource, Object> m =
|
||||||
(RestModifyView<RestResource, Object>) view;
|
(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<?>) {
|
} else if (view instanceof RestReadView<?>) {
|
||||||
result = ((RestReadView<RestResource>) view).apply(rsrc);
|
result = ((RestReadView<RestResource>) view).apply(rsrc);
|
||||||
} else {
|
} else {
|
||||||
@@ -298,24 +306,30 @@ public class RestApiServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (AuthException e) {
|
} catch (AuthException e) {
|
||||||
replyError(res, SC_FORBIDDEN, e.getMessage());
|
replyError(res, status = SC_FORBIDDEN, e.getMessage());
|
||||||
} catch (BadRequestException e) {
|
} catch (BadRequestException e) {
|
||||||
replyError(res, SC_BAD_REQUEST, e.getMessage());
|
replyError(res, status = SC_BAD_REQUEST, e.getMessage());
|
||||||
} catch (MethodNotAllowedException e) {
|
} 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) {
|
} catch (ResourceConflictException e) {
|
||||||
replyError(res, SC_CONFLICT, e.getMessage());
|
replyError(res, status = SC_CONFLICT, e.getMessage());
|
||||||
} catch (PreconditionFailedException e) {
|
} catch (PreconditionFailedException e) {
|
||||||
replyError(res, SC_PRECONDITION_FAILED,
|
replyError(res, status = SC_PRECONDITION_FAILED,
|
||||||
Objects.firstNonNull(e.getMessage(), "Precondition failed"));
|
Objects.firstNonNull(e.getMessage(), "Precondition failed"));
|
||||||
} catch (ResourceNotFoundException e) {
|
} catch (ResourceNotFoundException e) {
|
||||||
replyError(res, SC_NOT_FOUND, "Not found");
|
replyError(res, status = SC_NOT_FOUND, "Not found");
|
||||||
} catch (AmbiguousViewException e) {
|
} catch (AmbiguousViewException e) {
|
||||||
replyError(res, SC_NOT_FOUND, e.getMessage());
|
replyError(res, status = SC_NOT_FOUND, e.getMessage());
|
||||||
} catch (JsonParseException e) {
|
} 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) {
|
} catch (Exception e) {
|
||||||
|
status = SC_INTERNAL_SERVER_ERROR;
|
||||||
handleException(e, req, res);
|
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;
|
package com.google.gerrit.httpd.rpc;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.gerrit.audit.AuditEvent;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.gerrit.audit.AuditService;
|
import com.google.gerrit.audit.AuditService;
|
||||||
|
import com.google.gerrit.audit.RpcAuditEvent;
|
||||||
import com.google.gerrit.common.audit.Audit;
|
import com.google.gerrit.common.audit.Audit;
|
||||||
import com.google.gerrit.common.auth.SignInRequired;
|
import com.google.gerrit.common.auth.SignInRequired;
|
||||||
import com.google.gerrit.common.errors.NotSignedInException;
|
import com.google.gerrit.common.errors.NotSignedInException;
|
||||||
@@ -37,11 +38,11 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.Arrays;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base JSON servlet to ensure the current user is not forged.
|
* Base JSON servlet to ensure the current user is not forged.
|
||||||
*/
|
*/
|
||||||
@@ -68,7 +69,7 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
|
|||||||
@Override
|
@Override
|
||||||
protected GerritCall createActiveCall(final HttpServletRequest req,
|
protected GerritCall createActiveCall(final HttpServletRequest req,
|
||||||
final HttpServletResponse rsp) {
|
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);
|
currentCall.set(call);
|
||||||
return call;
|
return call;
|
||||||
}
|
}
|
||||||
@@ -134,62 +135,86 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
|
|||||||
if (note != null) {
|
if (note != null) {
|
||||||
final String sid = call.getWebSession().getSessionId();
|
final String sid = call.getWebSession().getSessionId();
|
||||||
final CurrentUser username = call.getWebSession().getCurrentUser();
|
final CurrentUser username = call.getWebSession().getCurrentUser();
|
||||||
final List<Object> args =
|
final Multimap<String, ?> args =
|
||||||
extractParams(note, call);
|
extractParams(note, call);
|
||||||
final String what = extractWhat(note, method.getName());
|
final String what = extractWhat(note, call);
|
||||||
final Object result = call.getResult();
|
final Object result = call.getResult();
|
||||||
|
|
||||||
audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
|
audit.dispatch(new RpcAuditEvent(sid, username, what, call.getWhen(),
|
||||||
result));
|
args, call.getHttpServletRequest().getMethod(), call.getHttpServletRequest().getMethod(),
|
||||||
|
((AuditedHttpServletResponse) (call.getHttpServletResponse()))
|
||||||
|
.getStatus(), result));
|
||||||
}
|
}
|
||||||
} catch (Throwable all) {
|
} catch (Throwable all) {
|
||||||
log.error("Unable to log the call", all);
|
log.error("Unable to log the call", all);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Object> extractParams(final Audit note, final GerritCall call) {
|
private Multimap<String, ?> extractParams(final Audit note, final GerritCall call) {
|
||||||
List<Object> args = Lists.newArrayList(Arrays.asList(call.getParams()));
|
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()) {
|
for (int idx : note.obfuscate()) {
|
||||||
args.set(idx, "*****");
|
args.removeAll("$" + idx);
|
||||||
|
args.put("$" + idx, "*****");
|
||||||
}
|
}
|
||||||
return args;
|
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();
|
String what = note.action();
|
||||||
if (what.length() == 0) {
|
if (what.length() == 0) {
|
||||||
boolean ccase = Character.isLowerCase(methodName.charAt(0));
|
what = call.getMethod().getName();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return what;
|
return methodClass + "." + what;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class GerritCall extends ActiveCall {
|
static class GerritCall extends ActiveCall {
|
||||||
private final WebSession session;
|
private final WebSession session;
|
||||||
private final long when;
|
private final long when;
|
||||||
private static final Field resultField;
|
private static final Field resultField;
|
||||||
|
private static final Field methodField;
|
||||||
|
|
||||||
// Needed to allow access to non-public result field in GWT/JSON-RPC
|
// Needed to allow access to non-public result field in GWT/JSON-RPC
|
||||||
static {
|
static {
|
||||||
|
resultField = getPrivateField(ActiveCall.class, "result");
|
||||||
|
methodField = getPrivateField(MethodHandle.class, "method");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field getPrivateField(Class<?> clazz, String fieldName) {
|
||||||
Field declaredField = null;
|
Field declaredField = null;
|
||||||
try {
|
try {
|
||||||
declaredField = ActiveCall.class.getDeclaredField("result");
|
declaredField = clazz.getDeclaredField(fieldName);
|
||||||
declaredField.setAccessible(true);
|
declaredField.setAccessible(true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Unable to expose RPS/JSON result field");
|
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
|
// Surrogate of the missing getResult() in GWT/JSON-RPC
|
||||||
|
|||||||
@@ -14,22 +14,22 @@
|
|||||||
|
|
||||||
package com.google.gerrit.audit;
|
package com.google.gerrit.audit;
|
||||||
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
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 com.google.gerrit.server.CurrentUser;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AuditEvent {
|
public class AuditEvent {
|
||||||
|
|
||||||
public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000";
|
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 String sessionId;
|
||||||
public final CurrentUser who;
|
public final CurrentUser who;
|
||||||
public final long when;
|
public final long when;
|
||||||
public final String what;
|
public final String what;
|
||||||
public final List<?> params;
|
public final Multimap<String, ?> params;
|
||||||
public final Object result;
|
public final Object result;
|
||||||
public final long timeAtStart;
|
public final long timeAtStart;
|
||||||
public final long elapsed;
|
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
|
* Creates a new audit event with results
|
||||||
*
|
*
|
||||||
@@ -96,28 +83,20 @@ public class AuditEvent {
|
|||||||
* @param result result of the event
|
* @param result result of the event
|
||||||
*/
|
*/
|
||||||
public AuditEvent(String sessionId, CurrentUser who, String what, long when,
|
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 !");
|
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.who = who;
|
||||||
this.what = what;
|
this.what = what;
|
||||||
this.when = when;
|
this.when = when;
|
||||||
this.timeAtStart = this.when;
|
this.timeAtStart = this.when;
|
||||||
this.params = getValueWithDefault(params, Collections.emptyList());
|
this.params = Objects.firstNonNull(params, EMPTY_PARAMS);
|
||||||
this.uuid = new UUID();
|
this.uuid = new UUID();
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.elapsed = System.currentTimeMillis() - timeAtStart;
|
this.elapsed = System.currentTimeMillis() - timeAtStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T getValueWithDefault(T value, T defaultValueIfNull) {
|
|
||||||
if (value == null) {
|
|
||||||
return defaultValueIfNull;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return uuid.hashCode();
|
return uuid.hashCode();
|
||||||
@@ -135,38 +114,7 @@ public class AuditEvent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder();
|
return String.format("AuditEvent UUID:%s, SID:%s, TS:%d, who:%s, what:%s",
|
||||||
sb.append(uuid.toString());
|
uuid.get(), sessionId, when, who, what);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
private void log(final int rc) {
|
||||||
if (logged.compareAndSet(false, true)) {
|
if (logged.compareAndSet(false, true)) {
|
||||||
log.onExecute(rc);
|
log.onExecute(cmd, rc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
|
|
||||||
package com.google.gerrit.sshd;
|
package com.google.gerrit.sshd;
|
||||||
|
|
||||||
import com.google.gerrit.extensions.events.LifecycleListener;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.gerrit.audit.AuditEvent;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.gerrit.audit.AuditService;
|
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.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.PeerDaemonUser;
|
import com.google.gerrit.server.PeerDaemonUser;
|
||||||
@@ -42,7 +44,6 @@ import org.eclipse.jgit.util.QuotedString;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
@@ -101,7 +102,7 @@ class SshLog implements LifecycleListener {
|
|||||||
|
|
||||||
void onLogin() {
|
void onLogin() {
|
||||||
async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
|
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) {
|
void onAuthFail(final SshSession sd) {
|
||||||
@@ -127,18 +128,14 @@ class SshLog implements LifecycleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async.append(event);
|
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();
|
final Context ctx = context.get();
|
||||||
ctx.finished = System.currentTimeMillis();
|
ctx.finished = System.currentTimeMillis();
|
||||||
|
|
||||||
final String commandLine = ctx.getCommandLine();
|
String cmd = extractWhat(dcmd);
|
||||||
String cmd = QuotedString.BOURNE.quote(commandLine);
|
|
||||||
if (cmd == commandLine) {
|
|
||||||
cmd = "'" + commandLine + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
final LoggingEvent event = log(cmd);
|
final LoggingEvent event = log(cmd);
|
||||||
event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
|
event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
|
||||||
@@ -165,19 +162,54 @@ class SshLog implements LifecycleListener {
|
|||||||
event.setProperty(P_STATUS, status);
|
event.setProperty(P_STATUS, status);
|
||||||
|
|
||||||
async.append(event);
|
async.append(event);
|
||||||
audit(context.get(), status, getCommand(commandLine),
|
audit(context.get(), status, dcmd);
|
||||||
CommandFactoryProvider.split(commandLine));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCommand(String commandLine) {
|
private Multimap<String, ?> extractParameters(DispatchCommand dcmd) {
|
||||||
commandLine = commandLine.trim();
|
String[] cmdArgs = dcmd.getArguments();
|
||||||
int spacePos = commandLine.indexOf(' ');
|
String paramName = null;
|
||||||
return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
|
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() {
|
void onLogout() {
|
||||||
async.append(log("LOGOUT"));
|
async.append(log("LOGOUT"));
|
||||||
audit(context.get(), "0", "LOGOUT", new String[] {});
|
audit(context.get(), "0", "LOGOUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoggingEvent log(final String msg) {
|
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 String sid = extractSessionId(ctx);
|
||||||
final long created = extractCreated(ctx);
|
final long created = extractCreated(ctx);
|
||||||
final String what = extractWhat(commandName, args);
|
auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx), cmd,
|
||||||
auditService.dispatch(new AuditEvent(sid, extractCurrentUser(ctx), "ssh:"
|
created, null, result));
|
||||||
+ what, created, Arrays.asList(args), result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractWhat(String commandName, String[] args) {
|
void audit(Context ctx, Object result, DispatchCommand cmd) {
|
||||||
String result = commandName;
|
final String sid = extractSessionId(ctx);
|
||||||
if ("gerrit".equals(commandName)) {
|
final long created = extractCreated(ctx);
|
||||||
if (args.length > 1)
|
auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx),
|
||||||
result = "gerrit"+"."+args[1];
|
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) {
|
private long extractCreated(final Context ctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user