// 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.gerrit.server.project.RefControl.isRE; import com.google.common.collect.Lists; import com.google.common.collect.Maps; 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.inject.Inject; import com.google.inject.Singleton; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; 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 usernames if the reference is a per-user reference, access sections * using the parameter variable "${username}" will first have each of * {@code usernames} inserted into them before seeing if they apply to * the reference named by {@code ref}. If null or empty, per-user * references are ignored. * @return map of permissions that apply to this reference, keyed by * permission name. */ PermissionCollection filter(Iterable<SectionMatcher> matcherList, String ref, Collection<String> usernames) { if (isRE(ref)) { ref = RefControl.shortestExample(ref); } else if (ref.endsWith("/*")) { ref = ref.substring(0, ref.length() - 1); } boolean hasUsernames = usernames != null && !usernames.isEmpty(); boolean perUser = false; Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap(); 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 (hasUsernames) { if (!perUser && sm.matcher instanceof RefPatternMatcher.ExpandParameters) { perUser = ((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref); } for (String username : usernames) { if (sm.match(ref, username)) { sectionToProject.put(sm.section, sm.project); break; } } } 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<SeenRule>(); Set<String> exclusiveGroupPermissions = new HashSet<>(); HashMap<String, List<PermissionRule>> permissions = new HashMap<>(); Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap(); 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 = new SeenRule(section, permission, rule); boolean addRule; if (rule.isBlock()) { addRule = true; } else { addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists; } if (addRule) { List<PermissionRule> r = permissions.get(permission.getName()); if (r == null) { r = new ArrayList<>(2); permissions.put(permission.getName(), r); } r.add(rule); ruleProps.put(rule, new ProjectRef(project, section.getName())); } } if (permission.getExclusiveGroup()) { exclusiveGroupPermissions.add(permission.getName()); } } } return new PermissionCollection(permissions, ruleProps, perUser); } } private final Map<String, List<PermissionRule>> rules; private final Map<PermissionRule, ProjectRef> ruleProps; private final boolean perUser; private PermissionCollection(Map<String, List<PermissionRule>> rules, Map<PermissionRule, ProjectRef> ruleProps, boolean perUser) { this.rules = rules; 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(); } 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. */ private static class SeenRule { final String refPattern; final String permissionName; final AccountGroup.UUID group; SeenRule(AccessSection section, Permission permission, PermissionRule rule) { refPattern = section.getName(); permissionName = permission.getName(); group = rule.getGroup().getUUID(); } @Override public int hashCode() { int hc = refPattern.hashCode(); hc = hc * 31 + permissionName.hashCode(); if (group != null) { hc = hc * 31 + group.hashCode(); } return hc; } @Override public boolean equals(Object other) { if (other instanceof SeenRule) { SeenRule a = this; SeenRule b = (SeenRule) other; return a.refPattern.equals(b.refPattern) // && a.permissionName.equals(b.permissionName) // && eq(a.group, b.group); } return false; } private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) { return a != null && b != null && a.equals(b); } } }