| // 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.project; |
| |
| import static com.google.common.base.MoreObjects.firstNonNull; |
| import static com.google.gerrit.server.project.RefPattern.isRE; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.MultimapBuilder; |
| 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.reviewdb.client.AccountGroup; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * 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; |
| |
| @Inject |
| Factory(SectionSortCache sorter) { |
| this.sorter = sorter; |
| } |
| |
| /** |
| * Get all permissions that apply to a reference. |
| * |
| * @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) { |
| if (isRE(ref)) { |
| ref = RefPattern.shortestExample(ref); |
| } else if (ref.endsWith("/*")) { |
| ref = ref.substring(0, ref.length() - 1); |
| } |
| |
| boolean perUser = false; |
| Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>(); |
| 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.matcher instanceof RefPatternMatcher.ExpandParameters) { |
| if (!((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref)) { |
| continue; |
| } |
| perUser = true; |
| if (sm.match(ref, user)) { |
| sectionToProject.put(sm.section, sm.project); |
| } |
| } else if (sm.match(ref, null)) { |
| sectionToProject.put(sm.section, sm.project); |
| } |
| } |
| List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet()); |
| sorter.sort(ref, sections); |
| |
| Set<SeenRule> seen = new HashSet<>(); |
| Set<String> exclusiveGroupPermissions = new HashSet<>(); |
| |
| HashMap<String, List<PermissionRule>> permissions = new HashMap<>(); |
| HashMap<String, List<PermissionRule>> overridden = new HashMap<>(); |
| Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap(); |
| ListMultimap<Project.NameKey, String> exclusivePermissionsByProject = |
| MultimapBuilder.hashKeys().arrayListValues().build(); |
| for (AccessSection section : sections) { |
| Project.NameKey project = sectionToProject.get(section); |
| for (Permission permission : section.getPermissions()) { |
| boolean exclusivePermissionExists = |
| exclusiveGroupPermissions.contains(permission.getName()); |
| |
| for (PermissionRule rule : permission.getRules()) { |
| SeenRule s = SeenRule.create(section, permission, rule); |
| boolean addRule; |
| if (rule.isBlock()) { |
| addRule = !exclusivePermissionsByProject.containsEntry(project, permission.getName()); |
| } else { |
| addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists; |
| } |
| |
| HashMap<String, List<PermissionRule>> p = null; |
| if (addRule) { |
| p = permissions; |
| } else if (!rule.isDeny() && !exclusivePermissionExists) { |
| p = overridden; |
| } |
| |
| if (p != null) { |
| List<PermissionRule> r = p.get(permission.getName()); |
| if (r == null) { |
| r = new ArrayList<>(2); |
| p.put(permission.getName(), r); |
| } |
| r.add(rule); |
| ruleProps.put(rule, ProjectRef.create(project, section.getName())); |
| } |
| } |
| |
| if (permission.getExclusiveGroup()) { |
| exclusivePermissionsByProject.put(project, permission.getName()); |
| exclusiveGroupPermissions.add(permission.getName()); |
| } |
| } |
| } |
| |
| return new PermissionCollection(permissions, overridden, ruleProps, perUser); |
| } |
| } |
| |
| private final Map<String, List<PermissionRule>> rules; |
| private final Map<String, List<PermissionRule>> overridden; |
| private final Map<PermissionRule, ProjectRef> ruleProps; |
| private final boolean perUser; |
| |
| private PermissionCollection( |
| Map<String, List<PermissionRule>> rules, |
| Map<String, List<PermissionRule>> overridden, |
| Map<PermissionRule, ProjectRef> ruleProps, |
| boolean perUser) { |
| this.rules = rules; |
| this.overridden = overridden; |
| this.ruleProps = ruleProps; |
| 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; |
| } |
| |
| /** |
| * Obtain all permission rules for a given type of permission. |
| * |
| * @param permissionName type of permission. |
| * @return all rules that apply to this reference, for any group. Never null; the empty list is |
| * returned when there are no rules for the requested permission name. |
| */ |
| public List<PermissionRule> getPermission(String permissionName) { |
| List<PermissionRule> r = rules.get(permissionName); |
| return r != null ? r : Collections.<PermissionRule>emptyList(); |
| } |
| |
| List<PermissionRule> getOverridden(String permissionName) { |
| return firstNonNull(overridden.get(permissionName), Collections.<PermissionRule>emptyList()); |
| } |
| |
| ProjectRef getRuleProps(PermissionRule rule) { |
| return ruleProps.get(rule); |
| } |
| |
| /** |
| * Obtain all declared permission rules that match the reference. |
| * |
| * @return all rules. The collection will iterate a permission if it was declared in the project |
| * configuration, either directly or inherited. If the project owner did not use a known |
| * permission (for example {@link Permission#FORGE_SERVER}, then it will not be represented in |
| * the result even if {@link #getPermission(String)} returns an empty list for the same |
| * permission. |
| */ |
| public Iterable<Map.Entry<String, List<PermissionRule>>> getDeclaredPermissions() { |
| return rules.entrySet(); |
| } |
| |
| /** Tracks whether or not a permission has been overridden. */ |
| @AutoValue |
| abstract static class SeenRule { |
| public abstract String refPattern(); |
| |
| public abstract String permissionName(); |
| |
| @Nullable |
| public abstract AccountGroup.UUID group(); |
| |
| static SeenRule create( |
| AccessSection section, Permission permission, @Nullable PermissionRule rule) { |
| AccountGroup.UUID group = |
| rule != null && rule.getGroup() != null ? rule.getGroup().getUUID() : null; |
| return new AutoValue_PermissionCollection_SeenRule( |
| section.getName(), permission.getName(), group); |
| } |
| } |
| } |