LCOV - code coverage report
Current view: top level - server/restapi/project - PutConfig.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 101 152 66.4 %
Date: 2022-11-19 15:00:39 Functions: 9 14 64.3 %

          Line data    Source code
       1             : // Copyright (C) 2013 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.project.ProjectConfig.COMMENTLINK;
      18             : import static com.google.gerrit.server.project.ProjectConfig.KEY_ENABLED;
      19             : import static com.google.gerrit.server.project.ProjectConfig.KEY_LINK;
      20             : import static com.google.gerrit.server.project.ProjectConfig.KEY_MATCH;
      21             : import static com.google.gerrit.server.project.ProjectConfig.KEY_PREFIX;
      22             : import static com.google.gerrit.server.project.ProjectConfig.KEY_SUFFIX;
      23             : import static com.google.gerrit.server.project.ProjectConfig.KEY_TEXT;
      24             : 
      25             : import com.google.common.base.Joiner;
      26             : import com.google.common.base.Strings;
      27             : import com.google.common.flogger.FluentLogger;
      28             : import com.google.gerrit.entities.BooleanProjectConfig;
      29             : import com.google.gerrit.entities.Project;
      30             : import com.google.gerrit.extensions.api.projects.CommentLinkInput;
      31             : import com.google.gerrit.extensions.api.projects.ConfigInfo;
      32             : import com.google.gerrit.extensions.api.projects.ConfigInput;
      33             : import com.google.gerrit.extensions.api.projects.ConfigValue;
      34             : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
      35             : import com.google.gerrit.extensions.client.InheritableBoolean;
      36             : import com.google.gerrit.extensions.registration.DynamicMap;
      37             : import com.google.gerrit.extensions.restapi.BadRequestException;
      38             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      39             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      40             : import com.google.gerrit.extensions.restapi.Response;
      41             : import com.google.gerrit.extensions.restapi.RestApiException;
      42             : import com.google.gerrit.extensions.restapi.RestModifyView;
      43             : import com.google.gerrit.extensions.restapi.RestView;
      44             : import com.google.gerrit.server.CurrentUser;
      45             : import com.google.gerrit.server.EnableSignedPush;
      46             : import com.google.gerrit.server.config.AllProjectsName;
      47             : import com.google.gerrit.server.config.PluginConfigFactory;
      48             : import com.google.gerrit.server.config.ProjectConfigEntry;
      49             : import com.google.gerrit.server.extensions.webui.UiActions;
      50             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      51             : import com.google.gerrit.server.permissions.PermissionBackend;
      52             : import com.google.gerrit.server.permissions.PermissionBackendException;
      53             : import com.google.gerrit.server.permissions.ProjectPermission;
      54             : import com.google.gerrit.server.project.BooleanProjectConfigTransformations;
      55             : import com.google.gerrit.server.project.ProjectCache;
      56             : import com.google.gerrit.server.project.ProjectConfig;
      57             : import com.google.gerrit.server.project.ProjectResource;
      58             : import com.google.gerrit.server.project.ProjectState;
      59             : import com.google.inject.Inject;
      60             : import com.google.inject.Provider;
      61             : import com.google.inject.Singleton;
      62             : import java.io.IOException;
      63             : import java.util.Arrays;
      64             : import java.util.List;
      65             : import java.util.Map;
      66             : import java.util.regex.Pattern;
      67             : import org.eclipse.jgit.errors.ConfigInvalidException;
      68             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      69             : import org.eclipse.jgit.lib.Config;
      70             : 
      71             : @Singleton
      72             : public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
      73         146 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      74             : 
      75         146 :   private static final Pattern PARAMETER_NAME_PATTERN =
      76         146 :       Pattern.compile("^[a-zA-Z0-9]+[a-zA-Z0-9-]*$");
      77             : 
      78             :   private final boolean serverEnableSignedPush;
      79             :   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
      80             :   private final ProjectCache projectCache;
      81             :   private final ProjectState.Factory projectStateFactory;
      82             :   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
      83             :   private final PluginConfigFactory cfgFactory;
      84             :   private final AllProjectsName allProjects;
      85             :   private final UiActions uiActions;
      86             :   private final DynamicMap<RestView<ProjectResource>> views;
      87             :   private final Provider<CurrentUser> user;
      88             :   private final PermissionBackend permissionBackend;
      89             :   private final ProjectConfig.Factory projectConfigFactory;
      90             : 
      91             :   @Inject
      92             :   PutConfig(
      93             :       @EnableSignedPush boolean serverEnableSignedPush,
      94             :       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
      95             :       ProjectCache projectCache,
      96             :       ProjectState.Factory projectStateFactory,
      97             :       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
      98             :       PluginConfigFactory cfgFactory,
      99             :       AllProjectsName allProjects,
     100             :       UiActions uiActions,
     101             :       DynamicMap<RestView<ProjectResource>> views,
     102             :       Provider<CurrentUser> user,
     103             :       PermissionBackend permissionBackend,
     104         146 :       ProjectConfig.Factory projectConfigFactory) {
     105         146 :     this.serverEnableSignedPush = serverEnableSignedPush;
     106         146 :     this.metaDataUpdateFactory = metaDataUpdateFactory;
     107         146 :     this.projectCache = projectCache;
     108         146 :     this.projectStateFactory = projectStateFactory;
     109         146 :     this.pluginConfigEntries = pluginConfigEntries;
     110         146 :     this.cfgFactory = cfgFactory;
     111         146 :     this.allProjects = allProjects;
     112         146 :     this.uiActions = uiActions;
     113         146 :     this.views = views;
     114         146 :     this.user = user;
     115         146 :     this.permissionBackend = permissionBackend;
     116         146 :     this.projectConfigFactory = projectConfigFactory;
     117         146 :   }
     118             : 
     119             :   @Override
     120             :   public Response<ConfigInfo> apply(ProjectResource rsrc, ConfigInput input)
     121             :       throws RestApiException, PermissionBackendException {
     122          22 :     permissionBackend
     123          22 :         .currentUser()
     124          22 :         .project(rsrc.getNameKey())
     125          22 :         .check(ProjectPermission.WRITE_CONFIG);
     126          22 :     return Response.ok(apply(rsrc.getProjectState(), input));
     127             :   }
     128             : 
     129             :   public ConfigInfo apply(ProjectState projectState, ConfigInput input)
     130             :       throws ResourceNotFoundException, BadRequestException, ResourceConflictException {
     131          22 :     Project.NameKey projectName = projectState.getNameKey();
     132          22 :     if (input == null) {
     133           0 :       throw new BadRequestException("config is required");
     134             :     }
     135             : 
     136          22 :     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
     137          22 :       ProjectConfig projectConfig = projectConfigFactory.read(md);
     138          22 :       projectConfig.updateProject(
     139             :           p -> {
     140          22 :             p.setDescription(Strings.emptyToNull(input.description));
     141          22 :             for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
     142          22 :               InheritableBoolean val = BooleanProjectConfigTransformations.get(cfg, input);
     143          22 :               if (val != null) {
     144          16 :                 p.setBooleanConfig(cfg, val);
     145             :               }
     146             :             }
     147          22 :             if (input.maxObjectSizeLimit != null) {
     148           1 :               p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
     149             :             }
     150          22 :             if (input.submitType != null) {
     151           2 :               p.setSubmitType(input.submitType);
     152             :             }
     153          22 :             if (input.state != null) {
     154           4 :               p.setState(input.state);
     155             :             }
     156          22 :           });
     157             : 
     158          22 :       if (input.pluginConfigValues != null) {
     159           1 :         setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
     160             :       }
     161             : 
     162          22 :       if (input.commentLinks != null) {
     163           1 :         updateCommentLinks(projectConfig, input.commentLinks);
     164             :       }
     165             : 
     166          22 :       md.setMessage("Modified project settings\n");
     167             :       try {
     168          22 :         projectConfig.commit(md);
     169          22 :         projectCache.evictAndReindex(projectConfig.getProject());
     170          22 :         md.getRepository().setGitwebDescription(projectConfig.getProject().getDescription());
     171           1 :       } catch (IOException e) {
     172           1 :         if (e.getCause() instanceof ConfigInvalidException) {
     173           1 :           throw new ResourceConflictException(
     174           1 :               "Cannot update " + projectName + ": " + e.getCause().getMessage());
     175             :         }
     176           0 :         logger.atWarning().withCause(e).log("Failed to update config of project %s.", projectName);
     177           0 :         throw new ResourceConflictException("Cannot update " + projectName);
     178          22 :       }
     179             : 
     180          22 :       ProjectState state = projectStateFactory.create(projectConfigFactory.read(md).getCacheable());
     181          22 :       return ConfigInfoCreator.constructInfo(
     182             :           serverEnableSignedPush,
     183             :           state,
     184          22 :           user.get(),
     185             :           pluginConfigEntries,
     186             :           cfgFactory,
     187             :           allProjects,
     188             :           uiActions,
     189             :           views);
     190           0 :     } catch (RepositoryNotFoundException notFound) {
     191           0 :       throw new ResourceNotFoundException(projectName.get(), notFound);
     192           0 :     } catch (ConfigInvalidException err) {
     193           0 :       throw new ResourceConflictException("Cannot read project " + projectName, err);
     194           0 :     } catch (IOException err) {
     195           0 :       throw new ResourceConflictException("Cannot update project " + projectName, err);
     196             :     }
     197             :   }
     198             : 
     199             :   private void setPluginConfigValues(
     200             :       ProjectState projectState,
     201             :       ProjectConfig projectConfig,
     202             :       Map<String, Map<String, ConfigValue>> pluginConfigValues)
     203             :       throws BadRequestException {
     204           1 :     for (Map.Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) {
     205           1 :       String pluginName = e.getKey();
     206           1 :       for (Map.Entry<String, ConfigValue> v : e.getValue().entrySet()) {
     207           1 :         ProjectConfigEntry projectConfigEntry = pluginConfigEntries.get(pluginName, v.getKey());
     208           1 :         if (projectConfigEntry != null) {
     209           1 :           if (!PARAMETER_NAME_PATTERN.matcher(v.getKey()).matches()) {
     210             :             // TODO check why we have this restriction
     211           0 :             logger.atWarning().log(
     212             :                 "Parameter name '%s' must match '%s'",
     213           0 :                 v.getKey(), PARAMETER_NAME_PATTERN.pattern());
     214           0 :             continue;
     215             :           }
     216           1 :           String oldValue = projectConfig.getPluginConfig(pluginName).getString(v.getKey());
     217           1 :           String value = v.getValue().value;
     218           1 :           if (projectConfigEntry.getType() == ProjectConfigEntryType.ARRAY) {
     219           0 :             List<String> l =
     220           0 :                 Arrays.asList(projectConfig.getPluginConfig(pluginName).getStringList(v.getKey()));
     221           0 :             oldValue = Joiner.on("\n").join(l);
     222           0 :             value = Joiner.on("\n").join(v.getValue().values);
     223             :           }
     224           1 :           if (Strings.emptyToNull(value) != null) {
     225           1 :             if (!value.equals(oldValue)) {
     226           1 :               validateProjectConfigEntryIsEditable(
     227           1 :                   projectConfigEntry, projectState, v.getKey(), pluginName);
     228           1 :               v.setValue(projectConfigEntry.preUpdate(v.getValue()));
     229           1 :               value = v.getValue().value;
     230             :               try {
     231           1 :                 switch (projectConfigEntry.getType()) {
     232             :                   case BOOLEAN:
     233           1 :                     boolean newBooleanValue = Boolean.parseBoolean(value);
     234           1 :                     projectConfig.updatePluginConfig(
     235           1 :                         pluginName, cfg -> cfg.setBoolean(v.getKey(), newBooleanValue));
     236           1 :                     break;
     237             :                   case INT:
     238           0 :                     int newIntValue = Integer.parseInt(value);
     239           0 :                     projectConfig.updatePluginConfig(
     240           0 :                         pluginName, cfg -> cfg.setInt(v.getKey(), newIntValue));
     241           0 :                     break;
     242             :                   case LONG:
     243           0 :                     long newLongValue = Long.parseLong(value);
     244           0 :                     projectConfig.updatePluginConfig(
     245           0 :                         pluginName, cfg -> cfg.setLong(v.getKey(), newLongValue));
     246           0 :                     break;
     247             :                   case LIST:
     248           0 :                     if (!projectConfigEntry.getPermittedValues().contains(value)) {
     249           0 :                       throw new BadRequestException(
     250           0 :                           String.format(
     251             :                               "The value '%s' is not permitted for parameter '%s' of plugin '"
     252             :                                   + pluginName
     253             :                                   + "'",
     254             :                               value,
     255           0 :                               v.getKey()));
     256             :                     }
     257             :                     // $FALL-THROUGH$
     258             :                   case STRING:
     259           0 :                     String valueToSet = value;
     260           0 :                     projectConfig.updatePluginConfig(
     261           0 :                         pluginName, cfg -> cfg.setString(v.getKey(), valueToSet));
     262           0 :                     break;
     263             :                   case ARRAY:
     264           0 :                     projectConfig.updatePluginConfig(
     265           0 :                         pluginName, cfg -> cfg.setStringList(v.getKey(), v.getValue().values));
     266           0 :                     break;
     267             :                   default:
     268           0 :                     logger.atWarning().log(
     269             :                         "The type '%s' of parameter '%s' is not supported.",
     270           0 :                         projectConfigEntry.getType().name(), v.getKey());
     271             :                 }
     272           0 :               } catch (NumberFormatException ex) {
     273           0 :                 throw new BadRequestException(
     274           0 :                     String.format(
     275             :                         "The value '%s' of config parameter '%s' of plugin '%s' is invalid: %s",
     276           0 :                         v.getValue(), v.getKey(), pluginName, ex.getMessage()));
     277           1 :               }
     278             :             }
     279             :           } else {
     280           0 :             if (oldValue != null) {
     281           0 :               validateProjectConfigEntryIsEditable(
     282           0 :                   projectConfigEntry, projectState, v.getKey(), pluginName);
     283           0 :               projectConfig.updatePluginConfig(pluginName, cfg -> cfg.unset(v.getKey()));
     284             :             }
     285             :           }
     286           1 :         } else {
     287           0 :           throw new BadRequestException(
     288           0 :               String.format(
     289             :                   "The config parameter '%s' of plugin '%s' does not exist.",
     290           0 :                   v.getKey(), pluginName));
     291             :         }
     292           1 :       }
     293           1 :     }
     294           1 :   }
     295             : 
     296             :   private void updateCommentLinks(
     297             :       ProjectConfig projectConfig, Map<String, CommentLinkInput> input) {
     298           1 :     for (Map.Entry<String, CommentLinkInput> e : input.entrySet()) {
     299           1 :       String name = e.getKey();
     300           1 :       CommentLinkInput value = e.getValue();
     301           1 :       if (value != null) {
     302             :         // Add or update the commentlink section
     303           1 :         Config cfg = new Config();
     304           1 :         cfg.setString(COMMENTLINK, name, KEY_MATCH, value.match);
     305           1 :         cfg.setString(COMMENTLINK, name, KEY_LINK, value.link);
     306           1 :         if (!Strings.isNullOrEmpty(value.prefix)) {
     307           1 :           cfg.setString(COMMENTLINK, name, KEY_PREFIX, value.prefix);
     308             :         }
     309           1 :         if (!Strings.isNullOrEmpty(value.suffix)) {
     310           1 :           cfg.setString(COMMENTLINK, name, KEY_SUFFIX, value.suffix);
     311             :         }
     312           1 :         if (!Strings.isNullOrEmpty(value.text)) {
     313           1 :           cfg.setString(COMMENTLINK, name, KEY_TEXT, value.text);
     314             :         }
     315           1 :         cfg.setBoolean(COMMENTLINK, name, KEY_ENABLED, value.enabled == null || value.enabled);
     316           1 :         projectConfig.addCommentLinkSection(ProjectConfig.buildCommentLink(cfg, name));
     317           1 :       } else {
     318             :         // Delete the commentlink section
     319           1 :         projectConfig.removeCommentLinkSection(name);
     320             :       }
     321           1 :     }
     322           1 :   }
     323             : 
     324             :   private static void validateProjectConfigEntryIsEditable(
     325             :       ProjectConfigEntry projectConfigEntry,
     326             :       ProjectState projectState,
     327             :       String parameterName,
     328             :       String pluginName)
     329             :       throws BadRequestException {
     330           1 :     if (!projectConfigEntry.isEditable(projectState)) {
     331           0 :       throw new BadRequestException(
     332           0 :           String.format(
     333             :               "Not allowed to set parameter '%s' of plugin '%s' on project '%s'.",
     334           0 :               parameterName, pluginName, projectState.getName()));
     335             :     }
     336           1 :   }
     337             : }

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