Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.group.db;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 :
19 : import com.google.auto.value.AutoValue;
20 : import com.google.common.collect.ImmutableList;
21 : import com.google.common.collect.ListMultimap;
22 : import com.google.common.collect.MultimapBuilder;
23 : import com.google.common.flogger.FluentLogger;
24 : import com.google.gerrit.entities.Account;
25 : import com.google.gerrit.entities.AccountGroup;
26 : import com.google.gerrit.entities.AccountGroupByIdAudit;
27 : import com.google.gerrit.entities.AccountGroupMemberAudit;
28 : import com.google.gerrit.entities.RefNames;
29 : import com.google.gerrit.server.config.AllUsersName;
30 : import com.google.gerrit.server.notedb.NoteDbUtil;
31 : import com.google.inject.Inject;
32 : import com.google.inject.Singleton;
33 : import java.io.IOException;
34 : import java.time.Instant;
35 : import java.util.ArrayList;
36 : import java.util.List;
37 : import java.util.Optional;
38 : import org.eclipse.jgit.errors.ConfigInvalidException;
39 : import org.eclipse.jgit.lib.PersonIdent;
40 : import org.eclipse.jgit.lib.Ref;
41 : import org.eclipse.jgit.lib.Repository;
42 : import org.eclipse.jgit.revwalk.FooterLine;
43 : import org.eclipse.jgit.revwalk.RevCommit;
44 : import org.eclipse.jgit.revwalk.RevSort;
45 : import org.eclipse.jgit.revwalk.RevWalk;
46 : import org.eclipse.jgit.util.RawParseUtils;
47 :
48 : /** NoteDb reader for group audit log. */
49 : @Singleton
50 : public class AuditLogReader {
51 153 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
52 :
53 : private final AllUsersName allUsersName;
54 :
55 : @Inject
56 153 : public AuditLogReader(AllUsersName allUsersName) {
57 153 : this.allUsersName = allUsersName;
58 153 : }
59 :
60 : // Having separate methods for reading the two types of audit records mirrors the split in
61 : // ReviewDb. Now that ReviewDb is gone, the audit record interface is more flexible and this may
62 : // be changed, e.g. to do only a single walk, or even change the record types.
63 :
64 : public ImmutableList<AccountGroupMemberAudit> getMembersAudit(
65 : Repository allUsersRepo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
66 3 : return getMembersAudit(getGroupId(allUsersRepo, uuid), parseCommits(allUsersRepo, uuid));
67 : }
68 :
69 : private ImmutableList<AccountGroupMemberAudit> getMembersAudit(
70 : AccountGroup.Id groupId, List<ParsedCommit> commits) {
71 : ListMultimap<MemberKey, AccountGroupMemberAudit.Builder> audits =
72 3 : MultimapBuilder.hashKeys().linkedListValues().build();
73 3 : List<AccountGroupMemberAudit.Builder> result = new ArrayList<>();
74 3 : for (ParsedCommit pc : commits) {
75 3 : for (Account.Id id : pc.addedMembers()) {
76 3 : MemberKey key = MemberKey.create(groupId, id);
77 : AccountGroupMemberAudit.Builder audit =
78 3 : AccountGroupMemberAudit.builder()
79 3 : .memberId(id)
80 3 : .groupId(groupId)
81 3 : .addedOn(pc.when())
82 3 : .addedBy(pc.authorId());
83 3 : audits.put(key, audit);
84 3 : result.add(audit);
85 3 : }
86 3 : for (Account.Id id : pc.removedMembers()) {
87 2 : List<AccountGroupMemberAudit.Builder> adds = audits.get(MemberKey.create(groupId, id));
88 2 : if (!adds.isEmpty()) {
89 2 : AccountGroupMemberAudit.Builder audit = adds.remove(0);
90 2 : audit.removed(pc.authorId(), pc.when());
91 2 : } else {
92 : // Match old behavior of DbGroupAuditListener and add a "legacy" add/remove pair.
93 : AccountGroupMemberAudit.Builder audit =
94 0 : AccountGroupMemberAudit.builder()
95 0 : .groupId(groupId)
96 0 : .memberId(id)
97 0 : .addedOn(pc.when())
98 0 : .addedBy(pc.authorId())
99 0 : .removedLegacy();
100 0 : result.add(audit);
101 : }
102 2 : }
103 3 : }
104 3 : return result.stream().map(AccountGroupMemberAudit.Builder::build).collect(toImmutableList());
105 : }
106 :
107 : public ImmutableList<AccountGroupByIdAudit> getSubgroupsAudit(
108 : Repository repo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
109 3 : return getSubgroupsAudit(getGroupId(repo, uuid), parseCommits(repo, uuid));
110 : }
111 :
112 : private ImmutableList<AccountGroupByIdAudit> getSubgroupsAudit(
113 : AccountGroup.Id groupId, List<ParsedCommit> commits) {
114 : ListMultimap<SubgroupKey, AccountGroupByIdAudit.Builder> audits =
115 3 : MultimapBuilder.hashKeys().linkedListValues().build();
116 3 : List<AccountGroupByIdAudit.Builder> result = new ArrayList<>();
117 3 : for (ParsedCommit pc : commits) {
118 3 : for (AccountGroup.UUID uuid : pc.addedSubgroups()) {
119 2 : SubgroupKey key = SubgroupKey.create(groupId, uuid);
120 : AccountGroupByIdAudit.Builder audit =
121 2 : AccountGroupByIdAudit.builder()
122 2 : .groupId(groupId)
123 2 : .includeUuid(uuid)
124 2 : .addedOn(pc.when())
125 2 : .addedBy(pc.authorId());
126 2 : audits.put(key, audit);
127 2 : result.add(audit);
128 2 : }
129 3 : for (AccountGroup.UUID uuid : pc.removedSubgroups()) {
130 2 : List<AccountGroupByIdAudit.Builder> adds = audits.get(SubgroupKey.create(groupId, uuid));
131 2 : if (!adds.isEmpty()) {
132 2 : AccountGroupByIdAudit.Builder audit = adds.remove(0);
133 2 : audit.removed(pc.authorId(), pc.when());
134 : } else {
135 : // Unlike members, DbGroupAuditListener didn't insert an add/remove pair here.
136 : }
137 2 : }
138 3 : }
139 3 : return result.stream().map(AccountGroupByIdAudit.Builder::build).collect(toImmutableList());
140 : }
141 :
142 : private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
143 3 : Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent());
144 3 : if (!authorId.isPresent()) {
145 : // Only report audit events from identified users, since this was a non-nullable field in
146 : // ReviewDb. May be revisited.
147 2 : return Optional.empty();
148 : }
149 :
150 3 : List<Account.Id> addedMembers = new ArrayList<>();
151 3 : List<AccountGroup.UUID> addedSubgroups = new ArrayList<>();
152 3 : List<Account.Id> removedMembers = new ArrayList<>();
153 3 : List<AccountGroup.UUID> removedSubgroups = new ArrayList<>();
154 :
155 3 : for (FooterLine line : c.getFooterLines()) {
156 3 : if (line.matches(GroupConfigCommitMessage.FOOTER_ADD_MEMBER)) {
157 3 : parseAccount(uuid, c, line).ifPresent(addedMembers::add);
158 2 : } else if (line.matches(GroupConfigCommitMessage.FOOTER_REMOVE_MEMBER)) {
159 2 : parseAccount(uuid, c, line).ifPresent(removedMembers::add);
160 2 : } else if (line.matches(GroupConfigCommitMessage.FOOTER_ADD_GROUP)) {
161 2 : parseGroup(uuid, c, line).ifPresent(addedSubgroups::add);
162 2 : } else if (line.matches(GroupConfigCommitMessage.FOOTER_REMOVE_GROUP)) {
163 2 : parseGroup(uuid, c, line).ifPresent(removedSubgroups::add);
164 : }
165 3 : }
166 3 : return Optional.of(
167 : new AutoValue_AuditLogReader_ParsedCommit(
168 3 : authorId.get(),
169 3 : c.getAuthorIdent().getWhenAsInstant(),
170 3 : ImmutableList.copyOf(addedMembers),
171 3 : ImmutableList.copyOf(removedMembers),
172 3 : ImmutableList.copyOf(addedSubgroups),
173 3 : ImmutableList.copyOf(removedSubgroups)));
174 : }
175 :
176 : private Optional<Account.Id> parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
177 3 : Optional<Account.Id> result =
178 3 : Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
179 3 : .flatMap(ident -> NoteDbUtil.parseIdent(ident));
180 3 : if (!result.isPresent()) {
181 0 : logInvalid(uuid, c, line);
182 : }
183 3 : return result;
184 : }
185 :
186 : private static Optional<AccountGroup.UUID> parseGroup(
187 : AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
188 2 : PersonIdent ident = RawParseUtils.parsePersonIdent(line.getValue());
189 2 : if (ident == null) {
190 0 : logInvalid(uuid, c, line);
191 0 : return Optional.empty();
192 : }
193 2 : return Optional.of(AccountGroup.uuid(ident.getEmailAddress()));
194 : }
195 :
196 : private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
197 0 : logger.atFine().log(
198 : "Invalid footer line in commit %s while parsing audit log for group %s: %s",
199 0 : c.name(), uuid, line);
200 0 : }
201 :
202 : private ImmutableList<ParsedCommit> parseCommits(Repository repo, AccountGroup.UUID uuid)
203 : throws IOException {
204 3 : try (RevWalk rw = new RevWalk(repo)) {
205 3 : Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
206 3 : if (ref == null) {
207 0 : return ImmutableList.of();
208 : }
209 :
210 3 : rw.reset();
211 3 : rw.markStart(rw.parseCommit(ref.getObjectId()));
212 3 : rw.setRetainBody(true);
213 3 : rw.sort(RevSort.COMMIT_TIME_DESC, true);
214 3 : rw.sort(RevSort.REVERSE, true);
215 :
216 3 : ImmutableList.Builder<ParsedCommit> result = ImmutableList.builder();
217 : RevCommit c;
218 3 : while ((c = rw.next()) != null) {
219 3 : parse(uuid, c).ifPresent(result::add);
220 : }
221 3 : return result.build();
222 0 : }
223 : }
224 :
225 : private AccountGroup.Id getGroupId(Repository allUsersRepo, AccountGroup.UUID uuid)
226 : throws ConfigInvalidException, IOException {
227 : // TODO(dborowitz): This re-walks all commits just to find createdOn, which we don't need.
228 3 : return GroupConfig.loadForGroup(allUsersName, allUsersRepo, uuid)
229 3 : .getLoadedGroup()
230 3 : .get()
231 3 : .getId();
232 : }
233 :
234 : @AutoValue
235 3 : abstract static class MemberKey {
236 : static MemberKey create(AccountGroup.Id groupId, Account.Id memberId) {
237 3 : return new AutoValue_AuditLogReader_MemberKey(groupId, memberId);
238 : }
239 :
240 : abstract AccountGroup.Id groupId();
241 :
242 : abstract Account.Id memberId();
243 : }
244 :
245 : @AutoValue
246 2 : abstract static class SubgroupKey {
247 : static SubgroupKey create(AccountGroup.Id groupId, AccountGroup.UUID subgroupUuid) {
248 2 : return new AutoValue_AuditLogReader_SubgroupKey(groupId, subgroupUuid);
249 : }
250 :
251 : abstract AccountGroup.Id groupId();
252 :
253 : abstract AccountGroup.UUID subgroupUuid();
254 : }
255 :
256 : @AutoValue
257 3 : abstract static class ParsedCommit {
258 : abstract Account.Id authorId();
259 :
260 : abstract Instant when();
261 :
262 : abstract ImmutableList<Account.Id> addedMembers();
263 :
264 : abstract ImmutableList<Account.Id> removedMembers();
265 :
266 : abstract ImmutableList<AccountGroup.UUID> addedSubgroups();
267 :
268 : abstract ImmutableList<AccountGroup.UUID> removedSubgroups();
269 : }
270 : }
|