All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.group.db.AuditLogReader Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// Copyright (C) 2017 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.db;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Account;
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.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.notedb.NoteDbUtil;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.RawParseUtils;

/** NoteDb reader for group audit log. */
@Singleton
public class AuditLogReader {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final String serverId;
  private final AllUsersName allUsersName;

  @Inject
  public AuditLogReader(@GerritServerId String serverId, AllUsersName allUsersName) {
    this.serverId = serverId;
    this.allUsersName = allUsersName;
  }

  // Having separate methods for reading the two types of audit records mirrors the split in
  // ReviewDb. Once ReviewDb is gone, the audit record interface becomes more flexible and we can
  // revisit this, e.g. to do only a single walk, or even change the record types.

  public ImmutableList getMembersAudit(
      Repository allUsersRepo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
    return getMembersAudit(getGroupId(allUsersRepo, uuid), parseCommits(allUsersRepo, uuid));
  }

  private ImmutableList getMembersAudit(
      AccountGroup.Id groupId, List commits) {
    ListMultimap audits =
        MultimapBuilder.hashKeys().linkedListValues().build();
    ImmutableList.Builder result = ImmutableList.builder();
    for (ParsedCommit pc : commits) {
      for (Account.Id id : pc.addedMembers()) {
        MemberKey key = MemberKey.create(groupId, id);
        AccountGroupMemberAudit audit =
            new AccountGroupMemberAudit(
                new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
        audits.put(key, audit);
        result.add(audit);
      }
      for (Account.Id id : pc.removedMembers()) {
        List adds = audits.get(MemberKey.create(groupId, id));
        if (!adds.isEmpty()) {
          AccountGroupMemberAudit audit = adds.remove(0);
          audit.removed(pc.authorId(), pc.when());
        } else {
          // Match old behavior of DbGroupAuditListener and add a "legacy" add/remove pair.
          AccountGroupMemberAudit audit =
              new AccountGroupMemberAudit(
                  new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
          audit.removedLegacy();
          result.add(audit);
        }
      }
    }
    return result.build();
  }

  public ImmutableList getSubgroupsAudit(
      Repository repo, AccountGroup.UUID uuid) throws IOException, ConfigInvalidException {
    return getSubgroupsAudit(getGroupId(repo, uuid), parseCommits(repo, uuid));
  }

  private ImmutableList getSubgroupsAudit(
      AccountGroup.Id groupId, List commits) {
    ListMultimap audits =
        MultimapBuilder.hashKeys().linkedListValues().build();
    ImmutableList.Builder result = ImmutableList.builder();
    for (ParsedCommit pc : commits) {
      for (AccountGroup.UUID uuid : pc.addedSubgroups()) {
        SubgroupKey key = SubgroupKey.create(groupId, uuid);
        AccountGroupByIdAud audit =
            new AccountGroupByIdAud(
                new AccountGroupByIdAud.Key(groupId, uuid, pc.when()), pc.authorId());
        audits.put(key, audit);
        result.add(audit);
      }
      for (AccountGroup.UUID uuid : pc.removedSubgroups()) {
        List adds = audits.get(SubgroupKey.create(groupId, uuid));
        if (!adds.isEmpty()) {
          AccountGroupByIdAud audit = adds.remove(0);
          audit.removed(pc.authorId(), pc.when());
        } else {
          // Unlike members, DbGroupAuditListener didn't insert an add/remove pair here.
        }
      }
    }
    return result.build();
  }

  private Optional parse(AccountGroup.UUID uuid, RevCommit c) {
    Optional authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
    if (!authorId.isPresent()) {
      // Only report audit events from identified users, since this is a non-nullable field in
      // ReviewDb. May be revisited after groups are fully migrated to NoteDb.
      return Optional.empty();
    }

    List addedMembers = new ArrayList<>();
    List addedSubgroups = new ArrayList<>();
    List removedMembers = new ArrayList<>();
    List removedSubgroups = new ArrayList<>();

    for (FooterLine line : c.getFooterLines()) {
      if (line.matches(GroupConfigCommitMessage.FOOTER_ADD_MEMBER)) {
        parseAccount(uuid, c, line).ifPresent(addedMembers::add);
      } else if (line.matches(GroupConfigCommitMessage.FOOTER_REMOVE_MEMBER)) {
        parseAccount(uuid, c, line).ifPresent(removedMembers::add);
      } else if (line.matches(GroupConfigCommitMessage.FOOTER_ADD_GROUP)) {
        parseGroup(uuid, c, line).ifPresent(addedSubgroups::add);
      } else if (line.matches(GroupConfigCommitMessage.FOOTER_REMOVE_GROUP)) {
        parseGroup(uuid, c, line).ifPresent(removedSubgroups::add);
      }
    }
    return Optional.of(
        new AutoValue_AuditLogReader_ParsedCommit(
            authorId.get(),
            new Timestamp(c.getAuthorIdent().getWhen().getTime()),
            ImmutableList.copyOf(addedMembers),
            ImmutableList.copyOf(removedMembers),
            ImmutableList.copyOf(addedSubgroups),
            ImmutableList.copyOf(removedSubgroups)));
  }

  private Optional parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
    Optional result =
        Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
            .flatMap(ident -> NoteDbUtil.parseIdent(ident, serverId));
    if (!result.isPresent()) {
      logInvalid(uuid, c, line);
    }
    return result;
  }

  private static Optional parseGroup(
      AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
    PersonIdent ident = RawParseUtils.parsePersonIdent(line.getValue());
    if (ident == null) {
      logInvalid(uuid, c, line);
      return Optional.empty();
    }
    return Optional.of(new AccountGroup.UUID(ident.getEmailAddress()));
  }

  private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
    logger.atFine().log(
        "Invalid footer line in commit %s while parsing audit log for group %s: %s",
        c.name(), uuid, line);
  }

  private ImmutableList parseCommits(Repository repo, AccountGroup.UUID uuid)
      throws IOException {
    try (RevWalk rw = new RevWalk(repo)) {
      Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
      if (ref == null) {
        return ImmutableList.of();
      }

      rw.reset();
      rw.markStart(rw.parseCommit(ref.getObjectId()));
      rw.setRetainBody(true);
      rw.sort(RevSort.COMMIT_TIME_DESC, true);
      rw.sort(RevSort.REVERSE, true);

      ImmutableList.Builder result = ImmutableList.builder();
      RevCommit c;
      while ((c = rw.next()) != null) {
        parse(uuid, c).ifPresent(result::add);
      }
      return result.build();
    }
  }

  private AccountGroup.Id getGroupId(Repository allUsersRepo, AccountGroup.UUID uuid)
      throws ConfigInvalidException, IOException {
    // TODO(dborowitz): This re-walks all commits just to find createdOn, which we don't need.
    return GroupConfig.loadForGroup(allUsersName, allUsersRepo, uuid)
        .getLoadedGroup()
        .get()
        .getId();
  }

  @AutoValue
  abstract static class MemberKey {
    static MemberKey create(AccountGroup.Id groupId, Account.Id memberId) {
      return new AutoValue_AuditLogReader_MemberKey(groupId, memberId);
    }

    abstract AccountGroup.Id groupId();

    abstract Account.Id memberId();
  }

  @AutoValue
  abstract static class SubgroupKey {
    static SubgroupKey create(AccountGroup.Id groupId, AccountGroup.UUID subgroupUuid) {
      return new AutoValue_AuditLogReader_SubgroupKey(groupId, subgroupUuid);
    }

    abstract AccountGroup.Id groupId();

    abstract AccountGroup.UUID subgroupUuid();
  }

  @AutoValue
  abstract static class ParsedCommit {
    abstract Account.Id authorId();

    abstract Timestamp when();

    abstract ImmutableList addedMembers();

    abstract ImmutableList removedMembers();

    abstract ImmutableList addedSubgroups();

    abstract ImmutableList removedSubgroups();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy