Line data Source code
1 : // Copyright (C) 2010 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.permissions;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static com.google.common.flogger.LazyArgs.lazy;
20 : import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
21 : import static java.util.stream.Collectors.toCollection;
22 :
23 : import com.google.auto.value.AutoValue;
24 : import com.google.common.base.Supplier;
25 : import com.google.common.base.Suppliers;
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.ImmutableMap;
28 : import com.google.common.collect.ImmutableSet;
29 : import com.google.common.flogger.FluentLogger;
30 : import com.google.gerrit.common.Nullable;
31 : import com.google.gerrit.entities.BranchNameKey;
32 : import com.google.gerrit.entities.Change;
33 : import com.google.gerrit.entities.RefNames;
34 : import com.google.gerrit.metrics.Counter0;
35 : import com.google.gerrit.metrics.Description;
36 : import com.google.gerrit.metrics.MetricMaker;
37 : import com.google.gerrit.server.CurrentUser;
38 : import com.google.gerrit.server.config.GerritServerConfig;
39 : import com.google.gerrit.server.git.SearchingChangeCacheImpl;
40 : import com.google.gerrit.server.git.TagCache;
41 : import com.google.gerrit.server.git.TagMatcher;
42 : import com.google.gerrit.server.logging.TraceContext;
43 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
44 : import com.google.gerrit.server.notedb.ChangeNotes;
45 : import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
46 : import com.google.gerrit.server.project.ProjectState;
47 : import com.google.gerrit.server.query.change.ChangeData;
48 : import com.google.inject.Inject;
49 : import com.google.inject.assistedinject.Assisted;
50 : import java.io.IOException;
51 : import java.util.ArrayList;
52 : import java.util.Collection;
53 : import java.util.List;
54 : import java.util.Objects;
55 : import java.util.stream.Collectors;
56 : import org.eclipse.jgit.lib.Config;
57 : import org.eclipse.jgit.lib.Constants;
58 : import org.eclipse.jgit.lib.Ref;
59 : import org.eclipse.jgit.lib.Repository;
60 :
61 : class DefaultRefFilter {
62 135 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
63 :
64 : interface Factory {
65 : DefaultRefFilter create(ProjectControl projectControl);
66 : }
67 :
68 : private final TagCache tagCache;
69 : private final PermissionBackend permissionBackend;
70 : private final RefVisibilityControl refVisibilityControl;
71 : private final ProjectControl projectControl;
72 : private final CurrentUser user;
73 : private final ProjectState projectState;
74 : private final PermissionBackend.ForProject permissionBackendForProject;
75 : private final @Nullable SearchingChangeCacheImpl searchingChangeDataProvider;
76 : private final ChangeData.Factory changeDataFactory;
77 : private final ChangeNotes.Factory changeNotesFactory;
78 : private final Counter0 fullFilterCount;
79 : private final Counter0 skipFilterCount;
80 : private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
81 :
82 : @Inject
83 : DefaultRefFilter(
84 : TagCache tagCache,
85 : PermissionBackend permissionBackend,
86 : RefVisibilityControl refVisibilityControl,
87 : @GerritServerConfig Config config,
88 : MetricMaker metricMaker,
89 : @Nullable SearchingChangeCacheImpl searchingChangeDataProvider,
90 : ChangeData.Factory changeDataFactory,
91 : ChangeNotes.Factory changeNotesFactory,
92 135 : @Assisted ProjectControl projectControl) {
93 135 : this.tagCache = tagCache;
94 135 : this.permissionBackend = permissionBackend;
95 135 : this.refVisibilityControl = refVisibilityControl;
96 135 : this.searchingChangeDataProvider = searchingChangeDataProvider;
97 135 : this.changeDataFactory = changeDataFactory;
98 135 : this.changeNotesFactory = changeNotesFactory;
99 135 : this.skipFullRefEvaluationIfAllRefsAreVisible =
100 135 : config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true);
101 135 : this.projectControl = projectControl;
102 :
103 135 : this.user = projectControl.getUser();
104 135 : this.projectState = projectControl.getProjectState();
105 135 : this.permissionBackendForProject =
106 135 : permissionBackend.user(user).project(projectState.getNameKey());
107 135 : this.fullFilterCount =
108 135 : metricMaker.newCounter(
109 : "permissions/ref_filter/full_filter_count",
110 135 : new Description("Rate of full ref filter operations").setRate());
111 135 : this.skipFilterCount =
112 135 : metricMaker.newCounter(
113 : "permissions/ref_filter/skip_filter_count",
114 : new Description(
115 : "Rate of ref filter operations where we skip full evaluation"
116 : + " because the user can read all refs")
117 135 : .setRate());
118 135 : }
119 :
120 : /** Filters given refs and tags by visibility. */
121 : ImmutableList<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
122 : throws PermissionBackendException {
123 135 : logger.atFinest().log(
124 : "Filter refs for repository %s by visibility (options = %s, refs = %s)",
125 135 : projectState.getNameKey(), opts, refs);
126 135 : logger.atFinest().log("Calling user: %s", user.getLoggableName());
127 135 : logger.atFinest().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
128 135 : logger.atFinest().log(
129 : "auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
130 135 : skipFullRefEvaluationIfAllRefsAreVisible);
131 135 : logger.atFinest().log(
132 : "Project state %s permits read = %s",
133 135 : projectState.getProject().getState(), projectState.statePermitsRead());
134 :
135 : // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
136 : // we have to investigate separately (deferred tags) then perform a reachability check starting
137 : // from all visible branches (refs/heads/*).
138 135 : Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges =
139 135 : Suppliers.memoize(
140 : () ->
141 19 : GitVisibleChangeFilter.getVisibleChanges(
142 : searchingChangeDataProvider,
143 : changeNotesFactory,
144 : changeDataFactory,
145 19 : projectState.getNameKey(),
146 : permissionBackendForProject,
147 : repo,
148 19 : changes(refs)));
149 135 : Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts, visibleChanges);
150 135 : ImmutableList.Builder<Ref> visibleRefs = ImmutableList.builder();
151 135 : visibleRefs.addAll(initialRefFilter.visibleRefs());
152 135 : if (!initialRefFilter.deferredTags().isEmpty()) {
153 9 : try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
154 9 : Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts, visibleChanges);
155 9 : checkState(
156 9 : allVisibleBranches.deferredTags().isEmpty(),
157 : "unexpected tags found when filtering refs/heads/* "
158 9 : + allVisibleBranches.deferredTags());
159 :
160 9 : TagMatcher tags =
161 : tagCache
162 9 : .get(projectState.getNameKey())
163 9 : .matcher(tagCache, repo, allVisibleBranches.visibleRefs());
164 9 : for (Ref tag : initialRefFilter.deferredTags()) {
165 : try {
166 9 : if (tags.isReachable(tag)) {
167 9 : logger.atFinest().log("Include reachable tag %s", tag.getName());
168 9 : visibleRefs.add(tag);
169 : } else {
170 8 : logger.atFinest().log("Filter out non-reachable tag %s", tag.getName());
171 : }
172 0 : } catch (IOException e) {
173 0 : throw new PermissionBackendException(e);
174 9 : }
175 9 : }
176 : }
177 : }
178 :
179 135 : ImmutableList<Ref> visibleRefList = visibleRefs.build();
180 135 : logger.atFinest().log("visible refs = %s", visibleRefList);
181 135 : return visibleRefList;
182 : }
183 :
184 : /**
185 : * Filters refs by visibility. Returns tags where visibility can't be trivially computed
186 : * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
187 : * compute will be returned as part of {@link Result#visibleRefs()}.
188 : */
189 : Result filterRefs(
190 : List<Ref> refs,
191 : RefFilterOptions opts,
192 : Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges)
193 : throws PermissionBackendException {
194 135 : logger.atFinest().log("Filter refs (refs = %s)", refs);
195 135 : if (!projectState.statePermitsRead()) {
196 0 : return new AutoValue_DefaultRefFilter_Result(ImmutableList.of(), ImmutableList.of());
197 : }
198 :
199 : // TODO(hiesel): Remove when optimization is done.
200 135 : boolean hasReadOnRefsStar =
201 135 : checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
202 135 : logger.atFinest().log("User has READ on refs/* = %s", hasReadOnRefsStar);
203 135 : if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
204 134 : if (hasReadOnRefsStar) {
205 133 : skipFilterCount.increment();
206 133 : logger.atFinest().log(
207 : "Fast path, all refs are visible because user has READ on refs/*: %s", refs);
208 133 : return new AutoValue_DefaultRefFilter_Result(
209 133 : ImmutableList.copyOf(refs), ImmutableList.of());
210 31 : } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
211 7 : skipFilterCount.increment();
212 7 : refs = fastHideRefsMetaConfig(refs);
213 7 : logger.atFinest().log(
214 : "Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
215 7 : return new AutoValue_DefaultRefFilter_Result(
216 7 : ImmutableList.copyOf(refs), ImmutableList.of());
217 : }
218 : }
219 36 : logger.atFinest().log("Doing full ref filtering");
220 36 : fullFilterCount.increment();
221 :
222 36 : boolean hasAccessDatabase =
223 : permissionBackend
224 36 : .user(projectControl.getUser())
225 36 : .testOrFalse(GlobalPermission.ACCESS_DATABASE);
226 36 : ImmutableList.Builder<Ref> resultRefs = ImmutableList.builderWithExpectedSize(refs.size());
227 36 : ImmutableList.Builder<Ref> deferredTags = ImmutableList.builder();
228 36 : for (Ref ref : refs) {
229 36 : String refName = ref.getName();
230 : Change.Id changeId;
231 36 : if (opts.filterMeta() && isMetadata(refName)) {
232 1 : logger.atFinest().log("Filter out metadata ref %s", refName);
233 36 : } else if (isTag(ref)) {
234 9 : if (hasReadOnRefsStar) {
235 : // The user has READ on refs/* with no effective block permission. This is the broadest
236 : // permission one can assign. There is no way to grant access to (specific) tags in
237 : // Gerrit,
238 : // so we have to assume that these users can see all tags because there could be tags that
239 : // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
240 : // matches Gerrit's historic behavior.
241 : // This makes it so that these users could see commits that they can't see otherwise
242 : // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
243 : // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
244 : // is a negligible risk.
245 6 : logger.atFinest().log("Include tag ref %s because user has read on refs/*", refName);
246 6 : resultRefs.add(ref);
247 : } else {
248 : // If its a tag, consider it later.
249 9 : if (ref.getObjectId() != null) {
250 9 : logger.atFinest().log("Defer tag ref %s", refName);
251 9 : deferredTags.add(ref);
252 : } else {
253 0 : logger.atFinest().log("Filter out tag ref %s that is not a tag", refName);
254 : }
255 : }
256 36 : } else if ((changeId = Change.Id.fromRef(refName)) != null) {
257 : // This is a mere performance optimization. RefVisibilityControl could determine the
258 : // visibility of these refs just fine. But instead, we use highly-optimized logic that
259 : // looks only on the available changes in the change index and cache (which are the
260 : // most recent changes).
261 19 : if (hasAccessDatabase) {
262 3 : resultRefs.add(ref);
263 19 : } else if (!visibleChanges.get().containsKey(changeId)) {
264 9 : logger.atFinest().log("Filter out invisible change ref %s", refName);
265 18 : } else if (RefNames.isRefsEdit(refName) && !visibleEdit(refName, visibleChanges.get())) {
266 1 : logger.atFinest().log("Filter out invisible change edit ref %s", refName);
267 : } else {
268 : // Change is visible
269 18 : resultRefs.add(ref);
270 : }
271 36 : } else if (refVisibilityControl.isVisible(projectControl, ref.getLeaf().getName())) {
272 34 : resultRefs.add(ref);
273 : }
274 36 : }
275 36 : Result result = new AutoValue_DefaultRefFilter_Result(resultRefs.build(), deferredTags.build());
276 36 : logger.atFinest().log("Result of ref filtering = %s", result);
277 36 : return result;
278 : }
279 :
280 : /**
281 : * Returns all refs tag we regard as starting points for reachability computation for tags. In
282 : * general, these are all refs not managed by Gerrit excluding symbolic refs and tags.
283 : *
284 : * <p>We exclude symbolic refs because their target will be included and this will suffice for
285 : * computing reachability.
286 : */
287 : private static List<Ref> getTaggableRefs(Repository repo) throws PermissionBackendException {
288 : try {
289 9 : List<Ref> allRefs = repo.getRefDatabase().getRefs();
290 9 : return allRefs.stream()
291 9 : .filter(
292 : r ->
293 9 : !RefNames.isGerritRef(r.getName())
294 9 : && !r.getName().startsWith(RefNames.REFS_TAGS)
295 9 : && !r.isSymbolic()
296 9 : && !r.getName().equals(RefNames.REFS_CONFIG))
297 9 : .collect(Collectors.toList());
298 0 : } catch (IOException e) {
299 0 : throw new PermissionBackendException(e);
300 : }
301 : }
302 :
303 : /**
304 : * Returns the number of changes contained in {@code refs}. A change has one meta ref and many
305 : * patch set refs. We count over the meta refs to make sure we get the number of unique changes in
306 : * the provided refs.
307 : */
308 : private static ImmutableSet<Change.Id> changes(Collection<Ref> refs) {
309 19 : return refs.stream()
310 19 : .map(Ref::getName)
311 19 : .map(Change.Id::fromRef)
312 19 : .filter(Objects::nonNull)
313 19 : .collect(toImmutableSet());
314 : }
315 :
316 : private List<Ref> fastHideRefsMetaConfig(List<Ref> refs) throws PermissionBackendException {
317 7 : if (!canReadRef(REFS_CONFIG)) {
318 7 : return refs.stream()
319 7 : .filter(r -> !r.getName().equals(REFS_CONFIG))
320 7 : .collect(toCollection(() -> new ArrayList<>(refs.size())));
321 : }
322 0 : return refs;
323 : }
324 :
325 : private boolean visibleEdit(String name, ImmutableMap<Change.Id, ChangeData> visibleChanges)
326 : throws PermissionBackendException {
327 1 : Change.Id id = Change.Id.fromEditRefPart(name);
328 1 : if (id == null) {
329 0 : logger.atWarning().log("Couldn't extract change ID from edit ref %s", name);
330 0 : return false;
331 : }
332 :
333 1 : if (user.isIdentifiedUser()
334 1 : && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
335 1 : && visibleChanges.containsKey(id)) {
336 1 : logger.atFinest().log("Own change edit ref is visible: %s", name);
337 1 : return true;
338 : }
339 :
340 1 : if (visibleChanges.containsKey(id)) {
341 : // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
342 1 : BranchNameKey dest = visibleChanges.get(id).change().getDest();
343 1 : boolean canRead =
344 1 : permissionBackendForProject.ref(dest.branch()).test(RefPermission.READ_PRIVATE_CHANGES);
345 1 : logger.atFinest().log(
346 1 : "Foreign change edit ref is " + (canRead ? "visible" : "invisible") + ": %s", name);
347 1 : return canRead;
348 : }
349 :
350 0 : logger.atFinest().log("Change %d of change edit ref %s is not visible", id.get(), name);
351 0 : return false;
352 : }
353 :
354 : private boolean isMetadata(String name) {
355 4 : boolean isMetaData = RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
356 4 : logger.atFinest().log("ref %s is " + (isMetaData ? "" : "not ") + "a metadata ref", name);
357 4 : return isMetaData;
358 : }
359 :
360 : private static boolean isTag(Ref ref) {
361 36 : return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
362 : }
363 :
364 : private boolean canReadRef(String ref) throws PermissionBackendException {
365 7 : return permissionBackendForProject.ref(ref).test(RefPermission.READ);
366 : }
367 :
368 : private boolean checkProjectPermission(
369 : PermissionBackend.ForProject forProject, ProjectPermission perm)
370 : throws PermissionBackendException {
371 135 : return forProject.test(perm);
372 : }
373 :
374 : @AutoValue
375 135 : abstract static class Result {
376 : /** Subset of the refs passed into the computation that is visible to the user. */
377 : abstract ImmutableList<Ref> visibleRefs();
378 :
379 : /**
380 : * List of tags where we couldn't figure out visibility in the first pass and need to do an
381 : * expensive ref walk.
382 : */
383 : abstract ImmutableList<Ref> deferredTags();
384 : }
385 : }
|