Merge changes from topic 'group-audit-log'

* changes:
  Add new group screen that shows the audit log of the group
  Add integration test for listing group audit events
  Allow to get audit log of a group through GroupApi
  Add new REST endpoint that provides the audit log of a group
This commit is contained in:
David Pursehouse 2015-07-14 08:57:10 +00:00 committed by Gerrit Code Review
commit 07e25e2dec
18 changed files with 672 additions and 0 deletions

View File

@ -592,6 +592,89 @@ describes the new owner group.
}
----
[[get-audit-log]]
=== Get Audit Log
--
'GET /groups/link:#group-id[\{group-id\}]/log.audit'
--
Gets the audit log of a Gerrit internal group.
.Request
----
GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/log.audit HTTP/1.0
----
As response a list of link:#group-audit-event-info[GroupAuditEventInfo]
entities is returned that describes the audit events of the group. The
returned audit events are sorted by date in reverse order so that the
newest audit event comes first.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
[
{
"member": {
"url": "#/admin/groups/uuid-fdda826a0815859ab48d22a05a43472f0f55f89a",
"options": {},
"group_id": 3,
"owner": "Administrators",
"owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a",
"id": "fdda826a0815859ab48d22a05a43472f0f55f89a",
"name": "MyGroup"
},
"type": "REMOVE_GROUP",
"user": {
"_account_id": 1000000,
"name": "Administrator",
"email": "admin@example.com",
"username": "admin"
},
"date": "2015-07-03 09:22:26.348000000"
},
{
"member": {
"url": "#/admin/groups/uuid-fdda826a0815859ab48d22a05a43472f0f55f89a",
"options": {},
"group_id": 3,
"owner": "Administrators",
"owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a",
"id": "fdda826a0815859ab48d22a05a43472f0f55f89a",
"name": "MyGroup"
},
"type": "ADD_GROUP",
"user": {
"_account_id": 1000000,
"name": "Administrator",
"email": "admin@example.com",
"username": "admin"
},
"date": "2015-07-03 08:43:36.592000000"
},
{
"member": {
"_account_id": 1000000,
"name": "Administrator",
"email": "admin@example.com",
"username": "admin"
},
"type": "ADD_USER",
"user": {
"_account_id": 1000001,
"name": "John Doe",
"email": "john.doe@example.com",
"username": "jdoe"
},
"date": "2015-07-01 13:36:36.602000000"
}
]
----
[[group-member-endpoints]]
== Group Member Endpoints
@ -1108,6 +1191,38 @@ Group name that uniquely identifies one group.
[[json-entities]]
== JSON Entities
[[group-audit-event-info]]
=== GroupAuditEventInfo
The `GroupAuditEventInfo` entity contains information about an audit
event of a group.
[options="header",cols="1,6"]
|======================
|Field Name|Description
|`member` |
The group member that is added/removed. If `type` is `ADD_USER` or
`REMOVE_USER` the member is returned as detailed
link:rest-api-accounts.html#account-info[AccountInfo] entity, if `type`
is `ADD_GROUP` or `REMOVE_GROUP` the member is returned as
link:#group-info[GroupInfo] entity.
|`type` |
The event type, can be: `ADD_USER`, `REMOVE_USER`, `ADD_GROUP` or
`REMOVE_GROUP`.
`ADD_USER`: A user was added as member to the group.
`REMOVE_USER`: A user member was removed from the group.
`ADD_GROUP`: A group was included as member in the group.
`REMOVE_GROUP`: An included group was removed from the group.
|`user` |
The user that did the add/remove as detailed
link:rest-api-accounts.html#account-info[AccountInfo] entity.
|`date` |
The timestamp of the event.
|======================
[[group-info]]
=== GroupInfo
The `GroupInfo` entity contains information about a group. This can be

View File

@ -27,8 +27,13 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@ -36,11 +41,13 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
import org.junit.Test;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -422,6 +429,62 @@ public class GroupsIT extends AbstractDaemonTest {
assertGroupInfo(adminGroup, Iterables.getOnlyElement(groups.values()));
}
@Test
public void getAuditLog() throws Exception {
GroupApi g = gApi.groups().create(name("group"));
List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(1);
assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
g.addMembers(user.username);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(2);
assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
g.removeMembers(user.username);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(3);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
String otherGroup = name("otherGroup");
gApi.groups().create(otherGroup);
g.addGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(4);
assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
g.removeGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(5);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
Timestamp lastDate = null;
for (GroupAuditEventInfo auditEvent : auditEvents) {
if (lastDate != null) {
assertThat(lastDate).isGreaterThan(auditEvent.date);
}
lastDate = auditEvent.date;
}
}
private void assertAuditEvent(GroupAuditEventInfo info, Type expectedType,
Account.Id expectedUser, Account.Id expectedMember) {
assertThat(info.user._accountId).isEqualTo(expectedUser.get());
assertThat(info.type).isEqualTo(expectedType);
assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class);
assertThat(((UserMemberAuditEventInfo) info).member._accountId).isEqualTo(
expectedMember.get());
}
private void assertAuditEvent(GroupAuditEventInfo info, Type expectedType,
Account.Id expectedUser, String expectedMemberGroupName) {
assertThat(info.user._accountId).isEqualTo(expectedUser.get());
assertThat(info.type).isEqualTo(expectedType);
assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class);
assertThat(((GroupMemberAuditEventInfo) info).member.name).isEqualTo(
expectedMemberGroupName);
}
private void assertMembers(String group, TestAccount... expectedMembers)
throws Exception {
assertMembers(

View File

@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.groups;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
@ -132,4 +133,12 @@ public interface GroupApi {
* @throws RestApiException
*/
void removeGroups(String... groups) throws RestApiException;
/**
* Returns the audit log of the group.
*
* @return list of audit events of the group.
* @throws RestApiException
*/
List<? extends GroupAuditEventInfo> auditLog() throws RestApiException;
}

View File

@ -0,0 +1,74 @@
// Copyright (C) 2015 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.extensions.common;
import java.sql.Timestamp;
public abstract class GroupAuditEventInfo {
public enum Type {
ADD_USER, REMOVE_USER, ADD_GROUP, REMOVE_GROUP
}
public Type type;
public AccountInfo user;
public Timestamp date;
public static UserMemberAuditEventInfo createAddUserEvent(AccountInfo user,
Timestamp date, AccountInfo member) {
return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
}
public static UserMemberAuditEventInfo createRemoveUserEvent(
AccountInfo user, Timestamp date, AccountInfo member) {
return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
}
public static GroupMemberAuditEventInfo createAddGroupEvent(AccountInfo user,
Timestamp date, GroupInfo member) {
return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
}
public static GroupMemberAuditEventInfo createRemoveGroupEvent(
AccountInfo user, Timestamp date, GroupInfo member) {
return new GroupMemberAuditEventInfo(Type.REMOVE_GROUP, user, date, member);
}
protected GroupAuditEventInfo(Type type, AccountInfo user,
Timestamp date) {
this.type = type;
this.user = user;
this.date = date;
}
public static class UserMemberAuditEventInfo extends GroupAuditEventInfo {
public AccountInfo member;
public UserMemberAuditEventInfo(Type type, AccountInfo user,
Timestamp date, AccountInfo member) {
super(type, user, date);
this.member = member;
}
}
public static class GroupMemberAuditEventInfo extends GroupAuditEventInfo {
public GroupInfo member;
public GroupMemberAuditEventInfo(Type type, AccountInfo user,
Timestamp date, GroupInfo member) {
super(type, user, date);
this.member = member;
}
}
}

View File

@ -50,6 +50,7 @@ import com.google.gerrit.client.account.MyWatchedProjectsScreen;
import com.google.gerrit.client.account.NewAgreementScreen;
import com.google.gerrit.client.account.RegisterScreen;
import com.google.gerrit.client.account.ValidateEmailScreen;
import com.google.gerrit.client.admin.AccountGroupAuditLogScreen;
import com.google.gerrit.client.admin.AccountGroupInfoScreen;
import com.google.gerrit.client.admin.AccountGroupMembersScreen;
import com.google.gerrit.client.admin.AccountGroupScreen;
@ -831,6 +832,8 @@ public class Dispatcher {
Gerrit.display(token, new AccountGroupInfoScreen(group, token));
} else if (AccountGroupScreen.MEMBERS.equals(panel)) {
Gerrit.display(token, new AccountGroupMembersScreen(group, token));
} else if (AccountGroupScreen.AUDIT_LOG.equals(panel)) {
Gerrit.display(token, new AccountGroupAuditLogScreen(group, token));
} else {
Gerrit.display(token, new NotFoundScreen());
}

View File

@ -0,0 +1,153 @@
// Copyright (C) 2015 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.client.admin;
import static com.google.gerrit.client.FormatUtil.mediumFormat;
import static com.google.gerrit.client.FormatUtil.name;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.groups.GroupApi;
import com.google.gerrit.client.groups.GroupAuditEventInfo;
import com.google.gerrit.client.groups.GroupInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import java.util.List;
public class AccountGroupAuditLogScreen extends AccountGroupScreen {
private AuditEventTable auditEventTable;
public AccountGroupAuditLogScreen(GroupInfo toShow, String token) {
super(toShow, token);
}
@Override
protected void onInitUI() {
super.onInitUI();
add(new SmallHeading(Util.C.headingAuditLog()));
auditEventTable = new AuditEventTable();
add(auditEventTable);
}
@Override
protected void display(GroupInfo group, boolean canModify) {
GroupApi.getAuditLog(group.getGroupUUID(),
new GerritCallback<JsArray<GroupAuditEventInfo>>() {
@Override
public void onSuccess(JsArray<GroupAuditEventInfo> result) {
auditEventTable.display(Natives.asList(result));
}
});
}
private class AuditEventTable extends FancyFlexTable<GroupAuditEventInfo> {
AuditEventTable() {
table.setText(0, 1, Util.C.columnDate());
table.setText(0, 2, Util.C.columnType());
table.setText(0, 3, Util.C.columnMember());
table.setText(0, 4, Util.C.columnByUser());
FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
}
void display(List<GroupAuditEventInfo> auditEvents) {
while (1 < table.getRowCount()) {
table.removeRow(table.getRowCount() - 1);
}
for (GroupAuditEventInfo auditEvent : auditEvents) {
int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
populate(row, auditEvent);
}
}
void populate(int row, GroupAuditEventInfo auditEvent) {
FlexCellFormatter fmt = table.getFlexCellFormatter();
table.setText(row, 1, mediumFormat(auditEvent.date()));
switch (auditEvent.type()) {
case ADD_USER:
case ADD_GROUP:
table.setText(row, 2, Util.C.typeAdded());
break;
case REMOVE_USER:
case REMOVE_GROUP:
table.setText(row, 2, Util.C.typeRemoved());
break;
}
switch (auditEvent.type()) {
case ADD_USER:
case REMOVE_USER:
table.setText(row, 3, formatAccount(auditEvent.memberAsUser()));
break;
case ADD_GROUP:
case REMOVE_GROUP:
GroupInfo member = auditEvent.memberAsGroup();
if (AccountGroup.isInternalGroup(member.getGroupUUID())) {
table.setWidget(row, 3,
new Hyperlink(member.name(),
Dispatcher.toGroup(member.getGroupUUID())));
fmt.getElement(row, 3).setTitle(null);
} else if (member.url() != null) {
Anchor a = new Anchor();
a.setText(member.name());
a.setHref(member.url());
a.setTitle("UUID " + member.getGroupUUID().get());
table.setWidget(row, 3, a);
fmt.getElement(row, 3).setTitle(null);
} else {
table.setText(row, 3, member.name());
fmt.getElement(row, 3).setTitle(
"UUID " + member.getGroupUUID().get());
}
break;
}
table.setText(row, 4, formatAccount(auditEvent.user()));
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
setRowItem(row, auditEvent);
}
}
private static String formatAccount(AccountInfo account) {
StringBuilder b = new StringBuilder();
b.append(name(account));
b.append(" (");
b.append(account._accountId());
b.append(")");
return b.toString();
}
}

View File

@ -25,15 +25,20 @@ import com.google.gerrit.reviewdb.client.AccountGroup;
public abstract class AccountGroupScreen extends MenuScreen {
public static final String INFO = "info";
public static final String MEMBERS = "members";
public static final String AUDIT_LOG = "audit-log";
private final GroupInfo group;
private final String token;
private final String membersTabToken;
private final String auditLogTabToken;
public AccountGroupScreen(final GroupInfo toShow, final String token) {
setRequiresSignIn(true);
this.group = toShow;
this.token = token;
this.membersTabToken = getTabToken(token, MEMBERS);
this.auditLogTabToken = getTabToken(token, AUDIT_LOG);
link(Util.C.groupTabGeneral(), getTabToken(token, INFO));
link(Util.C.groupTabMembers(), membersTabToken,
@ -56,6 +61,11 @@ public abstract class AccountGroupScreen extends MenuScreen {
GroupApi.isGroupOwner(group.name(), new GerritCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
if (result) {
link(Util.C.groupTabAuditLog(), auditLogTabToken,
AccountGroup.isInternalGroup(group.getGroupUUID()));
setToken(token);
}
display(group, result);
}
});

View File

@ -68,6 +68,7 @@ public interface AdminConstants extends Constants {
String headingParentProjectName();
String columnProjectName();
String headingAgreements();
String headingAuditLog();
String headingProjectSubmitType();
String projectSubmitType_FAST_FORWARD_ONLY();
@ -89,6 +90,13 @@ public interface AdminConstants extends Constants {
String columnGroupNotifications();
String columnGroupVisibleToAll();
String columnDate();
String columnType();
String columnByUser();
String typeAdded();
String typeRemoved();
String columnBranchName();
String columnBranchRevision();
String initialRevision();
@ -104,6 +112,7 @@ public interface AdminConstants extends Constants {
String createGroupTitle();
String groupTabGeneral();
String groupTabMembers();
String groupTabAuditLog();
String projectListTitle();
String projectFilter();
String createProjectTitle();

View File

@ -47,6 +47,7 @@ noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For
headingExternalGroup = Selected External Group
headingCreateGroup = Create New Group
headingAgreements = Contributor Agreements
headingAuditLog = Audit Log
headingProjectSubmitType = Submit Type
projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
@ -68,6 +69,13 @@ columnGroupType = Group Type
columnGroupNotifications = Email Only Authors
columnGroupVisibleToAll = Visible To All
columnDate = Date
columnType = Type
columnByUser = By User
typeAdded = Added
typeRemoved = Removed
columnBranchName = Branch Name
columnBranchRevision = Revision
initialRevision = Initial Revision
@ -83,6 +91,7 @@ groupFilter = Filter
createGroupTitle = Create Group
groupTabGeneral = General
groupTabMembers = Members
groupTabAuditLog = Audit Log
projectListTitle = Projects
projectFilter = Filter
createProjectTitle = Create Project

View File

@ -193,6 +193,12 @@ public class GroupApi {
}
}
/** Get audit log of a group. */
public static void getAuditLog(AccountGroup.UUID group,
AsyncCallback<JsArray<GroupAuditEventInfo>> cb) {
group(group).view("log.audit").get(cb);
}
private static RestApi members(AccountGroup.UUID group) {
return group(group).view("members");
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2015 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.client.groups;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
import java.sql.Timestamp;
public class GroupAuditEventInfo extends JavaScriptObject {
public enum Type {
ADD_USER, REMOVE_USER, ADD_GROUP, REMOVE_GROUP
}
public final Timestamp date() {
return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
}
public final Type type() {
return Type.valueOf(typeRaw());
}
public final native AccountInfo user() /*-{ return this.user; }-*/;
public final native AccountInfo memberAsUser() /*-{ return this.member; }-*/;
public final native GroupInfo memberAsGroup() /*-{ return this.member; }-*/;
private final native String dateRaw() /*-{ return this.date; }-*/;
private final native String typeRaw() /*-{ return this.type; }-*/;
protected GroupAuditEventInfo() {
}
}

View File

@ -98,4 +98,16 @@ public final class AccountGroupByIdAud {
removedBy = deleter;
removedOn = when;
}
public Account.Id getAddedBy() {
return addedBy;
}
public Account.Id getRemovedBy() {
return removedBy;
}
public Timestamp getRemovedOn() {
return removedOn;
}
}

View File

@ -103,4 +103,16 @@ public final class AccountGroupMemberAudit {
removedBy = addedBy;
removedOn = key.addedOn;
}
public Account.Id getAddedBy() {
return addedBy;
}
public Account.Id getRemovedBy() {
return removedBy;
}
public Timestamp getRemovedOn() {
return removedOn;
}
}

View File

@ -32,4 +32,8 @@ public interface AccountGroupByIdAudAccess extends
@Query("WHERE key.groupId = ? AND key.includeUUID = ?")
ResultSet<AccountGroupByIdAud> byGroupInclude(AccountGroup.Id groupId,
AccountGroup.UUID incGroupUUID) throws OrmException;
@Query("WHERE key.groupId = ?")
ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId)
throws OrmException;
}

View File

@ -33,4 +33,8 @@ public interface AccountGroupMemberAuditAccess extends
@Query("WHERE key.groupId = ? AND key.accountId = ?")
ResultSet<AccountGroupMemberAudit> byGroupAccount(AccountGroup.Id groupId,
Account.Id accountId) throws OrmException;
@Query("WHERE key.groupId = ?")
ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId)
throws OrmException;
}

View File

@ -17,6 +17,7 @@ package com.google.gerrit.server.api.groups;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@ -25,6 +26,7 @@ import com.google.gerrit.server.group.AddIncludedGroups;
import com.google.gerrit.server.group.AddMembers;
import com.google.gerrit.server.group.DeleteIncludedGroups;
import com.google.gerrit.server.group.DeleteMembers;
import com.google.gerrit.server.group.GetAuditLog;
import com.google.gerrit.server.group.GetDescription;
import com.google.gerrit.server.group.GetDetail;
import com.google.gerrit.server.group.GetGroup;
@ -67,6 +69,7 @@ class GroupApiImpl implements GroupApi {
private final ListIncludedGroups listGroups;
private final AddIncludedGroups addGroups;
private final DeleteIncludedGroups deleteGroups;
private final GetAuditLog getAuditLog;
private final GroupResource rsrc;
@AssistedInject
@ -87,6 +90,7 @@ class GroupApiImpl implements GroupApi {
ListIncludedGroups listGroups,
AddIncludedGroups addGroups,
DeleteIncludedGroups deleteGroups,
GetAuditLog getAuditLog,
@Assisted GroupResource rsrc) {
this.getGroup = getGroup;
this.getDetail = getDetail;
@ -104,6 +108,7 @@ class GroupApiImpl implements GroupApi {
this.listGroups = listGroups;
this.addGroups = addGroups;
this.deleteGroups = deleteGroups;
this.getAuditLog = getAuditLog;
this.rsrc = rsrc;
}
@ -258,4 +263,12 @@ class GroupApiImpl implements GroupApi {
}
}
@Override
public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException {
try {
return getAuditLog.apply(rsrc);
} catch (OrmException e) {
throw new RestApiException("Cannot get audit log", e);
}
}
}

View File

@ -0,0 +1,130 @@
// Copyright (C) 2015 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.server.group;
import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GroupCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@Singleton
public class GetAuditLog implements RestReadView<GroupResource> {
private final Provider<ReviewDb> db;
private final AccountLoader.Factory accountLoaderFactory;
private final GroupCache groupCache;
private final GroupJson groupJson;
@Inject
public GetAuditLog(Provider<ReviewDb> db,
AccountLoader.Factory accountLoaderFactory,
GroupCache groupCache,
GroupJson groupJson) {
this.db = db;
this.accountLoaderFactory = accountLoaderFactory;
this.groupCache = groupCache;
this.groupJson = groupJson;
}
@Override
public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
throws AuthException, ResourceNotFoundException,
MethodNotAllowedException, OrmException {
if (rsrc.toAccountGroup() == null) {
throw new MethodNotAllowedException();
} else if (!rsrc.getControl().isOwner()) {
throw new AuthException("Not group owner");
}
AccountGroup group = db.get().accountGroups().get(
rsrc.toAccountGroup().getId());
if (group == null) {
throw new ResourceNotFoundException();
}
AccountLoader accountLoader = accountLoaderFactory.create(true);
List<GroupAuditEventInfo> auditEvents = new ArrayList<>();
for (AccountGroupMemberAudit auditEvent :
db.get().accountGroupMembersAudit().byGroup(group.getId()).toList()) {
AccountInfo member = accountLoader.get(auditEvent.getKey().getParentKey());
auditEvents.add(GroupAuditEventInfo.createAddUserEvent(
accountLoader.get(auditEvent.getAddedBy()),
auditEvent.getKey().getAddedOn(), member));
if (!auditEvent.isActive()) {
auditEvents.add(GroupAuditEventInfo.createRemoveUserEvent(
accountLoader.get(auditEvent.getRemovedBy()),
auditEvent.getRemovedOn(), member));
}
}
for (AccountGroupByIdAud auditEvent :
db.get().accountGroupByIdAud().byGroup(group.getId()).toList()) {
AccountGroup.UUID includedGroupUUID = auditEvent.getKey().getIncludeUUID();
AccountGroup includedGroup = groupCache.get(includedGroupUUID);
GroupInfo member;
if (includedGroup != null) {
member = groupJson.format(GroupDescriptions.forAccountGroup(includedGroup));
} else {
member = new GroupInfo();
member.id = Url.encode(includedGroupUUID.get());
}
auditEvents.add(GroupAuditEventInfo.createAddGroupEvent(
accountLoader.get(auditEvent.getAddedBy()),
auditEvent.getKey().getAddedOn(), member));
if (!auditEvent.isActive()) {
auditEvents.add(GroupAuditEventInfo.createRemoveGroupEvent(
accountLoader.get(auditEvent.getRemovedBy()),
auditEvent.getRemovedOn(), member));
}
}
accountLoader.fill();
// sort by date in reverse order so that the newest audit event comes first
Collections.sort(auditEvents, new Comparator<GroupAuditEventInfo>() {
@Override
public int compare(GroupAuditEventInfo e1, GroupAuditEventInfo e2) {
return e2.date.compareTo(e1.date);
}
});
return auditEvents;
}
}

View File

@ -55,6 +55,7 @@ public class Module extends RestApiModule {
put(GROUP_KIND, "owner").to(PutOwner.class);
get(GROUP_KIND, "options").to(GetOptions.class);
put(GROUP_KIND, "options").to(PutOptions.class);
get(GROUP_KIND, "log.audit").to(GetAuditLog.class);
child(GROUP_KIND, "members").to(MembersCollection.class);
get(MEMBER_KIND).to(GetMember.class);