You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SetAction.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.setting.ws;
  21. import com.google.common.collect.ArrayListMultimap;
  22. import com.google.common.collect.ImmutableList;
  23. import com.google.common.collect.ListMultimap;
  24. import com.google.gson.Gson;
  25. import com.google.gson.JsonSyntaxException;
  26. import com.google.gson.reflect.TypeToken;
  27. import java.lang.reflect.Type;
  28. import java.util.Collections;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Optional;
  32. import java.util.Set;
  33. import java.util.stream.Collector;
  34. import java.util.stream.Collectors;
  35. import java.util.stream.IntStream;
  36. import javax.annotation.CheckForNull;
  37. import javax.annotation.Nullable;
  38. import org.apache.commons.lang.StringUtils;
  39. import org.sonar.api.PropertyType;
  40. import org.sonar.api.config.PropertyDefinition;
  41. import org.sonar.api.config.PropertyDefinitions;
  42. import org.sonar.api.config.PropertyFieldDefinition;
  43. import org.sonar.api.server.ws.Change;
  44. import org.sonar.api.server.ws.Request;
  45. import org.sonar.api.server.ws.Response;
  46. import org.sonar.api.server.ws.WebService;
  47. import org.sonar.api.web.UserRole;
  48. import org.sonar.db.DbClient;
  49. import org.sonar.db.DbSession;
  50. import org.sonar.db.component.ComponentDto;
  51. import org.sonar.db.property.PropertyDto;
  52. import org.sonar.scanner.protocol.GsonHelper;
  53. import org.sonar.server.component.ComponentFinder;
  54. import org.sonar.server.exceptions.BadRequestException;
  55. import org.sonar.server.platform.SettingsChangeNotifier;
  56. import org.sonar.server.setting.ws.SettingValidations.SettingData;
  57. import org.sonar.server.user.UserSession;
  58. import static com.google.common.base.Preconditions.checkArgument;
  59. import static java.lang.String.format;
  60. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
  61. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
  62. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_FIELD_VALUES;
  63. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEY;
  64. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
  65. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUE;
  66. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUES;
  67. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  68. import static org.sonar.server.ws.WsUtils.checkRequest;
  69. public class SetAction implements SettingsWsAction {
  70. private static final Collector<CharSequence, ?, String> COMMA_JOINER = Collectors.joining(",");
  71. private static final String MSG_NO_EMPTY_VALUE = "A non empty value must be provided";
  72. private static final int VALUE_MAXIMUM_LENGTH = 4000;
  73. private final PropertyDefinitions propertyDefinitions;
  74. private final DbClient dbClient;
  75. private final ComponentFinder componentFinder;
  76. private final UserSession userSession;
  77. private final SettingsUpdater settingsUpdater;
  78. private final SettingsChangeNotifier settingsChangeNotifier;
  79. private final SettingValidations validations;
  80. private final SettingsWsSupport settingsWsSupport;
  81. public SetAction(PropertyDefinitions propertyDefinitions, DbClient dbClient, ComponentFinder componentFinder, UserSession userSession,
  82. SettingsUpdater settingsUpdater, SettingsChangeNotifier settingsChangeNotifier, SettingValidations validations, SettingsWsSupport settingsWsSupport) {
  83. this.propertyDefinitions = propertyDefinitions;
  84. this.dbClient = dbClient;
  85. this.componentFinder = componentFinder;
  86. this.userSession = userSession;
  87. this.settingsUpdater = settingsUpdater;
  88. this.settingsChangeNotifier = settingsChangeNotifier;
  89. this.validations = validations;
  90. this.settingsWsSupport = settingsWsSupport;
  91. }
  92. @Override
  93. public void define(WebService.NewController context) {
  94. WebService.NewAction action = context.createAction("set")
  95. .setDescription("Update a setting value.<br>" +
  96. "Either '%s' or '%s' must be provided.<br> " +
  97. "The settings defined in config/sonar.properties are read-only and can't be changed.<br/>" +
  98. "Requires one of the following permissions: " +
  99. "<ul>" +
  100. "<li>'Administer System'</li>" +
  101. "<li>'Administer' rights on the specified component</li>" +
  102. "</ul>",
  103. PARAM_VALUE, PARAM_VALUES)
  104. .setSince("6.1")
  105. .setChangelog(
  106. new Change("7.6", String.format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT)),
  107. new Change("7.1", "The settings defined in config/sonar.properties are read-only and can't be changed"))
  108. .setPost(true)
  109. .setHandler(this);
  110. action.createParam(PARAM_KEY)
  111. .setDescription("Setting key")
  112. .setExampleValue("sonar.links.scm")
  113. .setRequired(true);
  114. action.createParam(PARAM_VALUE)
  115. .setMaximumLength(VALUE_MAXIMUM_LENGTH)
  116. .setDescription("Setting value. To reset a value, please use the reset web service.")
  117. .setExampleValue("git@github.com:SonarSource/sonarqube.git");
  118. action.createParam(PARAM_VALUES)
  119. .setDescription("Setting multi value. To set several values, the parameter must be called once for each value.")
  120. .setExampleValue("values=firstValue&values=secondValue&values=thirdValue");
  121. action.createParam(PARAM_FIELD_VALUES)
  122. .setDescription("Setting field values. To set several values, the parameter must be called once for each value.")
  123. .setExampleValue(PARAM_FIELD_VALUES + "={\"firstField\":\"first value\", \"secondField\":\"second value\", \"thirdField\":\"third value\"}");
  124. action.createParam(PARAM_COMPONENT)
  125. .setDescription("Component key")
  126. .setDeprecatedKey("componentKey", "6.3")
  127. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  128. settingsWsSupport.addBranchParam(action);
  129. settingsWsSupport.addPullRequestParam(action);
  130. }
  131. @Override
  132. public void handle(Request request, Response response) throws Exception {
  133. try (DbSession dbSession = dbClient.openSession(false)) {
  134. SetRequest wsRequest = toWsRequest(request);
  135. SettingsWsSupport.validateKey(wsRequest.getKey());
  136. doHandle(dbSession, wsRequest);
  137. }
  138. response.noContent();
  139. }
  140. private void doHandle(DbSession dbSession, SetRequest request) {
  141. Optional<ComponentDto> component = searchComponent(dbSession, request);
  142. checkPermissions(component);
  143. PropertyDefinition definition = propertyDefinitions.get(request.getKey());
  144. String value;
  145. commonChecks(request, component);
  146. if (!request.getFieldValues().isEmpty()) {
  147. value = doHandlePropertySet(dbSession, request, definition, component);
  148. } else {
  149. validate(request);
  150. PropertyDto property = toProperty(request, component);
  151. value = property.getValue();
  152. dbClient.propertiesDao().saveProperty(dbSession, property);
  153. }
  154. dbSession.commit();
  155. if (!component.isPresent()) {
  156. settingsChangeNotifier.onGlobalPropertyChange(persistedKey(request), value);
  157. }
  158. }
  159. private String doHandlePropertySet(DbSession dbSession, SetRequest request, @Nullable PropertyDefinition definition, Optional<ComponentDto> component) {
  160. validatePropertySet(request, definition);
  161. int[] fieldIds = IntStream.rangeClosed(1, request.getFieldValues().size()).toArray();
  162. String inlinedFieldKeys = IntStream.of(fieldIds).mapToObj(String::valueOf).collect(COMMA_JOINER);
  163. String key = persistedKey(request);
  164. Long componentId = component.isPresent() ? component.get().getId() : null;
  165. deleteSettings(dbSession, component, key);
  166. dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(key).setValue(inlinedFieldKeys).setResourceId(componentId));
  167. List<String> fieldValues = request.getFieldValues();
  168. IntStream.of(fieldIds).boxed()
  169. .flatMap(i -> readOneFieldValues(fieldValues.get(i - 1), request.getKey()).entrySet().stream()
  170. .map(entry -> new KeyValue(key + "." + i + "." + entry.getKey(), entry.getValue())))
  171. .forEach(keyValue -> dbClient.propertiesDao().saveProperty(dbSession, toFieldProperty(keyValue, componentId)));
  172. return inlinedFieldKeys;
  173. }
  174. private void deleteSettings(DbSession dbSession, Optional<ComponentDto> component, String key) {
  175. if (component.isPresent()) {
  176. settingsUpdater.deleteComponentSettings(dbSession, component.get(), key);
  177. } else {
  178. settingsUpdater.deleteGlobalSettings(dbSession, key);
  179. }
  180. }
  181. private void commonChecks(SetRequest request, Optional<ComponentDto> component) {
  182. checkValueIsSet(request);
  183. String settingKey = request.getKey();
  184. SettingData settingData = new SettingData(settingKey, valuesFromRequest(request), component.orElse(null));
  185. ImmutableList.of(validations.scope(), validations.qualifier(), validations.valueType())
  186. .forEach(validation -> validation.accept(settingData));
  187. component.map(ComponentDto::getBranch)
  188. .ifPresent(b -> checkArgument(SettingsWs.SETTING_ON_BRANCHES.contains(settingKey), format("Setting '%s' cannot be set on a branch", settingKey)));
  189. }
  190. private static void validatePropertySet(SetRequest request, @Nullable PropertyDefinition definition) {
  191. checkRequest(definition != null, "Setting '%s' is undefined", request.getKey());
  192. checkRequest(PropertyType.PROPERTY_SET.equals(definition.type()), "Parameter '%s' is used for setting of property set type only", PARAM_FIELD_VALUES);
  193. Set<String> fieldKeys = definition.fields().stream().map(PropertyFieldDefinition::key).collect(Collectors.toSet());
  194. ListMultimap<String, String> valuesByFieldKeys = ArrayListMultimap.create(fieldKeys.size(), request.getFieldValues().size() * fieldKeys.size());
  195. request.getFieldValues().stream()
  196. .map(oneFieldValues -> readOneFieldValues(oneFieldValues, request.getKey()))
  197. .peek(map -> checkRequest(map.values().stream().anyMatch(StringUtils::isNotBlank), MSG_NO_EMPTY_VALUE))
  198. .flatMap(map -> map.entrySet().stream())
  199. .peek(entry -> valuesByFieldKeys.put(entry.getKey(), entry.getValue()))
  200. .forEach(entry -> checkRequest(fieldKeys.contains(entry.getKey()), "Unknown field key '%s' for setting '%s'", entry.getKey(), request.getKey()));
  201. checkFieldType(request, definition, valuesByFieldKeys);
  202. }
  203. private void validate(SetRequest request) {
  204. PropertyDefinition definition = propertyDefinitions.get(request.getKey());
  205. if (definition == null) {
  206. return;
  207. }
  208. checkSingleOrMultiValue(request, definition);
  209. }
  210. private static void checkFieldType(SetRequest request, PropertyDefinition definition, ListMultimap<String, String> valuesByFieldKeys) {
  211. for (PropertyFieldDefinition fieldDefinition : definition.fields()) {
  212. for (String value : valuesByFieldKeys.get(fieldDefinition.key())) {
  213. PropertyDefinition.Result result = fieldDefinition.validate(value);
  214. checkRequest(result.isValid(),
  215. "Error when validating setting with key '%s'. Field '%s' has incorrect value '%s'.",
  216. request.getKey(), fieldDefinition.key(), value);
  217. }
  218. }
  219. }
  220. private static void checkSingleOrMultiValue(SetRequest request, PropertyDefinition definition) {
  221. checkRequest(definition.multiValues() ^ request.getValue() != null,
  222. "Parameter '%s' must be used for single value setting. Parameter '%s' must be used for multi value setting.", PARAM_VALUE, PARAM_VALUES);
  223. }
  224. private static void checkValueIsSet(SetRequest request) {
  225. checkRequest(
  226. request.getValue() != null
  227. ^ !request.getValues().isEmpty()
  228. ^ !request.getFieldValues().isEmpty(),
  229. "Either '%s', '%s' or '%s' must be provided", PARAM_VALUE, PARAM_VALUES, PARAM_FIELD_VALUES);
  230. checkRequest(request.getValues().stream().allMatch(StringUtils::isNotBlank), MSG_NO_EMPTY_VALUE);
  231. checkRequest(request.getValue() == null || StringUtils.isNotBlank(request.getValue()), MSG_NO_EMPTY_VALUE);
  232. }
  233. private static List<String> valuesFromRequest(SetRequest request) {
  234. return request.getValue() == null ? request.getValues() : Collections.singletonList(request.getValue());
  235. }
  236. private String persistedKey(SetRequest request) {
  237. PropertyDefinition definition = propertyDefinitions.get(request.getKey());
  238. // handles deprecated key but persist the new key
  239. return definition == null ? request.getKey() : definition.key();
  240. }
  241. private static String persistedValue(SetRequest request) {
  242. return request.getValue() == null
  243. ? request.getValues().stream().map(value -> value.replace(",", "%2C")).collect(COMMA_JOINER)
  244. : request.getValue();
  245. }
  246. private void checkPermissions(Optional<ComponentDto> component) {
  247. if (component.isPresent()) {
  248. userSession.checkComponentPermission(UserRole.ADMIN, component.get());
  249. } else {
  250. userSession.checkIsSystemAdministrator();
  251. }
  252. }
  253. private static SetRequest toWsRequest(Request request) {
  254. SetRequest set = new SetRequest()
  255. .setKey(request.mandatoryParam(PARAM_KEY))
  256. .setValue(request.param(PARAM_VALUE))
  257. .setValues(request.multiParam(PARAM_VALUES))
  258. .setFieldValues(request.multiParam(PARAM_FIELD_VALUES))
  259. .setComponent(request.param(PARAM_COMPONENT))
  260. .setBranch(request.param(PARAM_BRANCH))
  261. .setPullRequest(request.param(PARAM_PULL_REQUEST));
  262. checkArgument(set.getValues() != null, "Setting values must not be null");
  263. checkArgument(set.getFieldValues() != null, "Setting fields values must not be null");
  264. return set;
  265. }
  266. private static Map<String, String> readOneFieldValues(String json, String key) {
  267. Type type = new TypeToken<Map<String, String>>() {
  268. }.getType();
  269. Gson gson = GsonHelper.create();
  270. try {
  271. return gson.fromJson(json, type);
  272. } catch (JsonSyntaxException e) {
  273. throw BadRequestException.create(String.format("JSON '%s' does not respect expected format for setting '%s'. Ex: {\"field1\":\"value1\", \"field2\":\"value2\"}", json, key));
  274. }
  275. }
  276. private Optional<ComponentDto> searchComponent(DbSession dbSession, SetRequest request) {
  277. String componentKey = request.getComponent();
  278. if (componentKey == null) {
  279. return Optional.empty();
  280. }
  281. return Optional.of(componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, request.getBranch(), request.getPullRequest()));
  282. }
  283. private PropertyDto toProperty(SetRequest request, Optional<ComponentDto> component) {
  284. String key = persistedKey(request);
  285. String value = persistedValue(request);
  286. PropertyDto property = new PropertyDto()
  287. .setKey(key)
  288. .setValue(value);
  289. if (component.isPresent()) {
  290. property.setResourceId(component.get().getId());
  291. }
  292. return property;
  293. }
  294. private static PropertyDto toFieldProperty(KeyValue keyValue, @Nullable Long componentId) {
  295. return new PropertyDto().setKey(keyValue.key).setValue(keyValue.value).setResourceId(componentId);
  296. }
  297. private static class KeyValue {
  298. private final String key;
  299. private final String value;
  300. private KeyValue(String key, String value) {
  301. this.key = key;
  302. this.value = value;
  303. }
  304. }
  305. private static class SetRequest {
  306. private String branch;
  307. private String pullRequest;
  308. private String component;
  309. private List<String> fieldValues;
  310. private String key;
  311. private String value;
  312. private List<String> values;
  313. public SetRequest setBranch(@Nullable String branch) {
  314. this.branch = branch;
  315. return this;
  316. }
  317. @CheckForNull
  318. public String getBranch() {
  319. return branch;
  320. }
  321. public SetRequest setPullRequest(@Nullable String pullRequest) {
  322. this.pullRequest = pullRequest;
  323. return this;
  324. }
  325. @CheckForNull
  326. public String getPullRequest() {
  327. return pullRequest;
  328. }
  329. public SetRequest setComponent(@Nullable String component) {
  330. this.component = component;
  331. return this;
  332. }
  333. @CheckForNull
  334. public String getComponent() {
  335. return component;
  336. }
  337. public SetRequest setFieldValues(List<String> fieldValues) {
  338. this.fieldValues = fieldValues;
  339. return this;
  340. }
  341. public List<String> getFieldValues() {
  342. return fieldValues;
  343. }
  344. public SetRequest setKey(String key) {
  345. this.key = key;
  346. return this;
  347. }
  348. public String getKey() {
  349. return key;
  350. }
  351. public SetRequest setValue(@Nullable String value) {
  352. this.value = value;
  353. return this;
  354. }
  355. @CheckForNull
  356. public String getValue() {
  357. return value;
  358. }
  359. public SetRequest setValues(@Nullable List<String> values) {
  360. this.values = values;
  361. return this;
  362. }
  363. public List<String> getValues() {
  364. return values;
  365. }
  366. }
  367. }