summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/permissions/PermissionCollection.java
blob: 1a3198d84ef85bd3edeb8a2346bb84de572d3f15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
// Copyright (C) 2011 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.permissions;

import static com.google.gerrit.common.data.PermissionRule.Action.BLOCK;
import static com.google.gerrit.server.project.RefPattern.isRE;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;

import com.google.auto.value.AutoValue;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.RefPattern;
import com.google.gerrit.server.project.RefPatternMatcher.ExpandParameters;
import com.google.gerrit.server.project.SectionMatcher;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Effective permissions applied to a reference in a project.
 *
 * <p>A collection may be user specific if a matching {@link AccessSection} uses "${username}" in
 * its name. The permissions granted in that section may only be granted to the username that
 * appears in the reference name, and also only if the user is a member of the relevant group.
 */
public class PermissionCollection {
  @Singleton
  public static class Factory {
    private final SectionSortCache sorter;
    // TODO(hiesel): Remove this once we got production data
    private final Timer0 filterLatency;

    @Inject
    Factory(SectionSortCache sorter, MetricMaker metricMaker) {
      this.sorter = sorter;
      this.filterLatency =
          metricMaker.newTimer(
              "permissions/permission_collection/filter_latency",
              new Description("Latency for access filter computations in PermissionCollection")
                  .setCumulative()
                  .setUnit(Units.NANOSECONDS));
    }

    /**
     * Drop the SectionMatchers that don't apply to the current ref. The user is only used for
     * expanding per-user ref patterns, and not for checking group memberships.
     *
     * @param matcherList the input sections.
     * @param ref the ref name for which to filter.
     * @param user Only used for expanding per-user ref patterns.
     * @param out the filtered sections.
     * @return true if the result is only valid for this user.
     */
    private static boolean filterRefMatchingSections(
        Iterable<SectionMatcher> matcherList,
        String ref,
        CurrentUser user,
        Map<AccessSection, Project.NameKey> out) {
      boolean perUser = false;
      for (SectionMatcher sm : matcherList) {
        // If the matcher has to expand parameters and its prefix matches the
        // reference there is a very good chance the reference is actually user
        // specific, even if the matcher does not match the reference. Since its
        // difficult to prove this is true all of the time, use an approximation
        // to prevent reuse of collections across users accessing the same
        // reference at the same time.
        //
        // This check usually gets caching right, as most per-user references
        // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
        // that will never be shared with non-user references, and the per-user
        // references are usually less frequent than the non-user references.
        if (sm.getMatcher() instanceof ExpandParameters) {
          if (!((ExpandParameters) sm.getMatcher()).matchPrefix(ref)) {
            continue;
          }
          perUser = true;
          if (sm.match(ref, user)) {
            out.put(sm.getSection(), sm.getProject());
          }
        } else if (sm.match(ref, null)) {
          out.put(sm.getSection(), sm.getProject());
        }
      }
      return perUser;
    }

    /**
     * Get all permissions that apply to a reference. The user is only used for per-user ref names,
     * so the return value may include permissions for groups the user is not part of.
     *
     * @param matcherList collection of sections that should be considered, in priority order
     *     (project specific definitions must appear before inherited ones).
     * @param ref reference being accessed.
     * @param user if the reference is a per-user reference, e.g. access sections using the
     *     parameter variable "${username}" will have each username inserted into them to see if
     *     they apply to the reference named by {@code ref}.
     * @return map of permissions that apply to this reference, keyed by permission name.
     */
    PermissionCollection filter(
        Iterable<SectionMatcher> matcherList, String ref, CurrentUser user) {
      try (Timer0.Context ignored = filterLatency.start()) {
        if (isRE(ref)) {
          ref = RefPattern.shortestExample(ref);
        } else if (ref.endsWith("/*")) {
          ref = ref.substring(0, ref.length() - 1);
        }

        // LinkedHashMap to maintain input ordering.
        Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>();
        boolean perUser = filterRefMatchingSections(matcherList, ref, user, sectionToProject);
        List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());

        // Sort by ref pattern specificity. For equally specific patterns, the sections from the
        // project closer to the current one come first.
        sorter.sort(ref, sections);

        // For block permissions, we want a different order: first, we want to go from parent to
        // child.
        List<Map.Entry<AccessSection, Project.NameKey>> accessDescending =
            Lists.reverse(Lists.newArrayList(sectionToProject.entrySet()));

        Map<Project.NameKey, List<AccessSection>> accessByProject =
            accessDescending.stream()
                .collect(
                    Collectors.groupingBy(
                        Map.Entry::getValue,
                        LinkedHashMap::new,
                        mapping(Map.Entry::getKey, toList())));
        // Within each project, sort by ref specificity.
        for (List<AccessSection> secs : accessByProject.values()) {
          sorter.sort(ref, secs);
        }

        return new PermissionCollection(
            Lists.newArrayList(accessByProject.values()), sections, perUser);
      }
    }
  }

  /** Returns permissions in the right order for evaluating BLOCK status. */
  List<List<Permission>> getBlockRules(String perm) {
    List<List<Permission>> ps = blockPerProjectByPermission.get(perm);
    if (ps == null) {
      ps = calculateBlockRules(perm);
      blockPerProjectByPermission.put(perm, ps);
    }
    return ps;
  }

  /** Returns permissions in the right order for evaluating ALLOW/DENY status. */
  List<PermissionRule> getAllowRules(String perm) {
    List<PermissionRule> ps = rulesByPermission.get(perm);
    if (ps == null) {
      ps = calculateAllowRules(perm);
      rulesByPermission.put(perm, ps);
    }
    return ps;
  }

  /** calculates permissions for ALLOW processing. */
  private List<PermissionRule> calculateAllowRules(String permName) {
    Set<SeenRule> seen = new HashSet<>();

    List<PermissionRule> r = new ArrayList<>();
    for (AccessSection s : accessSectionsUpward) {
      Permission p = s.getPermission(permName);
      if (p == null) {
        continue;
      }
      for (PermissionRule pr : p.getRules()) {
        SeenRule sr = SeenRule.create(s, pr);
        if (seen.contains(sr)) {
          // We allow only one rule per (ref-pattern, group) tuple. This is used to implement DENY:
          // If we see a DENY before an ALLOW rule, that causes the ALLOW rule to be skipped here,
          // negating access.
          continue;
        }
        seen.add(sr);

        if (pr.getAction() == BLOCK) {
          // Block rules are handled elsewhere.
          continue;
        }

        if (pr.getAction() == PermissionRule.Action.DENY) {
          // DENY rules work by not adding ALLOW rules. Nothing else to do.
          continue;
        }
        r.add(pr);
      }
      if (p.getExclusiveGroup()) {
        // We found an exclusive permission, so no need to further go up the hierarchy.
        break;
      }
    }
    return r;
  }

  // Calculates the inputs for determining BLOCK status, grouped by project.
  private List<List<Permission>> calculateBlockRules(String permName) {
    List<List<Permission>> result = new ArrayList<>();
    for (List<AccessSection> secs : this.accessSectionsPerProjectDownward) {
      List<Permission> perms = new ArrayList<>();
      boolean blockFound = false;
      for (AccessSection sec : secs) {
        Permission p = sec.getPermission(permName);
        if (p == null) {
          continue;
        }
        for (PermissionRule pr : p.getRules()) {
          if (blockFound || pr.getAction() == Action.BLOCK) {
            blockFound = true;
            break;
          }
        }

        perms.add(p);
      }

      if (blockFound) {
        result.add(perms);
      }
    }
    return result;
  }

  private List<List<AccessSection>> accessSectionsPerProjectDownward;
  private List<AccessSection> accessSectionsUpward;

  private final Map<String, List<PermissionRule>> rulesByPermission;
  private final Map<String, List<List<Permission>>> blockPerProjectByPermission;
  private final boolean perUser;

  private PermissionCollection(
      List<List<AccessSection>> accessSectionsDownward,
      List<AccessSection> accessSectionsUpward,
      boolean perUser) {
    this.accessSectionsPerProjectDownward = accessSectionsDownward;
    this.accessSectionsUpward = accessSectionsUpward;
    this.rulesByPermission = new HashMap<>();
    this.blockPerProjectByPermission = new HashMap<>();
    this.perUser = perUser;
  }

  /**
   * @return true if a "${username}" pattern might need to be expanded to build this collection,
   *     making the results user specific.
   */
  public boolean isUserSpecific() {
    return perUser;
  }

  /** (ref, permission, group) tuple. */
  @AutoValue
  abstract static class SeenRule {
    public abstract String refPattern();

    @Nullable
    public abstract AccountGroup.UUID group();

    static SeenRule create(AccessSection section, @Nullable PermissionRule rule) {
      AccountGroup.UUID group =
          rule != null && rule.getGroup() != null ? rule.getGroup().getUUID() : null;
      return new AutoValue_PermissionCollection_SeenRule(section.getName(), group);
    }
  }
}