LCOV - code coverage report
Current view: top level - server/group/db - GroupsNoteDbConsistencyChecker.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 119 127 93.7 %
Date: 2022-11-19 15:00:39 Functions: 12 14 85.7 %

          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.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.error;
      18             : import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
      19             : 
      20             : import com.google.common.annotations.VisibleForTesting;
      21             : import com.google.common.collect.BiMap;
      22             : import com.google.common.collect.HashBiMap;
      23             : import com.google.common.collect.ImmutableList;
      24             : import com.google.common.flogger.FluentLogger;
      25             : import com.google.errorprone.annotations.FormatMethod;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.entities.AccountGroup;
      28             : import com.google.gerrit.entities.GroupReference;
      29             : import com.google.gerrit.entities.InternalGroup;
      30             : import com.google.gerrit.entities.RefNames;
      31             : import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
      32             : import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
      33             : import com.google.gerrit.server.config.AllUsersName;
      34             : import com.google.inject.Inject;
      35             : import com.google.inject.Singleton;
      36             : import java.io.IOException;
      37             : import java.nio.charset.StandardCharsets;
      38             : import java.util.ArrayList;
      39             : import java.util.HashMap;
      40             : import java.util.List;
      41             : import java.util.Map;
      42             : import java.util.Objects;
      43             : import java.util.Optional;
      44             : import org.eclipse.jgit.errors.ConfigInvalidException;
      45             : import org.eclipse.jgit.lib.ObjectId;
      46             : import org.eclipse.jgit.lib.ObjectLoader;
      47             : import org.eclipse.jgit.lib.Ref;
      48             : import org.eclipse.jgit.lib.Repository;
      49             : import org.eclipse.jgit.notes.Note;
      50             : import org.eclipse.jgit.notes.NoteMap;
      51             : import org.eclipse.jgit.revwalk.RevCommit;
      52             : import org.eclipse.jgit.revwalk.RevWalk;
      53             : 
      54             : /** Check the referential integrity of NoteDb group storage. */
      55             : @Singleton
      56             : public class GroupsNoteDbConsistencyChecker {
      57         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      58             : 
      59             :   private final AllUsersName allUsersName;
      60             : 
      61             :   @Inject
      62         138 :   GroupsNoteDbConsistencyChecker(AllUsersName allUsersName) {
      63         138 :     this.allUsersName = allUsersName;
      64         138 :   }
      65             : 
      66             :   /**
      67             :    * The result of a consistency check. The UUID map is only non-null if no problems were detected.
      68             :    */
      69           1 :   public static class Result {
      70             :     public List<ConsistencyProblemInfo> problems;
      71             : 
      72             :     @Nullable public Map<AccountGroup.UUID, InternalGroup> uuidToGroupMap;
      73             :   }
      74             : 
      75             :   /** Checks for problems with the given All-Users repo. */
      76             :   public Result check(Repository allUsersRepo) throws IOException {
      77           1 :     Result r = doCheck(allUsersRepo);
      78           1 :     if (!r.problems.isEmpty()) {
      79           1 :       r.uuidToGroupMap = null;
      80             :     }
      81           1 :     return r;
      82             :   }
      83             : 
      84             :   private Result doCheck(Repository allUsersRepo) throws IOException {
      85           1 :     Result result = new Result();
      86           1 :     result.problems = new ArrayList<>();
      87           1 :     result.uuidToGroupMap = new HashMap<>();
      88             : 
      89           1 :     BiMap<AccountGroup.UUID, String> uuidNameBiMap = HashBiMap.create();
      90             : 
      91             :     // Get group refs and group names ref using the most atomic API available, in an attempt to
      92             :     // avoid seeing half-committed group updates.
      93           1 :     List<Ref> refs =
      94             :         allUsersRepo
      95           1 :             .getRefDatabase()
      96           1 :             .getRefsByPrefix(RefNames.REFS_GROUPS, RefNames.REFS_GROUPNAMES);
      97           1 :     readGroups(allUsersRepo, refs, result);
      98           1 :     readGroupNames(allUsersRepo, refs, result, uuidNameBiMap);
      99             :     // The sequential IDs are not keys in NoteDb, so no need to check them.
     100             : 
     101           1 :     if (!result.problems.isEmpty()) {
     102           1 :       return result;
     103             :     }
     104             : 
     105             :     // Continue checking if we could read data without problems.
     106           1 :     result.problems.addAll(checkGlobalConsistency(result.uuidToGroupMap, uuidNameBiMap));
     107             : 
     108           1 :     return result;
     109             :   }
     110             : 
     111             :   private void readGroups(Repository allUsersRepo, List<Ref> refs, Result result)
     112             :       throws IOException {
     113           1 :     for (Ref ref : refs) {
     114           1 :       if (!ref.getName().startsWith(RefNames.REFS_GROUPS)) {
     115           1 :         continue;
     116             :       }
     117             : 
     118           1 :       AccountGroup.UUID uuid = AccountGroup.UUID.fromRef(ref.getName());
     119           1 :       if (uuid == null) {
     120           1 :         result.problems.add(error("null UUID from %s", ref.getName()));
     121           1 :         continue;
     122             :       }
     123             :       try {
     124           1 :         GroupConfig cfg =
     125           1 :             GroupConfig.loadForGroupSnapshot(allUsersName, allUsersRepo, uuid, ref.getObjectId());
     126           1 :         result.uuidToGroupMap.put(uuid, cfg.getLoadedGroup().get());
     127           1 :       } catch (ConfigInvalidException e) {
     128           1 :         result.problems.add(error("group %s does not parse: %s", uuid, e.getMessage()));
     129           1 :       }
     130           1 :     }
     131           1 :   }
     132             : 
     133             :   private void readGroupNames(
     134             :       Repository repo,
     135             :       List<Ref> refs,
     136             :       Result result,
     137             :       BiMap<AccountGroup.UUID, String> uuidNameBiMap)
     138             :       throws IOException {
     139           1 :     Optional<Ref> maybeRef =
     140           1 :         refs.stream().filter(r -> r.getName().equals(RefNames.REFS_GROUPNAMES)).findFirst();
     141           1 :     if (!maybeRef.isPresent()) {
     142           1 :       result.problems.add(error("ref %s does not exist", RefNames.REFS_GROUPNAMES));
     143           1 :       return;
     144             :     }
     145           1 :     Ref ref = maybeRef.get();
     146             : 
     147           1 :     try (RevWalk rw = new RevWalk(repo)) {
     148           1 :       RevCommit c = rw.parseCommit(ref.getObjectId());
     149           1 :       NoteMap nm = NoteMap.read(rw.getObjectReader(), c);
     150             : 
     151           1 :       for (Note note : nm) {
     152           1 :         ObjectLoader ld = rw.getObjectReader().open(note.getData());
     153           1 :         byte[] data = ld.getCachedBytes();
     154             : 
     155             :         GroupReference gRef;
     156             :         try {
     157           1 :           gRef = GroupNameNotes.getFromNoteData(data);
     158           1 :         } catch (ConfigInvalidException e) {
     159           1 :           result.problems.add(
     160           1 :               error(
     161             :                   "notename entry %s: %s does not parse: %s",
     162           1 :                   note, new String(data, StandardCharsets.UTF_8), e.getMessage()));
     163           1 :           continue;
     164           1 :         }
     165             : 
     166           1 :         ObjectId nameKey = GroupNameNotes.getNoteKey(AccountGroup.nameKey(gRef.getName()));
     167           1 :         if (!Objects.equals(nameKey, note)) {
     168           0 :           result.problems.add(
     169           0 :               error("notename entry %s does not match name %s", note, gRef.getName()));
     170             :         }
     171             : 
     172             :         // We trust SHA1 to have no collisions, so no need to check uniqueness of name.
     173           1 :         uuidNameBiMap.put(gRef.getUUID(), gRef.getName());
     174           1 :       }
     175             :     }
     176           1 :   }
     177             : 
     178             :   /** Check invariants of the group refs with the group name refs. */
     179             :   private List<ConsistencyProblemInfo> checkGlobalConsistency(
     180             :       Map<AccountGroup.UUID, InternalGroup> uuidToGroupMap,
     181             :       BiMap<AccountGroup.UUID, String> uuidNameBiMap) {
     182           1 :     List<ConsistencyProblemInfo> problems = new ArrayList<>();
     183             : 
     184             :     // Check consistency between the data coming from different refs.
     185           1 :     for (AccountGroup.UUID uuid : uuidToGroupMap.keySet()) {
     186           1 :       if (!uuidNameBiMap.containsKey(uuid)) {
     187           1 :         problems.add(error("group %s has no entry in name map", uuid));
     188           1 :         continue;
     189             :       }
     190             : 
     191           1 :       String noteName = uuidNameBiMap.get(uuid);
     192           1 :       String groupRefName = uuidToGroupMap.get(uuid).getName();
     193           1 :       if (!Objects.equals(noteName, groupRefName)) {
     194           1 :         problems.add(
     195           1 :             error(
     196             :                 "inconsistent name for group %s (name map %s vs. group ref %s)",
     197             :                 uuid, noteName, groupRefName));
     198             :       }
     199           1 :     }
     200             : 
     201           1 :     for (AccountGroup.UUID uuid : uuidNameBiMap.keySet()) {
     202           1 :       if (!uuidToGroupMap.containsKey(uuid)) {
     203           1 :         problems.add(
     204           1 :             error(
     205             :                 "name map has entry (%s, %s), entry missing as group ref",
     206           1 :                 uuid, uuidNameBiMap.get(uuid)));
     207             :       }
     208           1 :     }
     209             : 
     210           1 :     if (problems.isEmpty()) {
     211             :       // Check ids.
     212           1 :       Map<AccountGroup.Id, InternalGroup> groupById = new HashMap<>();
     213           1 :       for (InternalGroup g : uuidToGroupMap.values()) {
     214           1 :         InternalGroup before = groupById.get(g.getId());
     215           1 :         if (before != null) {
     216           1 :           problems.add(
     217           1 :               error(
     218             :                   "shared group id %s for %s (%s) and %s (%s)",
     219           1 :                   g.getId(),
     220           1 :                   before.getName(),
     221           1 :                   before.getGroupUUID(),
     222           1 :                   g.getName(),
     223           1 :                   g.getGroupUUID()));
     224             :         }
     225           1 :         groupById.put(g.getId(), g);
     226           1 :       }
     227             :     }
     228             : 
     229           1 :     return problems;
     230             :   }
     231             : 
     232             :   public static void ensureConsistentWithGroupNameNotes(
     233             :       Repository allUsersRepo, InternalGroup group) throws IOException {
     234         151 :     ImmutableList<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
     235         151 :         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
     236         151 :             allUsersRepo, group.getNameKey(), group.getGroupUUID());
     237         151 :     problems.forEach(GroupsNoteDbConsistencyChecker::logConsistencyProblem);
     238         151 :   }
     239             : 
     240             :   /**
     241             :    * Check group 'uuid' and 'name' read from 'group.config' with group name notes.
     242             :    *
     243             :    * @param allUsersRepo 'All-Users' repository.
     244             :    * @param groupName the name of the group to be checked.
     245             :    * @param groupUUID the {@code AccountGroup.UUID} of the group to be checked.
     246             :    * @return a list of {@code ConsistencyProblemInfo} containing the problem details.
     247             :    */
     248             :   @VisibleForTesting
     249             :   static ImmutableList<ConsistencyProblemInfo> checkWithGroupNameNotes(
     250             :       Repository allUsersRepo, AccountGroup.NameKey groupName, AccountGroup.UUID groupUUID)
     251             :       throws IOException {
     252             :     try {
     253         152 :       Optional<GroupReference> groupRef = GroupNameNotes.loadGroup(allUsersRepo, groupName);
     254             : 
     255         152 :       if (!groupRef.isPresent()) {
     256           2 :         return ImmutableList.of(
     257           2 :             warning("Group with name '%s' doesn't exist in the list of all names", groupName));
     258             :       }
     259             : 
     260         152 :       AccountGroup.UUID uuid = groupRef.get().getUUID();
     261             : 
     262         152 :       ImmutableList.Builder<ConsistencyProblemInfo> problems = ImmutableList.builder();
     263         152 :       if (!Objects.equals(groupUUID, uuid)) {
     264           1 :         problems.add(
     265           1 :             warning(
     266             :                 "group with name '%s' has UUID '%s' in 'group.config' but '%s' in group name notes",
     267             :                 groupName, groupUUID, uuid));
     268             :       }
     269             : 
     270         152 :       String name = groupName.get();
     271         152 :       String actualName = groupRef.get().getName();
     272         152 :       if (!Objects.equals(name, actualName)) {
     273           1 :         problems.add(
     274           1 :             warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
     275             :       }
     276         152 :       return problems.build();
     277           1 :     } catch (ConfigInvalidException e) {
     278           1 :       return ImmutableList.of(
     279           1 :           warning("fail to check consistency with group name notes: %s", e.getMessage()));
     280             :     }
     281             :   }
     282             : 
     283             :   @FormatMethod
     284             :   public static void logConsistencyProblemAsWarning(String fmt, Object... args) {
     285           0 :     logConsistencyProblem(warning(fmt, args));
     286           0 :   }
     287             : 
     288             :   public static void logConsistencyProblem(ConsistencyProblemInfo p) {
     289           1 :     if (p.status == ConsistencyProblemInfo.Status.WARNING) {
     290           1 :       logger.atWarning().log("%s", p.message);
     291             :     } else {
     292           0 :       logger.atSevere().log("%s", p.message);
     293             :     }
     294           1 :   }
     295             : 
     296             :   public static void logFailToLoadFromGroupRefAsWarning(AccountGroup.UUID uuid) {
     297           0 :     logConsistencyProblem(
     298           0 :         warning("Group with UUID %s from group name notes failed to load from group ref", uuid));
     299           0 :   }
     300             : }

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