LCOV - code coverage report
Current view: top level - server/permissions - PermissionCollection.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 100 102 98.0 %
Date: 2022-11-19 15:00:39 Functions: 10 11 90.9 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750