diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt index fbd3ba5d01..afdf36d53d 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt @@ -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 diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java index 8ed416513a..5423450abc 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java @@ -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 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( diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java index 417e3714f4..f5a89c05c6 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java @@ -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 auditLog() throws RestApiException; } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java new file mode 100644 index 0000000000..1d8839fd5d --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java @@ -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; + } + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index e43139cf8c..cda3060e79 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java @@ -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()); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java new file mode 100644 index 0000000000..4f46d3945b --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java @@ -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>() { + @Override + public void onSuccess(JsArray result) { + auditEventTable.display(Natives.asList(result)); + } + }); + } + + private class AuditEventTable extends FancyFlexTable { + 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 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(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java index 88935498d0..ad3b3f85f3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java @@ -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() { @Override public void onSuccess(Boolean result) { + if (result) { + link(Util.C.groupTabAuditLog(), auditLogTabToken, + AccountGroup.isInternalGroup(group.getGroupUUID())); + setToken(token); + } display(group, result); } }); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java index 6fbd678991..66a64b41e7 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java @@ -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(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties index 8aaa95ee87..952ea5f5ec 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties @@ -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 diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java index e2b111293d..02deb28c31 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java @@ -193,6 +193,12 @@ public class GroupApi { } } + /** Get audit log of a group. */ + public static void getAuditLog(AccountGroup.UUID group, + AsyncCallback> cb) { + group(group).view("log.audit").get(cb); + } + private static RestApi members(AccountGroup.UUID group) { return group(group).view("members"); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java new file mode 100644 index 0000000000..2d1de6707e --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java @@ -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() { + } +} diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java index 07e7d036ff..161a66e003 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java @@ -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; + } } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java index d3798dbf9d..c1b057aae4 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java @@ -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; + } } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java index d16d286270..480e8e460d 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java @@ -32,4 +32,8 @@ public interface AccountGroupByIdAudAccess extends @Query("WHERE key.groupId = ? AND key.includeUUID = ?") ResultSet byGroupInclude(AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException; + + @Query("WHERE key.groupId = ?") + ResultSet byGroup(AccountGroup.Id groupId) + throws OrmException; } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java index 236d1c1e36..041254a488 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java @@ -33,4 +33,8 @@ public interface AccountGroupMemberAuditAccess extends @Query("WHERE key.groupId = ? AND key.accountId = ?") ResultSet byGroupAccount(AccountGroup.Id groupId, Account.Id accountId) throws OrmException; + + @Query("WHERE key.groupId = ?") + ResultSet byGroup(AccountGroup.Id groupId) + throws OrmException; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java index b7c8cd9f61..f11ed86dce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java @@ -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 auditLog() throws RestApiException { + try { + return getAuditLog.apply(rsrc); + } catch (OrmException e) { + throw new RestApiException("Cannot get audit log", e); + } + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java new file mode 100644 index 0000000000..34c2b76f13 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java @@ -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 { + private final Provider db; + private final AccountLoader.Factory accountLoaderFactory; + private final GroupCache groupCache; + private final GroupJson groupJson; + + @Inject + public GetAuditLog(Provider db, + AccountLoader.Factory accountLoaderFactory, + GroupCache groupCache, + GroupJson groupJson) { + this.db = db; + this.accountLoaderFactory = accountLoaderFactory; + this.groupCache = groupCache; + this.groupJson = groupJson; + } + + @Override + public List 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 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() { + @Override + public int compare(GroupAuditEventInfo e1, GroupAuditEventInfo e2) { + return e2.date.compareTo(e1.date); + } + }); + + return auditEvents; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java index 9b5d9abae0..041e2fa1e7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java @@ -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);