Line data Source code
1 : // Copyright (C) 2011 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.gerrit.entities.PermissionRule.Action.BLOCK;
18 : import static com.google.gerrit.server.project.RefPattern.containsParameters;
19 : import static com.google.gerrit.server.project.RefPattern.isRE;
20 : import static java.util.stream.Collectors.mapping;
21 : import static java.util.stream.Collectors.toList;
22 :
23 : import com.google.auto.value.AutoValue;
24 : import com.google.common.collect.Lists;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.entities.AccessSection;
27 : import com.google.gerrit.entities.AccountGroup;
28 : import com.google.gerrit.entities.Permission;
29 : import com.google.gerrit.entities.PermissionRule;
30 : import com.google.gerrit.entities.PermissionRule.Action;
31 : import com.google.gerrit.entities.Project;
32 : import com.google.gerrit.metrics.Description;
33 : import com.google.gerrit.metrics.Description.Units;
34 : import com.google.gerrit.metrics.MetricMaker;
35 : import com.google.gerrit.metrics.Timer0;
36 : import com.google.gerrit.server.CurrentUser;
37 : import com.google.gerrit.server.project.RefPattern;
38 : import com.google.gerrit.server.project.RefPatternMatcher.ExpandParameters;
39 : import com.google.gerrit.server.project.SectionMatcher;
40 : import com.google.inject.Inject;
41 : import com.google.inject.Singleton;
42 : import java.util.ArrayList;
43 : import java.util.HashMap;
44 : import java.util.HashSet;
45 : import java.util.LinkedHashMap;
46 : import java.util.List;
47 : import java.util.Map;
48 : import java.util.Set;
49 : import java.util.stream.Collectors;
50 :
51 : /**
52 : * Effective permissions applied to a reference in a project.
53 : *
54 : * <p>A collection may be user specific if a matching {@link AccessSection} uses "${username}" in
55 : * its name. The permissions granted in that section may only be granted to the username that
56 : * appears in the reference name, and also only if the user is a member of the relevant group.
57 : */
58 : public class PermissionCollection {
59 : @Singleton
60 : public static class Factory {
61 : private final SectionSortCache sorter;
62 : // TODO(hiesel): Remove this once we got production data
63 : private final Timer0 filterLatency;
64 :
65 : @Inject
66 148 : Factory(SectionSortCache sorter, MetricMaker metricMaker) {
67 148 : this.sorter = sorter;
68 148 : this.filterLatency =
69 148 : metricMaker.newTimer(
70 : "permissions/permission_collection/filter_latency",
71 : new Description("Latency for access filter computations in PermissionCollection")
72 148 : .setCumulative()
73 148 : .setUnit(Units.NANOSECONDS));
74 148 : }
75 :
76 : /**
77 : * Drop the SectionMatchers that don't apply to the current ref. The user is only used for
78 : * expanding per-user ref patterns, and not for checking group memberships.
79 : *
80 : * @param matcherList the input sections.
81 : * @param ref the ref name for which to filter.
82 : * @param user Only used for expanding per-user ref patterns.
83 : * @param out the filtered sections.
84 : * @return true if the result is only valid for this user.
85 : */
86 : private static boolean filterRefMatchingSections(
87 : Iterable<SectionMatcher> matcherList,
88 : String ref,
89 : CurrentUser user,
90 : Map<AccessSection, Project.NameKey> out) {
91 145 : boolean perUser = false;
92 145 : for (SectionMatcher sm : matcherList) {
93 : // If the matcher has to expand parameters and its prefix matches the
94 : // reference there is a very good chance the reference is actually user
95 : // specific, even if the matcher does not match the reference. Since its
96 : // difficult to prove this is true all of the time, use an approximation
97 : // to prevent reuse of collections across users accessing the same
98 : // reference at the same time.
99 : //
100 : // This check usually gets caching right, as most per-user references
101 : // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
102 : // that will never be shared with non-user references, and the per-user
103 : // references are usually less frequent than the non-user references.
104 145 : if (sm.getMatcher() instanceof ExpandParameters) {
105 52 : if (!((ExpandParameters) sm.getMatcher()).matchPrefix(ref)) {
106 49 : continue;
107 : }
108 47 : perUser = true;
109 47 : if (sm.match(ref, user)) {
110 21 : out.put(sm.getSection(), sm.getProject());
111 : }
112 145 : } else if (sm.match(ref, null)) {
113 145 : out.put(sm.getSection(), sm.getProject());
114 : }
115 145 : }
116 145 : return perUser;
117 : }
118 :
119 : /**
120 : * Get all permissions that apply to a reference. The user is only used for per-user ref names,
121 : * so the return value may include permissions for groups the user is not part of.
122 : *
123 : * @param matcherList collection of sections that should be considered, in priority order
124 : * (project specific definitions must appear before inherited ones).
125 : * @param ref reference being accessed.
126 : * @param user if the reference is a per-user reference, e.g. access sections using the
127 : * parameter variable "${username}" will have each username inserted into them to see if
128 : * they apply to the reference named by {@code ref}.
129 : * @return map of permissions that apply to this reference, keyed by permission name.
130 : */
131 : PermissionCollection filter(
132 : Iterable<SectionMatcher> matcherList, String ref, CurrentUser user) {
133 145 : try (Timer0.Context ignored = filterLatency.start()) {
134 145 : if (isRE(ref)) {
135 1 : if (!containsParameters(ref)) {
136 0 : ref = RefPattern.shortestExample(ref);
137 : }
138 145 : } else if (ref.endsWith("/*")) {
139 144 : ref = ref.substring(0, ref.length() - 1);
140 : }
141 :
142 : // LinkedHashMap to maintain input ordering.
143 145 : Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>();
144 145 : boolean perUser = filterRefMatchingSections(matcherList, ref, user, sectionToProject);
145 145 : List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
146 :
147 : // Sort by ref pattern specificity. For equally specific patterns, the sections from the
148 : // project closer to the current one come first.
149 145 : sorter.sort(ref, sections);
150 :
151 : // For block permissions, we want a different order: first, we want to go from parent to
152 : // child.
153 145 : List<Map.Entry<AccessSection, Project.NameKey>> accessDescending =
154 145 : Lists.reverse(Lists.newArrayList(sectionToProject.entrySet()));
155 :
156 145 : Map<Project.NameKey, List<AccessSection>> accessByProject =
157 145 : accessDescending.stream()
158 145 : .collect(
159 145 : Collectors.groupingBy(
160 : Map.Entry::getValue,
161 : LinkedHashMap::new,
162 145 : mapping(Map.Entry::getKey, toList())));
163 : // Within each project, sort by ref specificity.
164 145 : for (List<AccessSection> secs : accessByProject.values()) {
165 145 : sorter.sort(ref, secs);
166 145 : }
167 :
168 145 : return new PermissionCollection(
169 145 : Lists.newArrayList(accessByProject.values()), sections, perUser);
170 : }
171 : }
172 : }
173 :
174 : /** Returns permissions in the right order for evaluating BLOCK status. */
175 : List<List<Permission>> getBlockRules(String perm) {
176 145 : List<List<Permission>> ps = blockPerProjectByPermission.get(perm);
177 145 : if (ps == null) {
178 145 : ps = calculateBlockRules(perm);
179 145 : blockPerProjectByPermission.put(perm, ps);
180 : }
181 145 : return ps;
182 : }
183 :
184 : /** Returns permissions in the right order for evaluating ALLOW/DENY status. */
185 : List<PermissionRule> getAllowRules(String perm) {
186 145 : List<PermissionRule> ps = rulesByPermission.get(perm);
187 145 : if (ps == null) {
188 145 : ps = calculateAllowRules(perm);
189 145 : rulesByPermission.put(perm, ps);
190 : }
191 145 : return ps;
192 : }
193 :
194 : /** calculates permissions for ALLOW processing. */
195 : private List<PermissionRule> calculateAllowRules(String permName) {
196 145 : Set<SeenRule> seen = new HashSet<>();
197 :
198 145 : List<PermissionRule> r = new ArrayList<>();
199 145 : for (AccessSection s : accessSectionsUpward) {
200 145 : Permission p = s.getPermission(permName);
201 145 : if (p == null) {
202 122 : continue;
203 : }
204 145 : for (PermissionRule pr : p.getRules()) {
205 145 : SeenRule sr = SeenRule.create(s, pr);
206 145 : if (seen.contains(sr)) {
207 : // We allow only one rule per (ref-pattern, group) tuple. This is used to implement DENY:
208 : // If we see a DENY before an ALLOW rule, that causes the ALLOW rule to be skipped here,
209 : // negating access.
210 27 : continue;
211 : }
212 145 : seen.add(sr);
213 :
214 145 : if (pr.getAction() == BLOCK) {
215 : // Block rules are handled elsewhere.
216 22 : continue;
217 : }
218 :
219 145 : if (pr.getAction() == PermissionRule.Action.DENY) {
220 : // DENY rules work by not adding ALLOW rules. Nothing else to do.
221 5 : continue;
222 : }
223 145 : r.add(pr);
224 145 : }
225 145 : if (p.getExclusiveGroup()) {
226 : // We found an exclusive permission, so no need to further go up the hierarchy.
227 138 : break;
228 : }
229 145 : }
230 145 : return r;
231 : }
232 :
233 : // Calculates the inputs for determining BLOCK status, grouped by project.
234 : private List<List<Permission>> calculateBlockRules(String permName) {
235 145 : List<List<Permission>> result = new ArrayList<>();
236 145 : for (List<AccessSection> secs : this.accessSectionsPerProjectDownward) {
237 145 : List<Permission> perms = new ArrayList<>();
238 145 : boolean blockFound = false;
239 145 : for (AccessSection sec : secs) {
240 145 : Permission p = sec.getPermission(permName);
241 145 : if (p == null) {
242 122 : continue;
243 : }
244 145 : for (PermissionRule pr : p.getRules()) {
245 145 : if (blockFound || pr.getAction() == Action.BLOCK) {
246 49 : blockFound = true;
247 49 : break;
248 : }
249 145 : }
250 :
251 145 : perms.add(p);
252 145 : }
253 :
254 145 : if (blockFound) {
255 49 : result.add(perms);
256 : }
257 145 : }
258 145 : return result;
259 : }
260 :
261 : private List<List<AccessSection>> accessSectionsPerProjectDownward;
262 : private List<AccessSection> accessSectionsUpward;
263 :
264 : private final Map<String, List<PermissionRule>> rulesByPermission;
265 : private final Map<String, List<List<Permission>>> blockPerProjectByPermission;
266 : private final boolean perUser;
267 :
268 : private PermissionCollection(
269 : List<List<AccessSection>> accessSectionsDownward,
270 : List<AccessSection> accessSectionsUpward,
271 145 : boolean perUser) {
272 145 : this.accessSectionsPerProjectDownward = accessSectionsDownward;
273 145 : this.accessSectionsUpward = accessSectionsUpward;
274 145 : this.rulesByPermission = new HashMap<>();
275 145 : this.blockPerProjectByPermission = new HashMap<>();
276 145 : this.perUser = perUser;
277 145 : }
278 :
279 : /**
280 : * Returns true if a "${username}" pattern might need to be expanded to build this collection,
281 : * making the results user specific.
282 : */
283 : public boolean isUserSpecific() {
284 0 : return perUser;
285 : }
286 :
287 : /** (ref, permission, group) tuple. */
288 : @AutoValue
289 145 : abstract static class SeenRule {
290 : public abstract String refPattern();
291 :
292 : @Nullable
293 : public abstract AccountGroup.UUID group();
294 :
295 : static SeenRule create(AccessSection section, @Nullable PermissionRule rule) {
296 : AccountGroup.UUID group =
297 145 : rule != null && rule.getGroup() != null ? rule.getGroup().getUUID() : null;
298 145 : return new AutoValue_PermissionCollection_SeenRule(section.getName(), group);
299 : }
300 : }
301 : }
|