| // 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.gerrit.entities.Account; | |
| import com.google.gerrit.entities.AccountGroup; | |
| import com.google.gerrit.entities.InternalGroup; | |
| import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; | |
| import com.google.gerrit.server.account.AccountState; | |
| import com.google.gerrit.server.account.Accounts; | |
| import com.google.gerrit.server.account.GroupBackend; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.HashSet; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.lib.Repository; | |
| /** | |
| * Checks individual groups for oddities, such as cycles, non-existent subgroups, etc. Only works if | |
| * we are writing to NoteDb. | |
| */ | |
| @Singleton | |
| public class GroupsConsistencyChecker { | |
| private final AllUsersName allUsersName; | |
| private final GroupBackend groupBackend; | |
| private final Accounts accounts; | |
| private final GitRepositoryManager repoManager; | |
| private final GroupsNoteDbConsistencyChecker globalChecker; | |
| @Inject | |
| GroupsConsistencyChecker( | |
| AllUsersName allUsersName, | |
| GroupBackend groupBackend, | |
| Accounts accounts, | |
| GitRepositoryManager repositoryManager, | |
| GroupsNoteDbConsistencyChecker globalChecker) { | |
| this.allUsersName = allUsersName; | |
| this.groupBackend = groupBackend; | |
| this.accounts = accounts; | |
| this.repoManager = repositoryManager; | |
| this.globalChecker = globalChecker; | |
| } | |
| /** Checks that all internal group references exist, and that no groups have cycles. */ | |
| public List<ConsistencyProblemInfo> check() throws IOException { | |
| try (Repository repo = repoManager.openRepository(allUsersName)) { | |
| GroupsNoteDbConsistencyChecker.Result result = globalChecker.check(repo); | |
| if (!result.problems.isEmpty()) { | |
| return result.problems; | |
| } | |
| for (InternalGroup g : result.uuidToGroupMap.values()) { | |
| result.problems.addAll(checkGroup(g, result.uuidToGroupMap)); | |
| } | |
| return result.problems; | |
| } | |
| } | |
| /** Checks the metadata for a single group for problems. */ | |
| private List<ConsistencyProblemInfo> checkGroup( | |
| InternalGroup g, Map<AccountGroup.UUID, InternalGroup> byUUID) throws IOException { | |
| List<ConsistencyProblemInfo> problems = new ArrayList<>(); | |
| problems.addAll(checkCycle(g, byUUID)); | |
| if (byUUID.get(g.getOwnerGroupUUID()) == null | |
| && groupBackend.get(g.getOwnerGroupUUID()) == null) { | |
| problems.add( | |
| error( | |
| "group %s (%s) has nonexistent owner group %s", | |
| g.getName(), g.getGroupUUID(), g.getOwnerGroupUUID())); | |
| } | |
| for (AccountGroup.UUID subUuid : g.getSubgroups()) { | |
| if (byUUID.get(subUuid) == null && groupBackend.get(subUuid) == null) { | |
| problems.add( | |
| error( | |
| "group %s (%s) has nonexistent subgroup %s", | |
| g.getName(), g.getGroupUUID(), subUuid)); | |
| } | |
| } | |
| for (Account.Id id : g.getMembers().asList()) { | |
| Optional<AccountState> account; | |
| try { | |
| account = accounts.get(id); | |
| } catch (ConfigInvalidException e) { | |
| problems.add( | |
| error( | |
| "group %s (%s) has member %s with invalid configuration: %s", | |
| g.getName(), g.getGroupUUID(), id, e.getMessage())); | |
| continue; | |
| } | |
| if (!account.isPresent()) { | |
| problems.add( | |
| error("group %s (%s) has nonexistent member %s", g.getName(), g.getGroupUUID(), id)); | |
| } | |
| } | |
| return problems; | |
| } | |
| /** checkCycle walks through root's subgroups recursively, and checks for cycles. */ | |
| private List<ConsistencyProblemInfo> checkCycle( | |
| InternalGroup root, Map<AccountGroup.UUID, InternalGroup> byUUID) { | |
| List<ConsistencyProblemInfo> problems = new ArrayList<>(); | |
| Set<InternalGroup> todo = new LinkedHashSet<>(); | |
| Set<InternalGroup> seen = new HashSet<>(); | |
| todo.add(root); | |
| while (!todo.isEmpty()) { | |
| InternalGroup t = todo.iterator().next(); | |
| todo.remove(t); | |
| if (seen.contains(t)) { | |
| continue; | |
| } | |
| seen.add(t); | |
| // We don't check for owner cycles, since those are normal in self-administered groups. | |
| for (AccountGroup.UUID subUuid : t.getSubgroups()) { | |
| InternalGroup g = byUUID.get(subUuid); | |
| if (g == null) { | |
| continue; | |
| } | |
| if (Objects.equals(g, root)) { | |
| problems.add( | |
| warning( | |
| "group %s (%s) contains a cycle: %s (%s) points to it as subgroup.", | |
| root.getName(), root.getGroupUUID(), t.getName(), t.getGroupUUID())); | |
| } | |
| todo.add(g); | |
| } | |
| } | |
| return problems; | |
| } | |
| } |