LCOV - code coverage report
Current view: top level - server/group/db - AuditLogReader.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 102 117 87.2 %
Date: 2022-11-19 15:00:39 Functions: 17 18 94.4 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750