Audit hooks on JSON/RPC and SSH commands using Gerrit plugins.
New @Audit annotation to enable invocation of AuditService injected on GerritGlobalModule. Annotation applies to JSON/RPC interfaces whilst on the SSH side audit is "hooked" directly into SshLog class. Enables the integration of Audit plugins through the implementation of audit-listeners. Dynamic loading and unloading of audit plugins is supported through the @Extension/@Listener association and automatically loaded and unloaded using Plugin module self-registration. In order to implement a new AuditListener implementation you only to: 1) Define an implementation of AuditListener and annotate as: @Listener public class MyAuditTrail extends AuditListener 2) Define a Plugin Module to bind your Audit implementation in the configure() method as: DynamicSet.bind(binder(), AuditListener.class) .to(MyAuditTrail.class); Change-Id: Iaa26c4687a4ef4cbe27fe8396a5e0b8f6627536f Signed-off-by: Luca Milanesio <luca.milanesio@gmail.com>
This commit is contained in:
committed by
Edwin Kempin
parent
d5e87c3aad
commit
27ba2ac5e6
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2012 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.common.audit;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Audit annotation for JSON/RPC interfaces.
|
||||
*
|
||||
* Flag with @Audit all the JSON/RPC methods to
|
||||
* be traced in audit-trail and submitted to the
|
||||
* AuditService.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
public @interface Audit {
|
||||
String action() default "";
|
||||
|
||||
/** List of positions of parameters to be obfuscated in audit-trail (i.e. passwords) */
|
||||
int[] obfuscate() default {};
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.auth.userpass;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
|
||||
import com.google.gwtjsonrpc.common.RemoteJsonService;
|
||||
@@ -22,6 +23,7 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
|
||||
|
||||
@RpcImpl(version = Version.V2_0)
|
||||
public interface UserPassAuthService extends RemoteJsonService {
|
||||
@Audit(action = "sign in", obfuscate={1})
|
||||
@AllowCrossSiteRequest
|
||||
void authenticate(String username, String password,
|
||||
AsyncCallback<LoginResult> callback);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
||||
@@ -33,20 +34,25 @@ public interface AccountSecurity extends RemoteJsonService {
|
||||
@SignInRequired
|
||||
void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteSshKeys(Set<AccountSshKey.Id> ids,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeUserName(String newName, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void generatePassword(AccountExternalId.Key key,
|
||||
AsyncCallback<AccountExternalId> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void clearPassword(AccountExternalId.Key key,
|
||||
AsyncCallback<AccountExternalId> gerritCallback);
|
||||
@@ -57,21 +63,26 @@ public interface AccountSecurity extends RemoteJsonService {
|
||||
@SignInRequired
|
||||
void myGroups(AsyncCallback<List<GroupDetail>> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteExternalIds(Set<AccountExternalId.Key> keys,
|
||||
AsyncCallback<Set<AccountExternalId.Key>> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void updateContact(String fullName, String emailAddr,
|
||||
ContactInformation info, AsyncCallback<Account> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void enterAgreement(String agreementName,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void registerEmail(String address, AsyncCallback<Account> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void validateEmail(String token, AsyncCallback<VoidResult> callback);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
|
||||
@@ -36,10 +37,12 @@ public interface AccountService extends RemoteJsonService {
|
||||
@SignInRequired
|
||||
void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changePreferences(AccountGeneralPreferences pref,
|
||||
AsyncCallback<VoidResult> gerritCallback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeDiffPreferences(AccountDiffPreference diffPref,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
@@ -47,14 +50,17 @@ public interface AccountService extends RemoteJsonService {
|
||||
@SignInRequired
|
||||
void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addProjectWatch(String projectName, String filter,
|
||||
AsyncCallback<AccountProjectWatchInfo> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void updateProjectWatch(AccountProjectWatch watch,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteProjectWatches(Set<AccountProjectWatch.Key> keys,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
@@ -25,12 +26,16 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
|
||||
|
||||
@RpcImpl(version = Version.V2_0)
|
||||
public interface ChangeDetailService extends RemoteJsonService {
|
||||
@Audit
|
||||
void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback);
|
||||
|
||||
@Audit
|
||||
void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
|
||||
|
||||
@Audit
|
||||
void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
|
||||
AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
import com.google.gwtjsonrpc.common.RemoteJsonService;
|
||||
@@ -28,6 +29,7 @@ public interface ChangeListService extends RemoteJsonService {
|
||||
*
|
||||
* @param req the add and remove cluster.
|
||||
*/
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
@@ -24,27 +25,34 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
|
||||
|
||||
@RpcImpl(version = Version.V2_0)
|
||||
public interface ChangeManageService extends RemoteJsonService {
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void abandonChange(PatchSet.Id patchSetId, String message,
|
||||
AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void revertChange(PatchSet.Id patchSetId, String message,
|
||||
AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void restoreChange(PatchSet.Id patchSetId, String message,
|
||||
AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
|
||||
@@ -28,48 +29,60 @@ import java.util.Set;
|
||||
|
||||
@RpcImpl(version = Version.V2_0)
|
||||
public interface GroupAdminService extends RemoteJsonService {
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void visibleGroups(AsyncCallback<GroupList> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
|
||||
AsyncCallback<GroupDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeGroupDescription(AccountGroup.Id groupId, String description,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void renameGroup(AccountGroup.Id groupId, String newName,
|
||||
AsyncCallback<GroupDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
|
||||
AsyncCallback<GroupDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addGroupInclude(AccountGroup.Id groupId, String groupName,
|
||||
AsyncCallback<GroupDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteGroupMembers(AccountGroup.Id groupId,
|
||||
Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteGroupIncludes(AccountGroup.Id groupId,
|
||||
Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
|
||||
@@ -34,13 +35,16 @@ import java.util.Set;
|
||||
|
||||
@RpcImpl(version = Version.V2_0)
|
||||
public interface PatchDetailService extends RemoteJsonService {
|
||||
@Audit
|
||||
void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
|
||||
AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void saveDraft(PatchLineComment comment,
|
||||
AsyncCallback<PatchLineComment> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteDraft(PatchLineComment.Key key, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@@ -57,18 +61,22 @@ public interface PatchDetailService extends RemoteJsonService {
|
||||
* change, then <code>null</code> is passed as result to
|
||||
* {@link AsyncCallback#onSuccess(Object)}
|
||||
*/
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void publishComments(PatchSet.Id psid, String message,
|
||||
Set<ApprovalCategoryValue.Id> approvals,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
|
||||
AsyncCallback<ReviewerResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void removeReviewer(Change.Id id, Account.Id reviewerId,
|
||||
AsyncCallback<ReviewerResult> callback);
|
||||
@@ -82,6 +90,7 @@ public interface PatchDetailService extends RemoteJsonService {
|
||||
/**
|
||||
* Update the reviewed status for the patch.
|
||||
*/
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common.data;
|
||||
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
@@ -34,6 +35,7 @@ public interface ProjectAdminService extends RemoteJsonService {
|
||||
void projectDetail(Project.NameKey projectName,
|
||||
AsyncCallback<ProjectDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void createNewProject(String projectName, String parentName,
|
||||
boolean emptyCommit, boolean permissionsOnly,
|
||||
@@ -42,10 +44,12 @@ public interface ProjectAdminService extends RemoteJsonService {
|
||||
void projectAccess(Project.NameKey projectName,
|
||||
AsyncCallback<ProjectAccess> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeProjectSettings(Project update,
|
||||
AsyncCallback<ProjectDetail> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void changeProjectAccess(Project.NameKey projectName, String baseRevision,
|
||||
String message, List<AccessSection> sections,
|
||||
@@ -59,10 +63,12 @@ public interface ProjectAdminService extends RemoteJsonService {
|
||||
void listBranches(Project.NameKey projectName,
|
||||
AsyncCallback<ListBranchesResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void addBranch(Project.NameKey projectName, String branchName,
|
||||
String startingRevision, AsyncCallback<ListBranchesResult> callback);
|
||||
|
||||
@Audit
|
||||
@SignInRequired
|
||||
void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
|
||||
AsyncCallback<Set<Branch.NameKey>> callback);
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
package com.google.gerrit.httpd;
|
||||
|
||||
import com.google.gerrit.audit.AuditEvent;
|
||||
import com.google.gerrit.audit.AuditService;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.account.AccountManager;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
@@ -36,19 +39,21 @@ class HttpLogoutServlet extends HttpServlet {
|
||||
private final Provider<WebSession> webSession;
|
||||
private final Provider<String> urlProvider;
|
||||
private final String logoutUrl;
|
||||
private final AuditService audit;
|
||||
|
||||
@Inject
|
||||
HttpLogoutServlet(final AuthConfig authConfig,
|
||||
final Provider<WebSession> webSession,
|
||||
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
|
||||
final AccountManager accountManager) {
|
||||
final AccountManager accountManager,
|
||||
final AuditService audit) {
|
||||
this.webSession = webSession;
|
||||
this.urlProvider = urlProvider;
|
||||
this.logoutUrl = authConfig.getLogoutURL();
|
||||
this.audit = audit;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(final HttpServletRequest req,
|
||||
private void doLogout(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
webSession.get().logout();
|
||||
if (logoutUrl != null) {
|
||||
@@ -67,4 +72,22 @@ class HttpLogoutServlet extends HttpServlet {
|
||||
rsp.sendRedirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
|
||||
final String sid = webSession.get().getToken();
|
||||
final CurrentUser currentUser = webSession.get().getCurrentUser();
|
||||
final String what = "sign out";
|
||||
final long when = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
doLogout(req, rsp);
|
||||
} finally {
|
||||
audit.dispatch(new AuditEvent(sid, currentUser,
|
||||
what, when, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,43 +14,71 @@
|
||||
|
||||
package com.google.gerrit.httpd.rpc;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gerrit.audit.AuditEvent;
|
||||
import com.google.gerrit.audit.AuditService;
|
||||
import com.google.gerrit.common.audit.Audit;
|
||||
import com.google.gerrit.common.auth.SignInRequired;
|
||||
import com.google.gerrit.common.errors.NotSignedInException;
|
||||
import com.google.gerrit.httpd.WebSession;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gwtjsonrpc.common.RemoteJsonService;
|
||||
import com.google.gwtjsonrpc.server.ActiveCall;
|
||||
import com.google.gwtjsonrpc.server.JsonServlet;
|
||||
import com.google.gwtjsonrpc.server.MethodHandle;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Base JSON servlet to ensure the current user is not forged.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
|
||||
private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class);
|
||||
private static final ThreadLocal<GerritCall> currentCall =
|
||||
new ThreadLocal<GerritCall>();
|
||||
private static final ThreadLocal<MethodHandle> currentMethod =
|
||||
new ThreadLocal<MethodHandle>();
|
||||
private final Provider<WebSession> session;
|
||||
private final RemoteJsonService service;
|
||||
private final AuditService audit;
|
||||
|
||||
|
||||
@Inject
|
||||
GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s) {
|
||||
GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s,
|
||||
final AuditService a) {
|
||||
session = w;
|
||||
service = s;
|
||||
audit = a;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GerritCall createActiveCall(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) {
|
||||
return new GerritCall(session.get(), req, rsp);
|
||||
final GerritCall call = new GerritCall(session.get(), req, rsp);
|
||||
currentCall.set(call);
|
||||
return call;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GsonBuilder createGsonBuilder() {
|
||||
final GsonBuilder g = super.createGsonBuilder();
|
||||
return gerritDefaultGsonBuilder();
|
||||
}
|
||||
|
||||
private static GsonBuilder gerritDefaultGsonBuilder() {
|
||||
final GsonBuilder g = defaultGsonBuilder();
|
||||
|
||||
g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class,
|
||||
new org.eclipse.jgit.diff.EditDeserializer());
|
||||
@@ -83,13 +111,113 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(final HttpServletRequest req,
|
||||
final HttpServletResponse resp) throws IOException {
|
||||
try {
|
||||
super.service(req, resp);
|
||||
} finally {
|
||||
audit();
|
||||
currentCall.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void audit() {
|
||||
try {
|
||||
GerritCall call = currentCall.get();
|
||||
Audit note = (Audit) call.getMethod().getAnnotation(Audit.class);
|
||||
if (note != null) {
|
||||
final String sid = call.getWebSession().getToken();
|
||||
final CurrentUser username = call.getWebSession().getCurrentUser();
|
||||
final List<Object> args =
|
||||
extractParams(note, call);
|
||||
final String what = extractWhat(note, call.getMethod().getName());
|
||||
final Object result = call.getResult();
|
||||
|
||||
audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
|
||||
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()));
|
||||
for (int idx : note.obfuscate()) {
|
||||
args.set(idx, "*****");
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private String extractWhat(final Audit note, final String methodName) {
|
||||
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();
|
||||
}
|
||||
|
||||
return what;
|
||||
}
|
||||
|
||||
static class GerritCall extends ActiveCall {
|
||||
private final WebSession session;
|
||||
private final long when;
|
||||
private static final Field resultField;
|
||||
|
||||
// Needed to allow access to non-public result field in GWT/JSON-RPC
|
||||
static {
|
||||
Field declaredField = null;
|
||||
try {
|
||||
declaredField = ActiveCall.class.getDeclaredField("result");
|
||||
declaredField.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to expose RPS/JSON result field");
|
||||
}
|
||||
|
||||
resultField = declaredField;
|
||||
}
|
||||
|
||||
// Surrogate of the missing getResult() in GWT/JSON-RPC
|
||||
public Object getResult() {
|
||||
if (resultField == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return resultField.get(this);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Cannot access result field");
|
||||
} catch (IllegalAccessException e) {
|
||||
log.error("No permissions to access result field");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
GerritCall(final WebSession session, final HttpServletRequest i,
|
||||
final HttpServletResponse o) {
|
||||
super(i, o);
|
||||
this.session = session;
|
||||
this.when = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodHandle getMethod() {
|
||||
if (currentMethod.get() == null) {
|
||||
return super.getMethod();
|
||||
} else {
|
||||
return currentMethod.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,5 +248,18 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
|
||||
return session.isSignedIn() && session.isTokenValid(keyIn);
|
||||
}
|
||||
}
|
||||
|
||||
public WebSession getWebSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public long getWhen() {
|
||||
return when;
|
||||
}
|
||||
|
||||
public long getElapsed() {
|
||||
return System.currentTimeMillis() - when;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2012 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.base.Preconditions;
|
||||
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";
|
||||
|
||||
public final String sessionId;
|
||||
public final CurrentUser who;
|
||||
public final long when;
|
||||
public final String what;
|
||||
public final List<?> params;
|
||||
public final Object result;
|
||||
public final long timeAtStart;
|
||||
public final long elapsed;
|
||||
public final UUID uuid;
|
||||
|
||||
public static class UUID {
|
||||
|
||||
protected final String uuid;
|
||||
|
||||
protected UUID() {
|
||||
uuid = String.format("audit:%s", java.util.UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public UUID(final String n) {
|
||||
uuid = n;
|
||||
}
|
||||
|
||||
public String get() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof UUID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return uuid.equals(((UUID) obj).uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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 AuditEvent(String sessionId, CurrentUser who, String what, long when,
|
||||
List<?> params, Object result) {
|
||||
Preconditions.checkNotNull(what, "what is a mandatory not null param !");
|
||||
|
||||
this.sessionId = getValueWithDefault(sessionId, UNKNOWN_SESSION_ID);
|
||||
this.who = who;
|
||||
this.what = what;
|
||||
this.when = when;
|
||||
this.timeAtStart = this.when;
|
||||
this.params = getValueWithDefault(params, Collections.emptyList());
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
|
||||
AuditEvent other = (AuditEvent) obj;
|
||||
return this.uuid.equals(other.uuid);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2012 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.gerrit.extensions.annotations.ExtensionPoint;
|
||||
|
||||
@ExtensionPoint
|
||||
public interface AuditListener {
|
||||
|
||||
void onAuditableAction(AuditEvent action);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2012 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.gerrit.extensions.registration.DynamicSet;
|
||||
import com.google.inject.AbstractModule;
|
||||
|
||||
public class AuditModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
DynamicSet.setOf(binder(), AuditListener.class);
|
||||
bind(AuditService.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2012 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.gerrit.extensions.registration.DynamicSet;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class AuditService {
|
||||
private final DynamicSet<AuditListener> auditListeners;
|
||||
|
||||
@Inject
|
||||
public AuditService(DynamicSet<AuditListener> auditListeners) {
|
||||
this.auditListeners = auditListeners;
|
||||
}
|
||||
|
||||
public void dispatch(AuditEvent action) {
|
||||
for (AuditListener auditListener : auditListeners) {
|
||||
auditListener.onAuditableAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package com.google.gerrit.server.config;
|
||||
import static com.google.inject.Scopes.SINGLETON;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.gerrit.audit.AuditModule;
|
||||
import com.google.gerrit.common.data.ApprovalTypes;
|
||||
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
|
||||
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
|
||||
@@ -167,6 +168,8 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
bind(ProjectControl.GenericFactory.class);
|
||||
factory(FunctionState.Factory.class);
|
||||
|
||||
install(new AuditModule());
|
||||
|
||||
bind(GitReferenceUpdated.class);
|
||||
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
|
||||
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
|
||||
|
||||
@@ -228,7 +228,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
}
|
||||
|
||||
/** Split a command line into a string array. */
|
||||
static String[] split(String commandLine) {
|
||||
static public String[] split(String commandLine) {
|
||||
final List<String> list = new ArrayList<String>();
|
||||
boolean inquote = false;
|
||||
boolean inDblQuote = false;
|
||||
|
||||
@@ -154,4 +154,8 @@ final class DispatchCommand extends BaseCommand {
|
||||
usage.append("\n");
|
||||
return usage.toString();
|
||||
}
|
||||
|
||||
public String getCommandName() {
|
||||
return commandName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package com.google.gerrit.sshd;
|
||||
|
||||
import com.google.gerrit.extensions.events.LifecycleListener;
|
||||
import com.google.gerrit.audit.AuditEvent;
|
||||
import com.google.gerrit.audit.AuditService;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.PeerDaemonUser;
|
||||
@@ -40,6 +42,7 @@ 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;
|
||||
@@ -58,12 +61,14 @@ class SshLog implements LifecycleListener {
|
||||
private final Provider<SshSession> session;
|
||||
private final Provider<Context> context;
|
||||
private final AsyncAppender async;
|
||||
private final AuditService auditService;
|
||||
|
||||
@Inject
|
||||
SshLog(final Provider<SshSession> session, final Provider<Context> context,
|
||||
final SitePaths site, @GerritServerConfig Config config) {
|
||||
final SitePaths site, @GerritServerConfig Config config, AuditService auditService) {
|
||||
this.session = session;
|
||||
this.context = context;
|
||||
this.auditService = auditService;
|
||||
|
||||
final DailyRollingFileAppender dst = new DailyRollingFileAppender();
|
||||
dst.setName(LOG_NAME);
|
||||
@@ -96,6 +101,7 @@ class SshLog implements LifecycleListener {
|
||||
|
||||
void onLogin() {
|
||||
async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
|
||||
audit("0", "LOGIN", new String[] {});
|
||||
}
|
||||
|
||||
void onAuthFail(final SshSession sd) {
|
||||
@@ -121,6 +127,7 @@ class SshLog implements LifecycleListener {
|
||||
}
|
||||
|
||||
async.append(event);
|
||||
audit("FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
|
||||
}
|
||||
|
||||
void onExecute(int exitValue) {
|
||||
@@ -158,10 +165,18 @@ class SshLog implements LifecycleListener {
|
||||
event.setProperty(P_STATUS, status);
|
||||
|
||||
async.append(event);
|
||||
audit(status, getCommand(commandLine), CommandFactoryProvider.split(commandLine));
|
||||
}
|
||||
|
||||
private String getCommand(String commandLine) {
|
||||
commandLine = commandLine.trim();
|
||||
int spacePos = commandLine.indexOf(' ');
|
||||
return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
|
||||
}
|
||||
|
||||
void onLogout() {
|
||||
async.append(log("LOGOUT"));
|
||||
audit("0", "LOGOUT", new String[] {});
|
||||
}
|
||||
|
||||
private LoggingEvent log(final String msg) {
|
||||
@@ -192,7 +207,6 @@ class SshLog implements LifecycleListener {
|
||||
|
||||
} else if (user instanceof PeerDaemonUser) {
|
||||
userName = PeerDaemonUser.USER_NAME;
|
||||
|
||||
}
|
||||
|
||||
event.setProperty(P_USER_NAME, userName);
|
||||
@@ -400,4 +414,44 @@ class SshLog implements LifecycleListener {
|
||||
public void setLogger(Logger logger) {
|
||||
}
|
||||
}
|
||||
|
||||
void audit(Object result, String commandName, String[] args) {
|
||||
final Context ctx = context.get();
|
||||
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));
|
||||
}
|
||||
|
||||
private String extractWhat(String commandName, String[] args) {
|
||||
String result = commandName;
|
||||
if ("gerrit".equals(commandName)) {
|
||||
if (args.length > 1)
|
||||
result = "gerrit"+"."+args[1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private long extractCreated(final Context ctx) {
|
||||
return (ctx != null) ? ctx.created : System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private CurrentUser extractCurrentUser(final Context ctx) {
|
||||
if (ctx != null) {
|
||||
SshSession session = ctx.getSession();
|
||||
return (session == null) ? null : session.getCurrentUser();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractSessionId(final Context ctx) {
|
||||
if (ctx != null) {
|
||||
SshSession session = ctx.getSession();
|
||||
return (session == null) ? null : IdGenerator.format(session.getSessionId());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user