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 : }
|