blob: 04143044cf4be56efc7ef12b3c6be4c4066612bf [file] [log] [blame]
// 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 static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.error;
import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.group.InternalGroup;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/** Check the referential integrity of NoteDb group storage. */
@Singleton
public class GroupsNoteDbConsistencyChecker {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AllUsersName allUsersName;
@Inject
GroupsNoteDbConsistencyChecker(AllUsersName allUsersName) {
this.allUsersName = allUsersName;
}
/**
* The result of a consistency check. The UUID map is only non-null if no problems were detected.
*/
public static class Result {
public List<ConsistencyProblemInfo> problems;
@Nullable public Map<AccountGroup.UUID, InternalGroup> uuidToGroupMap;
}
/** Checks for problems with the given All-Users repo. */
public Result check(Repository allUsersRepo) throws IOException {
Result r = doCheck(allUsersRepo);
if (!r.problems.isEmpty()) {
r.uuidToGroupMap = null;
}
return r;
}
private Result doCheck(Repository allUsersRepo) throws IOException {
Result result = new Result();
result.problems = new ArrayList<>();
result.uuidToGroupMap = new HashMap<>();
BiMap<AccountGroup.UUID, String> uuidNameBiMap = HashBiMap.create();
// Get group refs and group names ref using the most atomic API available, in an attempt to
// avoid seeing half-committed group updates.
List<Ref> refs =
allUsersRepo
.getRefDatabase()
.getRefsByPrefix(RefNames.REFS_GROUPS, RefNames.REFS_GROUPNAMES);
readGroups(allUsersRepo, refs, result);
readGroupNames(allUsersRepo, refs, result, uuidNameBiMap);
// The sequential IDs are not keys in NoteDb, so no need to check them.
if (!result.problems.isEmpty()) {
return result;
}
// Continue checking if we could read data without problems.
result.problems.addAll(checkGlobalConsistency(result.uuidToGroupMap, uuidNameBiMap));
return result;
}
private void readGroups(Repository allUsersRepo, List<Ref> refs, Result result)
throws IOException {
for (Ref ref : refs) {
if (!ref.getName().startsWith(RefNames.REFS_GROUPS)) {
continue;
}
AccountGroup.UUID uuid = AccountGroup.UUID.fromRef(ref.getName());
if (uuid == null) {
result.problems.add(error("null UUID from %s", ref.getName()));
continue;
}
try {
GroupConfig cfg =
GroupConfig.loadForGroupSnapshot(allUsersName, allUsersRepo, uuid, ref.getObjectId());
result.uuidToGroupMap.put(uuid, cfg.getLoadedGroup().get());
} catch (ConfigInvalidException e) {
result.problems.add(error("group %s does not parse: %s", uuid, e.getMessage()));
}
}
}
private void readGroupNames(
Repository repo,
List<Ref> refs,
Result result,
BiMap<AccountGroup.UUID, String> uuidNameBiMap)
throws IOException {
Optional<Ref> maybeRef =
refs.stream().filter(r -> r.getName().equals(RefNames.REFS_GROUPNAMES)).findFirst();
if (!maybeRef.isPresent()) {
String msg = String.format("ref %s does not exist", RefNames.REFS_GROUPNAMES);
result.problems.add(error(msg));
return;
}
Ref ref = maybeRef.get();
try (RevWalk rw = new RevWalk(repo)) {
RevCommit c = rw.parseCommit(ref.getObjectId());
NoteMap nm = NoteMap.read(rw.getObjectReader(), c);
for (Note note : nm) {
ObjectLoader ld = rw.getObjectReader().open(note.getData());
byte[] data = ld.getCachedBytes();
GroupReference gRef;
try {
gRef = GroupNameNotes.getFromNoteData(data);
} catch (ConfigInvalidException e) {
result.problems.add(
error(
"notename entry %s: %s does not parse: %s",
note, new String(data, StandardCharsets.UTF_8), e.getMessage()));
continue;
}
ObjectId nameKey = GroupNameNotes.getNoteKey(AccountGroup.nameKey(gRef.getName()));
if (!Objects.equals(nameKey, note)) {
result.problems.add(
error("notename entry %s does not match name %s", note, gRef.getName()));
}
// We trust SHA1 to have no collisions, so no need to check uniqueness of name.
uuidNameBiMap.put(gRef.getUUID(), gRef.getName());
}
}
}
/** Check invariants of the group refs with the group name refs. */
private List<ConsistencyProblemInfo> checkGlobalConsistency(
Map<AccountGroup.UUID, InternalGroup> uuidToGroupMap,
BiMap<AccountGroup.UUID, String> uuidNameBiMap) {
List<ConsistencyProblemInfo> problems = new ArrayList<>();
// Check consistency between the data coming from different refs.
for (AccountGroup.UUID uuid : uuidToGroupMap.keySet()) {
if (!uuidNameBiMap.containsKey(uuid)) {
problems.add(error("group %s has no entry in name map", uuid));
continue;
}
String noteName = uuidNameBiMap.get(uuid);
String groupRefName = uuidToGroupMap.get(uuid).getName();
if (!Objects.equals(noteName, groupRefName)) {
problems.add(
error(
"inconsistent name for group %s (name map %s vs. group ref %s)",
uuid, noteName, groupRefName));
}
}
for (AccountGroup.UUID uuid : uuidNameBiMap.keySet()) {
if (!uuidToGroupMap.containsKey(uuid)) {
problems.add(
error(
"name map has entry (%s, %s), entry missing as group ref",
uuid, uuidNameBiMap.get(uuid)));
}
}
if (problems.isEmpty()) {
// Check ids.
Map<AccountGroup.Id, InternalGroup> groupById = new HashMap<>();
for (InternalGroup g : uuidToGroupMap.values()) {
InternalGroup before = groupById.get(g.getId());
if (before != null) {
problems.add(
error(
"shared group id %s for %s (%s) and %s (%s)",
g.getId(),
before.getName(),
before.getGroupUUID(),
g.getName(),
g.getGroupUUID()));
}
groupById.put(g.getId(), g);
}
}
return problems;
}
public static void ensureConsistentWithGroupNameNotes(
Repository allUsersRepo, InternalGroup group) throws IOException {
List<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, group.getNameKey(), group.getGroupUUID());
problems.forEach(GroupsNoteDbConsistencyChecker::logConsistencyProblem);
}
/**
* Check group 'uuid' and 'name' read from 'group.config' with group name notes.
*
* @param allUsersRepo 'All-Users' repository.
* @param groupName the name of the group to be checked.
* @param groupUUID the {@code AccountGroup.UUID} of the group to be checked.
* @return a list of {@code ConsistencyProblemInfo} containing the problem details.
*/
@VisibleForTesting
static List<ConsistencyProblemInfo> checkWithGroupNameNotes(
Repository allUsersRepo, AccountGroup.NameKey groupName, AccountGroup.UUID groupUUID)
throws IOException {
try {
Optional<GroupReference> groupRef = GroupNameNotes.loadGroup(allUsersRepo, groupName);
if (!groupRef.isPresent()) {
return ImmutableList.of(
warning("Group with name '%s' doesn't exist in the list of all names", groupName));
}
AccountGroup.UUID uuid = groupRef.get().getUUID();
List<ConsistencyProblemInfo> problems = new ArrayList<>();
if (!Objects.equals(groupUUID, uuid)) {
problems.add(
warning(
"group with name '%s' has UUID '%s' in 'group.config' but '%s' in group name notes",
groupName, groupUUID, uuid));
}
String name = groupName.get();
String actualName = groupRef.get().getName();
if (!Objects.equals(name, actualName)) {
problems.add(
warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
}
return problems;
} catch (ConfigInvalidException e) {
return ImmutableList.of(
warning("fail to check consistency with group name notes: %s", e.getMessage()));
}
}
public static void logConsistencyProblemAsWarning(String fmt, Object... args) {
logConsistencyProblem(warning(fmt, args));
}
public static void logConsistencyProblem(ConsistencyProblemInfo p) {
if (p.status == ConsistencyProblemInfo.Status.WARNING) {
logger.atWarning().log(p.message);
} else {
logger.atSevere().log(p.message);
}
}
public static void logFailToLoadFromGroupRefAsWarning(AccountGroup.UUID uuid) {
logConsistencyProblem(
warning("Group with UUID %s from group name notes failed to load from group ref", uuid));
}
}