| // 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; |
| } |
| } |