LCOV - code coverage report
Current view: top level - server/restapi/project - GetAccess.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 133 148 89.9 %
Date: 2022-11-19 15:00:39 Functions: 11 12 91.7 %

          Line data    Source code
       1             : // Copyright (C) 2016 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.restapi.project;
      16             : 
      17             : import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
      18             : import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_REF;
      19             : import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_TAG_REF;
      20             : import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
      21             : import static com.google.gerrit.server.permissions.RefPermission.READ;
      22             : import static com.google.gerrit.server.permissions.RefPermission.WRITE_CONFIG;
      23             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      24             : import static java.util.stream.Collectors.toMap;
      25             : 
      26             : import com.google.common.collect.ImmutableBiMap;
      27             : import com.google.common.collect.Iterables;
      28             : import com.google.common.flogger.FluentLogger;
      29             : import com.google.gerrit.common.Nullable;
      30             : import com.google.gerrit.entities.AccessSection;
      31             : import com.google.gerrit.entities.AccountGroup;
      32             : import com.google.gerrit.entities.GroupDescription;
      33             : import com.google.gerrit.entities.Permission;
      34             : import com.google.gerrit.entities.PermissionRule;
      35             : import com.google.gerrit.entities.Project;
      36             : import com.google.gerrit.entities.RefNames;
      37             : import com.google.gerrit.extensions.api.access.AccessSectionInfo;
      38             : import com.google.gerrit.extensions.api.access.PermissionInfo;
      39             : import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
      40             : import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
      41             : import com.google.gerrit.extensions.common.GroupInfo;
      42             : import com.google.gerrit.extensions.restapi.AuthException;
      43             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      44             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      45             : import com.google.gerrit.extensions.restapi.Response;
      46             : import com.google.gerrit.extensions.restapi.RestReadView;
      47             : import com.google.gerrit.server.CurrentUser;
      48             : import com.google.gerrit.server.WebLinks;
      49             : import com.google.gerrit.server.account.GroupBackend;
      50             : import com.google.gerrit.server.config.AllProjectsName;
      51             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      52             : import com.google.gerrit.server.permissions.GlobalPermission;
      53             : import com.google.gerrit.server.permissions.PermissionBackend;
      54             : import com.google.gerrit.server.permissions.PermissionBackendException;
      55             : import com.google.gerrit.server.permissions.ProjectPermission;
      56             : import com.google.gerrit.server.permissions.RefPermission;
      57             : import com.google.gerrit.server.project.ProjectCache;
      58             : import com.google.gerrit.server.project.ProjectConfig;
      59             : import com.google.gerrit.server.project.ProjectJson;
      60             : import com.google.gerrit.server.project.ProjectResource;
      61             : import com.google.gerrit.server.project.ProjectState;
      62             : import com.google.inject.Inject;
      63             : import com.google.inject.Provider;
      64             : import com.google.inject.Singleton;
      65             : import java.io.IOException;
      66             : import java.util.ArrayList;
      67             : import java.util.HashMap;
      68             : import java.util.HashSet;
      69             : import java.util.Map;
      70             : import org.eclipse.jgit.errors.ConfigInvalidException;
      71             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      72             : 
      73             : @Singleton
      74             : public class GetAccess implements RestReadView<ProjectResource> {
      75         146 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      76             : 
      77         146 :   public static final ImmutableBiMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
      78         146 :       ImmutableBiMap.of(
      79             :           PermissionRule.Action.ALLOW,
      80             :           PermissionRuleInfo.Action.ALLOW,
      81             :           PermissionRule.Action.BATCH,
      82             :           PermissionRuleInfo.Action.BATCH,
      83             :           PermissionRule.Action.BLOCK,
      84             :           PermissionRuleInfo.Action.BLOCK,
      85             :           PermissionRule.Action.DENY,
      86             :           PermissionRuleInfo.Action.DENY,
      87             :           PermissionRule.Action.INTERACTIVE,
      88             :           PermissionRuleInfo.Action.INTERACTIVE);
      89             : 
      90             :   private final Provider<CurrentUser> user;
      91             :   private final PermissionBackend permissionBackend;
      92             :   private final AllProjectsName allProjectsName;
      93             :   private final ProjectJson projectJson;
      94             :   private final ProjectCache projectCache;
      95             :   private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory;
      96             :   private final GroupBackend groupBackend;
      97             :   private final WebLinks webLinks;
      98             :   private final ProjectConfig.Factory projectConfigFactory;
      99             : 
     100             :   @Inject
     101             :   public GetAccess(
     102             :       Provider<CurrentUser> self,
     103             :       PermissionBackend permissionBackend,
     104             :       AllProjectsName allProjectsName,
     105             :       ProjectCache projectCache,
     106             :       Provider<MetaDataUpdate.Server> metaDataUpdateFactory,
     107             :       ProjectJson projectJson,
     108             :       GroupBackend groupBackend,
     109             :       WebLinks webLinks,
     110         146 :       ProjectConfig.Factory projectConfigFactory) {
     111         146 :     this.user = self;
     112         146 :     this.permissionBackend = permissionBackend;
     113         146 :     this.allProjectsName = allProjectsName;
     114         146 :     this.projectJson = projectJson;
     115         146 :     this.projectCache = projectCache;
     116         146 :     this.metaDataUpdateFactory = metaDataUpdateFactory;
     117         146 :     this.groupBackend = groupBackend;
     118         146 :     this.webLinks = webLinks;
     119         146 :     this.projectConfigFactory = projectConfigFactory;
     120         146 :   }
     121             : 
     122             :   public ProjectAccessInfo apply(Project.NameKey nameKey) throws Exception {
     123           9 :     ProjectState state =
     124           9 :         projectCache.get(nameKey).orElseThrow(() -> new ResourceNotFoundException(nameKey.get()));
     125           9 :     return apply(new ProjectResource(state, user.get())).value();
     126             :   }
     127             : 
     128             :   @Override
     129             :   public Response<ProjectAccessInfo> apply(ProjectResource rsrc)
     130             :       throws ResourceNotFoundException, ResourceConflictException, IOException,
     131             :           PermissionBackendException {
     132             :     // Load the current configuration from the repository, ensuring it's the most
     133             :     // recent version available. If it differs from what was in the project
     134             :     // state, force a cache flush now.
     135             : 
     136           9 :     Project.NameKey projectName = rsrc.getNameKey();
     137           9 :     ProjectAccessInfo info = new ProjectAccessInfo();
     138           9 :     ProjectState projectState =
     139           9 :         projectCache.get(projectName).orElseThrow(illegalState(projectName));
     140           9 :     PermissionBackend.ForProject perm = permissionBackend.currentUser().project(projectName);
     141             : 
     142             :     ProjectConfig config;
     143           9 :     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
     144           9 :       config = projectConfigFactory.read(md);
     145           9 :       info.configWebLinks = new ArrayList<>();
     146             : 
     147             :       // config may have a null revision if the repo doesn't have its own refs/meta/config.
     148           9 :       if (config.getRevision() != null) {
     149           9 :         info.configWebLinks.addAll(
     150           9 :             webLinks.getFileHistoryLinks(
     151           9 :                 projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG));
     152             :       }
     153             : 
     154           9 :       if (config.updateGroupNames(groupBackend)) {
     155           0 :         md.setMessage("Update group names\n");
     156           0 :         config.commit(md);
     157           0 :         projectCache.evictAndReindex(config.getProject());
     158           0 :         projectState = projectCache.get(projectName).orElseThrow(illegalState(projectName));
     159           0 :         perm = permissionBackend.currentUser().project(projectName);
     160           9 :       } else if (config.getRevision() != null
     161           9 :           && !config.getRevision().equals(projectState.getConfig().getRevision().orElse(null))) {
     162           1 :         projectCache.evictAndReindex(config.getProject());
     163           1 :         projectState = projectCache.get(projectName).orElseThrow(illegalState(projectName));
     164           1 :         perm = permissionBackend.currentUser().project(projectName);
     165             :       }
     166           0 :     } catch (ConfigInvalidException e) {
     167           0 :       throw new ResourceConflictException(e.getMessage());
     168           0 :     } catch (RepositoryNotFoundException e) {
     169           0 :       throw new ResourceNotFoundException(rsrc.getName(), e);
     170           9 :     }
     171             : 
     172             :     // The following implementation must match the ProjectAccessFactory JSON RPC endpoint.
     173             : 
     174           9 :     info.local = new HashMap<>();
     175           9 :     info.ownerOf = new HashSet<>();
     176           9 :     Map<AccountGroup.UUID, GroupInfo> groups = new HashMap<>();
     177           9 :     boolean canReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
     178           9 :     boolean canWriteConfig = check(perm, ProjectPermission.WRITE_CONFIG);
     179             : 
     180             :     // Check if the project state permits read only when the user is not allowed to write the config
     181             :     // (=owner). This is so that the owner can still read (and in the next step write) the project's
     182             :     // config to set the project state to any state that is not HIDDEN.
     183           9 :     if (!canWriteConfig) {
     184           1 :       projectState.checkStatePermitsRead();
     185             :     }
     186             : 
     187           9 :     for (AccessSection section : config.getAccessSections()) {
     188           7 :       String name = section.getName();
     189           7 :       if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
     190           3 :         if (canWriteConfig) {
     191           3 :           info.local.put(name, createAccessSection(groups, section));
     192           3 :           info.ownerOf.add(name);
     193             : 
     194           0 :         } else if (canReadConfig) {
     195           0 :           info.local.put(section.getName(), createAccessSection(groups, section));
     196             :         }
     197             : 
     198           7 :       } else if (AccessSection.isValidRefSectionName(name)) {
     199           7 :         if (check(perm, name, WRITE_CONFIG)) {
     200           7 :           info.local.put(name, createAccessSection(groups, section));
     201           7 :           info.ownerOf.add(name);
     202             : 
     203           1 :         } else if (canReadConfig) {
     204           0 :           info.local.put(name, createAccessSection(groups, section));
     205             : 
     206           1 :         } else if (check(perm, name, READ)) {
     207             :           // Filter the section to only add rules describing groups that
     208             :           // are visible to the current-user. This includes any group the
     209             :           // user is a member of, as well as groups they own or that
     210             :           // are visible to all users.
     211             : 
     212           1 :           AccessSection.Builder dst = null;
     213           1 :           for (Permission srcPerm : section.getPermissions()) {
     214           1 :             Permission.Builder dstPerm = null;
     215             : 
     216           1 :             for (PermissionRule srcRule : srcPerm.getRules()) {
     217           1 :               AccountGroup.UUID groupId = srcRule.getGroup().getUUID();
     218           1 :               if (groupId == null) {
     219           0 :                 continue;
     220             :               }
     221             : 
     222           1 :               loadGroup(groups, groupId);
     223           1 :               if (dstPerm == null) {
     224           1 :                 if (dst == null) {
     225           1 :                   dst = AccessSection.builder(name);
     226           1 :                   info.local.put(name, createAccessSection(groups, dst.build()));
     227             :                 }
     228           1 :                 dstPerm = dst.upsertPermission(srcPerm.getName());
     229             :               }
     230           1 :               dstPerm.add(srcRule.toBuilder());
     231           1 :             }
     232           1 :           }
     233             :         }
     234             :       }
     235           7 :     }
     236             : 
     237           9 :     if (info.ownerOf.isEmpty()) {
     238             :       try {
     239           3 :         permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     240             :         // Special case: If the section list is empty, this project has no current
     241             :         // access control information. Fall back to site administrators.
     242           3 :         info.ownerOf.add(AccessSection.ALL);
     243           1 :       } catch (AuthException e) {
     244             :         // Do nothing.
     245           3 :       }
     246             :     }
     247             : 
     248           9 :     if (config.getRevision() != null) {
     249           9 :       info.revision = config.getRevision().name();
     250             :     }
     251             : 
     252           9 :     ProjectState parent = Iterables.getFirst(projectState.parents(), null);
     253           9 :     if (parent != null) {
     254           7 :       info.inheritsFrom = projectJson.format(parent.getProject());
     255             :     }
     256             : 
     257           9 :     if (projectName.equals(allProjectsName)
     258           3 :         && permissionBackend.currentUser().testOrFalse(ADMINISTRATE_SERVER)) {
     259           3 :       info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
     260             :     }
     261             : 
     262           9 :     info.isOwner = toBoolean(canWriteConfig);
     263           9 :     info.canUpload =
     264           9 :         toBoolean(
     265           9 :             projectState.statePermitsWrite()
     266             :                 && (canWriteConfig
     267             :                     || (canReadConfig
     268           9 :                         && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))));
     269           9 :     info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF));
     270           9 :     info.canAddTags = toBoolean(perm.testOrFalse(CREATE_TAG_REF));
     271           9 :     info.configVisible = canReadConfig || canWriteConfig;
     272             : 
     273           9 :     info.groups =
     274           9 :         groups.entrySet().stream()
     275           9 :             .filter(e -> e.getValue() != null)
     276           9 :             .collect(toMap(e -> e.getKey().get(), Map.Entry::getValue));
     277             : 
     278           9 :     return Response.ok(info);
     279             :   }
     280             : 
     281             :   private void loadGroup(Map<AccountGroup.UUID, GroupInfo> groups, AccountGroup.UUID id) {
     282           7 :     if (!groups.containsKey(id)) {
     283           7 :       GroupDescription.Basic basic = groupBackend.get(id);
     284             :       GroupInfo group;
     285           7 :       if (basic != null) {
     286           7 :         group = new GroupInfo();
     287             :         // The UI only needs name + URL, so don't populate other fields to avoid leaking data
     288             :         // about groups invisible to the user.
     289           7 :         group.name = basic.getName();
     290           7 :         group.url = basic.getUrl();
     291             :       } else {
     292           0 :         logger.atWarning().log("no such group: %s", id);
     293           0 :         group = null;
     294             :       }
     295           7 :       groups.put(id, group);
     296             :     }
     297           7 :   }
     298             : 
     299             :   private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
     300             :       throws PermissionBackendException {
     301             :     try {
     302           9 :       ctx.ref(ref).check(perm);
     303           9 :       return true;
     304           3 :     } catch (AuthException denied) {
     305           3 :       return false;
     306             :     }
     307             :   }
     308             : 
     309             :   private static boolean check(PermissionBackend.ForProject ctx, ProjectPermission perm)
     310             :       throws PermissionBackendException {
     311             :     try {
     312           9 :       ctx.check(perm);
     313           9 :       return true;
     314           1 :     } catch (AuthException denied) {
     315           1 :       return false;
     316             :     }
     317             :   }
     318             : 
     319             :   private AccessSectionInfo createAccessSection(
     320             :       Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) {
     321           7 :     AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
     322           7 :     accessSectionInfo.permissions = new HashMap<>();
     323           7 :     for (Permission p : section.getPermissions()) {
     324           7 :       PermissionInfo pInfo = new PermissionInfo(p.getLabel(), p.getExclusiveGroup() ? true : null);
     325           7 :       pInfo.rules = new HashMap<>();
     326           7 :       for (PermissionRule r : p.getRules()) {
     327           7 :         PermissionRuleInfo info =
     328           7 :             new PermissionRuleInfo(ACTION_TYPE.get(r.getAction()), r.getForce());
     329           7 :         if (r.hasRange()) {
     330           3 :           info.max = r.getMax();
     331           3 :           info.min = r.getMin();
     332             :         }
     333           7 :         AccountGroup.UUID group = r.getGroup().getUUID();
     334           7 :         if (group != null) {
     335           7 :           pInfo.rules.putIfAbsent(group.get(), info); // First entry for the group wins
     336           7 :           loadGroup(groups, group);
     337             :         }
     338           7 :       }
     339           7 :       accessSectionInfo.permissions.put(p.getName(), pInfo);
     340           7 :     }
     341           7 :     return accessSectionInfo;
     342             :   }
     343             : 
     344             :   @Nullable
     345             :   private static Boolean toBoolean(boolean value) {
     346           9 :     return value ? true : null;
     347             :   }
     348             : }

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