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:
Luca Milanesio
2012-04-13 11:12:18 +01:00
committed by Edwin Kempin
parent d5e87c3aad
commit 27ba2ac5e6
20 changed files with 592 additions and 10 deletions

View File

@@ -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 {};
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.auth.userpass; 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.AsyncCallback;
import com.google.gwtjsonrpc.common.AllowCrossSiteRequest; import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
import com.google.gwtjsonrpc.common.RemoteJsonService; import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -22,6 +23,7 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0) @RpcImpl(version = Version.V2_0)
public interface UserPassAuthService extends RemoteJsonService { public interface UserPassAuthService extends RemoteJsonService {
@Audit(action = "sign in", obfuscate={1})
@AllowCrossSiteRequest @AllowCrossSiteRequest
void authenticate(String username, String password, void authenticate(String username, String password,
AsyncCallback<LoginResult> callback); AsyncCallback<LoginResult> callback);

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -33,20 +34,25 @@ public interface AccountSecurity extends RemoteJsonService {
@SignInRequired @SignInRequired
void mySshKeys(AsyncCallback<List<AccountSshKey>> callback); void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
@Audit
@SignInRequired @SignInRequired
void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback); void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
@Audit
@SignInRequired @SignInRequired
void deleteSshKeys(Set<AccountSshKey.Id> ids, void deleteSshKeys(Set<AccountSshKey.Id> ids,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void changeUserName(String newName, AsyncCallback<VoidResult> callback); void changeUserName(String newName, AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void generatePassword(AccountExternalId.Key key, void generatePassword(AccountExternalId.Key key,
AsyncCallback<AccountExternalId> callback); AsyncCallback<AccountExternalId> callback);
@Audit
@SignInRequired @SignInRequired
void clearPassword(AccountExternalId.Key key, void clearPassword(AccountExternalId.Key key,
AsyncCallback<AccountExternalId> gerritCallback); AsyncCallback<AccountExternalId> gerritCallback);
@@ -57,21 +63,26 @@ public interface AccountSecurity extends RemoteJsonService {
@SignInRequired @SignInRequired
void myGroups(AsyncCallback<List<GroupDetail>> callback); void myGroups(AsyncCallback<List<GroupDetail>> callback);
@Audit
@SignInRequired @SignInRequired
void deleteExternalIds(Set<AccountExternalId.Key> keys, void deleteExternalIds(Set<AccountExternalId.Key> keys,
AsyncCallback<Set<AccountExternalId.Key>> callback); AsyncCallback<Set<AccountExternalId.Key>> callback);
@Audit
@SignInRequired @SignInRequired
void updateContact(String fullName, String emailAddr, void updateContact(String fullName, String emailAddr,
ContactInformation info, AsyncCallback<Account> callback); ContactInformation info, AsyncCallback<Account> callback);
@Audit
@SignInRequired @SignInRequired
void enterAgreement(String agreementName, void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void registerEmail(String address, AsyncCallback<Account> callback); void registerEmail(String address, AsyncCallback<Account> callback);
@Audit
@SignInRequired @SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback); void validateEmail(String token, AsyncCallback<VoidResult> callback);
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference; import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -36,10 +37,12 @@ public interface AccountService extends RemoteJsonService {
@SignInRequired @SignInRequired
void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback); void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback);
@Audit
@SignInRequired @SignInRequired
void changePreferences(AccountGeneralPreferences pref, void changePreferences(AccountGeneralPreferences pref,
AsyncCallback<VoidResult> gerritCallback); AsyncCallback<VoidResult> gerritCallback);
@Audit
@SignInRequired @SignInRequired
void changeDiffPreferences(AccountDiffPreference diffPref, void changeDiffPreferences(AccountDiffPreference diffPref,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@@ -47,14 +50,17 @@ public interface AccountService extends RemoteJsonService {
@SignInRequired @SignInRequired
void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback); void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
@Audit
@SignInRequired @SignInRequired
void addProjectWatch(String projectName, String filter, void addProjectWatch(String projectName, String filter,
AsyncCallback<AccountProjectWatchInfo> callback); AsyncCallback<AccountProjectWatchInfo> callback);
@Audit
@SignInRequired @SignInRequired
void updateProjectWatch(AccountProjectWatch watch, void updateProjectWatch(AccountProjectWatch watch,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void deleteProjectWatches(Set<AccountProjectWatch.Key> keys, void deleteProjectWatches(Set<AccountProjectWatch.Key> keys,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.AccountDiffPreference; import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -25,12 +26,16 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0) @RpcImpl(version = Version.V2_0)
public interface ChangeDetailService extends RemoteJsonService { public interface ChangeDetailService extends RemoteJsonService {
@Audit
void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback); void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback);
@Audit
void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback); void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback);
@Audit
void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback); void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
@Audit
void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key, void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback); AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired; import com.google.gerrit.common.auth.SignInRequired;
import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService; import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -28,6 +29,7 @@ public interface ChangeListService extends RemoteJsonService {
* *
* @param req the add and remove cluster. * @param req the add and remove cluster.
*/ */
@Audit
@SignInRequired @SignInRequired
void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback); void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback);
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -24,27 +25,34 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0) @RpcImpl(version = Version.V2_0)
public interface ChangeManageService extends RemoteJsonService { public interface ChangeManageService extends RemoteJsonService {
@Audit
@SignInRequired @SignInRequired
void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback); void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void abandonChange(PatchSet.Id patchSetId, String message, void abandonChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback); AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void revertChange(PatchSet.Id patchSetId, String message, void revertChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback); AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void restoreChange(PatchSet.Id patchSetId, String message, void restoreChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback); AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback); void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback); void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback); void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude; import com.google.gerrit.reviewdb.client.AccountGroupInclude;
@@ -28,48 +29,60 @@ import java.util.Set;
@RpcImpl(version = Version.V2_0) @RpcImpl(version = Version.V2_0)
public interface GroupAdminService extends RemoteJsonService { public interface GroupAdminService extends RemoteJsonService {
@Audit
@SignInRequired @SignInRequired
void visibleGroups(AsyncCallback<GroupList> callback); void visibleGroups(AsyncCallback<GroupList> callback);
@Audit
@SignInRequired @SignInRequired
void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback); void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
@Audit
@SignInRequired @SignInRequired
void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid, void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
AsyncCallback<GroupDetail> callback); AsyncCallback<GroupDetail> callback);
@Audit
@SignInRequired @SignInRequired
void changeGroupDescription(AccountGroup.Id groupId, String description, void changeGroupDescription(AccountGroup.Id groupId, String description,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions, void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName, void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void renameGroup(AccountGroup.Id groupId, String newName, void renameGroup(AccountGroup.Id groupId, String newName,
AsyncCallback<GroupDetail> callback); AsyncCallback<GroupDetail> callback);
@Audit
@SignInRequired @SignInRequired
void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType, void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void addGroupMember(AccountGroup.Id groupId, String nameOrEmail, void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
AsyncCallback<GroupDetail> callback); AsyncCallback<GroupDetail> callback);
@Audit
@SignInRequired @SignInRequired
void addGroupInclude(AccountGroup.Id groupId, String groupName, void addGroupInclude(AccountGroup.Id groupId, String groupName,
AsyncCallback<GroupDetail> callback); AsyncCallback<GroupDetail> callback);
@Audit
@SignInRequired @SignInRequired
void deleteGroupMembers(AccountGroup.Id groupId, void deleteGroupMembers(AccountGroup.Id groupId,
Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback); Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void deleteGroupIncludes(AccountGroup.Id groupId, void deleteGroupIncludes(AccountGroup.Id groupId,
Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback); Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference; import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -34,13 +35,16 @@ import java.util.Set;
@RpcImpl(version = Version.V2_0) @RpcImpl(version = Version.V2_0)
public interface PatchDetailService extends RemoteJsonService { public interface PatchDetailService extends RemoteJsonService {
@Audit
void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b, void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback); AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
@Audit
@SignInRequired @SignInRequired
void saveDraft(PatchLineComment comment, void saveDraft(PatchLineComment comment,
AsyncCallback<PatchLineComment> callback); AsyncCallback<PatchLineComment> callback);
@Audit
@SignInRequired @SignInRequired
void deleteDraft(PatchLineComment.Key key, AsyncCallback<VoidResult> callback); 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 * change, then <code>null</code> is passed as result to
* {@link AsyncCallback#onSuccess(Object)} * {@link AsyncCallback#onSuccess(Object)}
*/ */
@Audit
@SignInRequired @SignInRequired
void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback); void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired @SignInRequired
void publishComments(PatchSet.Id psid, String message, void publishComments(PatchSet.Id psid, String message,
Set<ApprovalCategoryValue.Id> approvals, Set<ApprovalCategoryValue.Id> approvals,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired @SignInRequired
void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed, void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
AsyncCallback<ReviewerResult> callback); AsyncCallback<ReviewerResult> callback);
@Audit
@SignInRequired @SignInRequired
void removeReviewer(Change.Id id, Account.Id reviewerId, void removeReviewer(Change.Id id, Account.Id reviewerId,
AsyncCallback<ReviewerResult> callback); AsyncCallback<ReviewerResult> callback);
@@ -82,6 +90,7 @@ public interface PatchDetailService extends RemoteJsonService {
/** /**
* Update the reviewed status for the patch. * Update the reviewed status for the patch.
*/ */
@Audit
@SignInRequired @SignInRequired
void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback); void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback);
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
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.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -34,6 +35,7 @@ public interface ProjectAdminService extends RemoteJsonService {
void projectDetail(Project.NameKey projectName, void projectDetail(Project.NameKey projectName,
AsyncCallback<ProjectDetail> callback); AsyncCallback<ProjectDetail> callback);
@Audit
@SignInRequired @SignInRequired
void createNewProject(String projectName, String parentName, void createNewProject(String projectName, String parentName,
boolean emptyCommit, boolean permissionsOnly, boolean emptyCommit, boolean permissionsOnly,
@@ -42,10 +44,12 @@ public interface ProjectAdminService extends RemoteJsonService {
void projectAccess(Project.NameKey projectName, void projectAccess(Project.NameKey projectName,
AsyncCallback<ProjectAccess> callback); AsyncCallback<ProjectAccess> callback);
@Audit
@SignInRequired @SignInRequired
void changeProjectSettings(Project update, void changeProjectSettings(Project update,
AsyncCallback<ProjectDetail> callback); AsyncCallback<ProjectDetail> callback);
@Audit
@SignInRequired @SignInRequired
void changeProjectAccess(Project.NameKey projectName, String baseRevision, void changeProjectAccess(Project.NameKey projectName, String baseRevision,
String message, List<AccessSection> sections, String message, List<AccessSection> sections,
@@ -59,10 +63,12 @@ public interface ProjectAdminService extends RemoteJsonService {
void listBranches(Project.NameKey projectName, void listBranches(Project.NameKey projectName,
AsyncCallback<ListBranchesResult> callback); AsyncCallback<ListBranchesResult> callback);
@Audit
@SignInRequired @SignInRequired
void addBranch(Project.NameKey projectName, String branchName, void addBranch(Project.NameKey projectName, String branchName,
String startingRevision, AsyncCallback<ListBranchesResult> callback); String startingRevision, AsyncCallback<ListBranchesResult> callback);
@Audit
@SignInRequired @SignInRequired
void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids, void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
AsyncCallback<Set<Branch.NameKey>> callback); AsyncCallback<Set<Branch.NameKey>> callback);

View File

@@ -14,7 +14,10 @@
package com.google.gerrit.httpd; 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.common.base.Strings;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -36,19 +39,21 @@ class HttpLogoutServlet extends HttpServlet {
private final Provider<WebSession> webSession; private final Provider<WebSession> webSession;
private final Provider<String> urlProvider; private final Provider<String> urlProvider;
private final String logoutUrl; private final String logoutUrl;
private final AuditService audit;
@Inject @Inject
HttpLogoutServlet(final AuthConfig authConfig, HttpLogoutServlet(final AuthConfig authConfig,
final Provider<WebSession> webSession, final Provider<WebSession> webSession,
@CanonicalWebUrl @Nullable final Provider<String> urlProvider, @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
final AccountManager accountManager) { final AccountManager accountManager,
final AuditService audit) {
this.webSession = webSession; this.webSession = webSession;
this.urlProvider = urlProvider; this.urlProvider = urlProvider;
this.logoutUrl = authConfig.getLogoutURL(); this.logoutUrl = authConfig.getLogoutURL();
this.audit = audit;
} }
@Override private void doLogout(final HttpServletRequest req,
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException { final HttpServletResponse rsp) throws IOException {
webSession.get().logout(); webSession.get().logout();
if (logoutUrl != null) { if (logoutUrl != null) {
@@ -67,4 +72,22 @@ class HttpLogoutServlet extends HttpServlet {
rsp.sendRedirect(url); 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));
}
}
} }

View File

@@ -14,43 +14,71 @@
package com.google.gerrit.httpd.rpc; 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.auth.SignInRequired;
import com.google.gerrit.common.errors.NotSignedInException; import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.CurrentUser;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gwtjsonrpc.common.RemoteJsonService; import com.google.gwtjsonrpc.common.RemoteJsonService;
import com.google.gwtjsonrpc.server.ActiveCall; import com.google.gwtjsonrpc.server.ActiveCall;
import com.google.gwtjsonrpc.server.JsonServlet; import com.google.gwtjsonrpc.server.JsonServlet;
import com.google.gwtjsonrpc.server.MethodHandle;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; 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.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.
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> { 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 Provider<WebSession> session;
private final RemoteJsonService service; private final RemoteJsonService service;
private final AuditService audit;
@Inject @Inject
GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s) { GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s,
final AuditService a) {
session = w; session = w;
service = s; service = s;
audit = a;
} }
@Override @Override
protected GerritCall createActiveCall(final HttpServletRequest req, protected GerritCall createActiveCall(final HttpServletRequest req,
final HttpServletResponse rsp) { 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 @Override
protected GsonBuilder createGsonBuilder() { 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, g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class,
new org.eclipse.jgit.diff.EditDeserializer()); new org.eclipse.jgit.diff.EditDeserializer());
@@ -83,13 +111,113 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
return service; 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 { static class GerritCall extends ActiveCall {
private final WebSession session; 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, GerritCall(final WebSession session, final HttpServletRequest i,
final HttpServletResponse o) { final HttpServletResponse o) {
super(i, o); super(i, o);
this.session = session; this.session = session;
this.when = System.currentTimeMillis();
}
@Override
public MethodHandle getMethod() {
if (currentMethod.get() == null) {
return super.getMethod();
} else {
return currentMethod.get();
}
} }
@Override @Override
@@ -120,5 +248,18 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
return session.isSignedIn() && session.isTokenValid(keyIn); return session.isSignedIn() && session.isTokenValid(keyIn);
} }
} }
public WebSession getWebSession() {
return session;
}
public long getWhen() {
return when;
}
public long getElapsed() {
return System.currentTimeMillis() - when;
}
} }
} }

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.config;
import static com.google.inject.Scopes.SINGLETON; import static com.google.inject.Scopes.SINGLETON;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener; import com.google.gerrit.extensions.events.NewProjectCreatedListener;
@@ -167,6 +168,8 @@ public class GerritGlobalModule extends FactoryModule {
bind(ProjectControl.GenericFactory.class); bind(ProjectControl.GenericFactory.class);
factory(FunctionState.Factory.class); factory(FunctionState.Factory.class);
install(new AuditModule());
bind(GitReferenceUpdated.class); bind(GitReferenceUpdated.class);
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {}); DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class); DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);

View File

@@ -228,7 +228,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
} }
/** Split a command line into a string array. */ /** 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>(); final List<String> list = new ArrayList<String>();
boolean inquote = false; boolean inquote = false;
boolean inDblQuote = false; boolean inDblQuote = false;

View File

@@ -154,4 +154,8 @@ final class DispatchCommand extends BaseCommand {
usage.append("\n"); usage.append("\n");
return usage.toString(); return usage.toString();
} }
public String getCommandName() {
return commandName;
}
} }

View File

@@ -15,6 +15,8 @@
package com.google.gerrit.sshd; package com.google.gerrit.sshd;
import com.google.gerrit.extensions.events.LifecycleListener; 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.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;
@@ -40,6 +42,7 @@ 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;
@@ -58,12 +61,14 @@ class SshLog implements LifecycleListener {
private final Provider<SshSession> session; private final Provider<SshSession> session;
private final Provider<Context> context; private final Provider<Context> context;
private final AsyncAppender async; private final AsyncAppender async;
private final AuditService auditService;
@Inject @Inject
SshLog(final Provider<SshSession> session, final Provider<Context> context, 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.session = session;
this.context = context; this.context = context;
this.auditService = auditService;
final DailyRollingFileAppender dst = new DailyRollingFileAppender(); final DailyRollingFileAppender dst = new DailyRollingFileAppender();
dst.setName(LOG_NAME); dst.setName(LOG_NAME);
@@ -96,6 +101,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("0", "LOGIN", new String[] {});
} }
void onAuthFail(final SshSession sd) { void onAuthFail(final SshSession sd) {
@@ -121,6 +127,7 @@ class SshLog implements LifecycleListener {
} }
async.append(event); async.append(event);
audit("FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
} }
void onExecute(int exitValue) { void onExecute(int exitValue) {
@@ -158,10 +165,18 @@ class SshLog implements LifecycleListener {
event.setProperty(P_STATUS, status); event.setProperty(P_STATUS, status);
async.append(event); 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() { void onLogout() {
async.append(log("LOGOUT")); async.append(log("LOGOUT"));
audit("0", "LOGOUT", new String[] {});
} }
private LoggingEvent log(final String msg) { private LoggingEvent log(final String msg) {
@@ -192,7 +207,6 @@ class SshLog implements LifecycleListener {
} else if (user instanceof PeerDaemonUser) { } else if (user instanceof PeerDaemonUser) {
userName = PeerDaemonUser.USER_NAME; userName = PeerDaemonUser.USER_NAME;
} }
event.setProperty(P_USER_NAME, userName); event.setProperty(P_USER_NAME, userName);
@@ -400,4 +414,44 @@ class SshLog implements LifecycleListener {
public void setLogger(Logger logger) { 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;
}
}
} }