Add new REST endpoint that provides the audit log of a group

Bug: Issue 1479
Change-Id: I59c51964cab1f1ab32f50db8645a09d96d7a0196
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin 2015-07-03 13:34:19 +02:00
parent d7f38a5ee3
commit fc3f832935
8 changed files with 352 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

@ -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

@ -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

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