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.

ValuesAction.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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.base.Splitter;
  22. import com.google.common.collect.ImmutableSet;
  23. import com.google.common.collect.Multimap;
  24. import com.google.common.collect.Ordering;
  25. import com.google.common.collect.TreeMultimap;
  26. import java.util.ArrayList;
  27. import java.util.HashMap;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Objects;
  31. import java.util.Optional;
  32. import java.util.Set;
  33. import java.util.function.Function;
  34. import java.util.stream.Collectors;
  35. import javax.annotation.CheckForNull;
  36. import javax.annotation.Nullable;
  37. import org.sonar.api.config.Configuration;
  38. import org.sonar.api.config.PropertyDefinition;
  39. import org.sonar.api.config.PropertyDefinitions;
  40. import org.sonar.api.server.ws.Change;
  41. import org.sonar.api.server.ws.Request;
  42. import org.sonar.api.server.ws.Response;
  43. import org.sonar.api.server.ws.WebService;
  44. import org.sonar.api.web.UserRole;
  45. import org.sonar.db.DbClient;
  46. import org.sonar.db.DbSession;
  47. import org.sonar.db.component.ComponentDto;
  48. import org.sonar.db.permission.GlobalPermission;
  49. import org.sonar.db.property.PropertyDto;
  50. import org.sonar.server.component.ComponentFinder;
  51. import org.sonar.server.user.UserSession;
  52. import org.sonarqube.ws.Settings;
  53. import org.sonarqube.ws.Settings.ValuesWsResponse;
  54. import static java.lang.String.format;
  55. import static java.util.stream.Stream.concat;
  56. import static org.apache.commons.lang.StringUtils.isEmpty;
  57. import static org.sonar.api.CoreProperties.SERVER_ID;
  58. import static org.sonar.api.CoreProperties.SERVER_STARTTIME;
  59. import static org.sonar.api.PropertyType.PROPERTY_SET;
  60. import static org.sonar.api.web.UserRole.USER;
  61. import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
  62. import static org.sonar.server.setting.ws.PropertySetExtractor.extractPropertySetKeys;
  63. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
  64. import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEYS;
  65. import static org.sonar.server.setting.ws.SettingsWsSupport.isSecured;
  66. import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
  67. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  68. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  69. public class ValuesAction implements SettingsWsAction {
  70. private static final Splitter COMMA_SPLITTER = Splitter.on(",");
  71. private static final String COMMA_ENCODED_VALUE = "%2C";
  72. private static final Splitter DOT_SPLITTER = Splitter.on(".").omitEmptyStrings();
  73. private static final Set<String> SERVER_SETTING_KEYS = ImmutableSet.of(SERVER_STARTTIME, SERVER_ID);
  74. private final DbClient dbClient;
  75. private final ComponentFinder componentFinder;
  76. private final UserSession userSession;
  77. private final PropertyDefinitions propertyDefinitions;
  78. private final SettingsWsSupport settingsWsSupport;
  79. private final boolean isSonarCloud;
  80. public ValuesAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, PropertyDefinitions propertyDefinitions,
  81. SettingsWsSupport settingsWsSupport, Configuration configuration) {
  82. this.dbClient = dbClient;
  83. this.componentFinder = componentFinder;
  84. this.userSession = userSession;
  85. this.propertyDefinitions = propertyDefinitions;
  86. this.settingsWsSupport = settingsWsSupport;
  87. this.isSonarCloud = configuration.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false);
  88. }
  89. @Override
  90. public void define(WebService.NewController context) {
  91. WebService.NewAction action = context.createAction("values")
  92. .setDescription("List settings values.<br>" +
  93. "If no value has been set for a setting, then the default value is returned.<br>" +
  94. "The settings from conf/sonar.properties are excluded from results.<br>" +
  95. "Requires 'Browse' or 'Execute Analysis' permission when a component is specified.<br/>")
  96. .setResponseExample(getClass().getResource("values-example.json"))
  97. .setSince("6.3")
  98. .setChangelog(
  99. new Change("9.1", "The value of secured settings are no longer returned"),
  100. new Change("7.6", String.format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT)),
  101. new Change("7.1", "The settings from conf/sonar.properties are excluded from results."))
  102. .setHandler(this);
  103. action.createParam(PARAM_KEYS)
  104. .setDescription("List of setting keys")
  105. .setExampleValue("sonar.test.inclusions,sonar.exclusions");
  106. action.createParam(PARAM_COMPONENT)
  107. .setDescription("Component key")
  108. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  109. }
  110. @Override
  111. public void handle(Request request, Response response) throws Exception {
  112. writeProtobuf(doHandle(request), request, response);
  113. }
  114. private ValuesWsResponse doHandle(Request request) {
  115. try (DbSession dbSession = dbClient.openSession(true)) {
  116. ValuesRequest valuesRequest = ValuesRequest.from(request);
  117. Optional<ComponentDto> component = loadComponent(dbSession, valuesRequest);
  118. Set<String> keys = loadKeys(valuesRequest);
  119. Map<String, String> keysToDisplayMap = getKeysToDisplayMap(keys);
  120. List<Setting> settings = loadSettings(dbSession, component, keysToDisplayMap.keySet());
  121. return new ValuesResponseBuilder(settings, component, keysToDisplayMap).build();
  122. }
  123. }
  124. private Set<String> loadKeys(ValuesRequest valuesRequest) {
  125. List<String> keys = valuesRequest.getKeys();
  126. Set<String> result;
  127. if (keys == null || keys.isEmpty()) {
  128. result = concat(propertyDefinitions.getAll().stream().map(PropertyDefinition::key), SERVER_SETTING_KEYS.stream()).collect(Collectors.toSet());
  129. } else {
  130. result = ImmutableSet.copyOf(keys);
  131. }
  132. result.forEach(SettingsWsSupport::validateKey);
  133. return result;
  134. }
  135. private Optional<ComponentDto> loadComponent(DbSession dbSession, ValuesRequest valuesRequest) {
  136. String componentKey = valuesRequest.getComponent();
  137. if (componentKey == null) {
  138. return Optional.empty();
  139. }
  140. ComponentDto component = componentFinder.getByKey(dbSession, componentKey);
  141. if (!userSession.hasComponentPermission(USER, component) &&
  142. !userSession.hasComponentPermission(UserRole.SCAN, component) &&
  143. !userSession.hasPermission(GlobalPermission.SCAN)) {
  144. throw insufficientPrivilegesException();
  145. }
  146. return Optional.of(component);
  147. }
  148. private List<Setting> loadSettings(DbSession dbSession, Optional<ComponentDto> component, Set<String> keys) {
  149. // List of settings must be kept in the following orders : default -> global -> component -> branch
  150. List<Setting> settings = new ArrayList<>();
  151. settings.addAll(loadDefaultValues(keys));
  152. settings.addAll(loadGlobalSettings(dbSession, keys));
  153. if (component.isPresent() && component.get().getBranch() != null && component.get().getMainBranchProjectUuid() != null) {
  154. ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, component.get().getMainBranchProjectUuid());
  155. settings.addAll(loadComponentSettings(dbSession, keys, project).values());
  156. }
  157. component.ifPresent(componentDto -> settings.addAll(loadComponentSettings(dbSession, keys, componentDto).values()));
  158. return settings.stream()
  159. .filter(s -> settingsWsSupport.isVisible(s.getKey(), component))
  160. .collect(Collectors.toList());
  161. }
  162. private List<Setting> loadDefaultValues(Set<String> keys) {
  163. return propertyDefinitions.getAll().stream()
  164. .filter(definition -> keys.contains(definition.key()))
  165. .filter(defaultProperty -> !isEmpty(defaultProperty.defaultValue()))
  166. .map(Setting::createFromDefinition)
  167. .collect(Collectors.toList());
  168. }
  169. private Map<String, String> getKeysToDisplayMap(Set<String> keys) {
  170. return keys.stream()
  171. .collect(Collectors.toMap(propertyDefinitions::validKey, Function.identity(),
  172. (u, v) -> {
  173. throw new IllegalArgumentException(format("'%s' and '%s' cannot be used at the same time as they refer to the same setting", u, v));
  174. }));
  175. }
  176. private List<Setting> loadGlobalSettings(DbSession dbSession, Set<String> keys) {
  177. List<PropertyDto> properties = dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, keys);
  178. List<PropertyDto> propertySets = dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, getPropertySetKeys(properties));
  179. return properties.stream()
  180. .map(property -> Setting.createFromDto(property, getPropertySets(property.getKey(), propertySets, null), propertyDefinitions.get(property.getKey())))
  181. .collect(Collectors.toList());
  182. }
  183. /**
  184. * Return list of settings by component uuid, sorted from project to lowest module
  185. */
  186. private Multimap<String, Setting> loadComponentSettings(DbSession dbSession, Set<String> keys, ComponentDto component) {
  187. List<String> componentUuids = DOT_SPLITTER.splitToList(component.moduleUuidPath());
  188. List<PropertyDto> properties = dbClient.propertiesDao().selectPropertiesByKeysAndComponentUuids(dbSession, keys, componentUuids);
  189. List<PropertyDto> propertySets = dbClient.propertiesDao().selectPropertiesByKeysAndComponentUuids(dbSession, getPropertySetKeys(properties), componentUuids);
  190. Multimap<String, Setting> settingsByUuid = TreeMultimap.create(Ordering.explicit(componentUuids), Ordering.arbitrary());
  191. for (PropertyDto propertyDto : properties) {
  192. String componentUuid = propertyDto.getComponentUuid();
  193. String propertyKey = propertyDto.getKey();
  194. settingsByUuid.put(componentUuid,
  195. Setting.createFromDto(propertyDto, getPropertySets(propertyKey, propertySets, componentUuid), propertyDefinitions.get(propertyKey)));
  196. }
  197. return settingsByUuid;
  198. }
  199. private Set<String> getPropertySetKeys(List<PropertyDto> properties) {
  200. return properties.stream()
  201. .filter(propertyDto -> propertyDefinitions.get(propertyDto.getKey()) != null)
  202. .filter(propertyDto -> propertyDefinitions.get(propertyDto.getKey()).type().equals(PROPERTY_SET))
  203. .flatMap(propertyDto -> extractPropertySetKeys(propertyDto, propertyDefinitions.get(propertyDto.getKey())).stream())
  204. .collect(Collectors.toSet());
  205. }
  206. private static List<PropertyDto> getPropertySets(String propertyKey, List<PropertyDto> propertySets, @Nullable String componentUuid) {
  207. return propertySets.stream()
  208. .filter(propertyDto -> Objects.equals(propertyDto.getComponentUuid(), componentUuid))
  209. .filter(propertyDto -> propertyDto.getKey().startsWith(propertyKey + "."))
  210. .collect(Collectors.toList());
  211. }
  212. private class ValuesResponseBuilder {
  213. private final List<Setting> settings;
  214. private final Optional<ComponentDto> requestedComponent;
  215. private final ValuesWsResponse.Builder valuesWsBuilder = ValuesWsResponse.newBuilder();
  216. private final Map<String, Settings.Setting.Builder> settingsBuilderByKey = new HashMap<>();
  217. private final Map<String, Setting> settingsByParentKey = new HashMap<>();
  218. private final Map<String, String> keysToDisplayMap;
  219. ValuesResponseBuilder(List<Setting> settings, Optional<ComponentDto> requestedComponent, Map<String, String> keysToDisplayMap) {
  220. this.settings = settings;
  221. this.requestedComponent = requestedComponent;
  222. this.keysToDisplayMap = keysToDisplayMap;
  223. }
  224. ValuesWsResponse build() {
  225. processSettings();
  226. settingsBuilderByKey.values().forEach(Settings.Setting.Builder::build);
  227. return valuesWsBuilder.build();
  228. }
  229. private void processSettings() {
  230. settings.forEach(setting -> {
  231. Settings.Setting.Builder valueBuilder = getOrCreateValueBuilder(keysToDisplayMap.get(setting.getKey()));
  232. setInherited(setting, valueBuilder);
  233. setValue(setting, valueBuilder);
  234. setParent(setting, valueBuilder);
  235. });
  236. }
  237. private Settings.Setting.Builder getOrCreateValueBuilder(String key) {
  238. return settingsBuilderByKey.computeIfAbsent(key, k -> valuesWsBuilder.addSettingsBuilder().setKey(key));
  239. }
  240. private void setInherited(Setting setting, Settings.Setting.Builder valueBuilder) {
  241. boolean isDefault = setting.isDefault();
  242. boolean isGlobal = !requestedComponent.isPresent();
  243. boolean isOnComponent = requestedComponent.isPresent() && Objects.equals(setting.getComponentUuid(), requestedComponent.get().uuid());
  244. boolean isSet = isGlobal || isOnComponent;
  245. valueBuilder.setInherited(isDefault || !isSet);
  246. }
  247. private void setValue(Setting setting, Settings.Setting.Builder valueBuilder) {
  248. if (isSecured(setting.getKey())) {
  249. return;
  250. }
  251. PropertyDefinition definition = setting.getDefinition();
  252. String value = setting.getValue();
  253. if (definition == null) {
  254. valueBuilder.setValue(value);
  255. return;
  256. }
  257. if (definition.type().equals(PROPERTY_SET)) {
  258. valueBuilder.setFieldValues(createFieldValuesBuilder(filterVisiblePropertySets(setting.getPropertySets())));
  259. } else if (definition.multiValues()) {
  260. valueBuilder.setValues(createValuesBuilder(value));
  261. } else {
  262. valueBuilder.setValue(value);
  263. }
  264. }
  265. private void setParent(Setting setting, Settings.Setting.Builder valueBuilder) {
  266. Setting parent = settingsByParentKey.get(setting.getKey());
  267. if (parent != null) {
  268. String value = valueBuilder.getInherited() ? setting.getValue() : parent.getValue();
  269. PropertyDefinition definition = setting.getDefinition();
  270. if (definition == null) {
  271. valueBuilder.setParentValue(value);
  272. return;
  273. }
  274. if (definition.type().equals(PROPERTY_SET)) {
  275. valueBuilder.setParentFieldValues(
  276. createFieldValuesBuilder(valueBuilder.getInherited() ? filterVisiblePropertySets(setting.getPropertySets()) : filterVisiblePropertySets(parent.getPropertySets())));
  277. } else if (definition.multiValues()) {
  278. valueBuilder.setParentValues(createValuesBuilder(value));
  279. } else {
  280. valueBuilder.setParentValue(value);
  281. }
  282. }
  283. settingsByParentKey.put(setting.getKey(), setting);
  284. }
  285. private Settings.Values.Builder createValuesBuilder(String value) {
  286. List<String> values = COMMA_SPLITTER.splitToList(value).stream().map(v -> v.replace(COMMA_ENCODED_VALUE, ",")).collect(Collectors.toList());
  287. return Settings.Values.newBuilder().addAllValues(values);
  288. }
  289. private Settings.FieldValues.Builder createFieldValuesBuilder(List<Map<String, String>> fieldValues) {
  290. Settings.FieldValues.Builder builder = Settings.FieldValues.newBuilder();
  291. for (Map<String, String> propertySetMap : fieldValues) {
  292. builder.addFieldValuesBuilder().putAllValue(propertySetMap);
  293. }
  294. return builder;
  295. }
  296. private List<Map<String, String>> filterVisiblePropertySets(List<Map<String, String>> propertySets) {
  297. List<Map<String, String>> filteredPropertySets = new ArrayList<>();
  298. propertySets.forEach(map -> {
  299. Map<String, String> set = new HashMap<>();
  300. map.entrySet().stream()
  301. .filter(entry -> settingsWsSupport.isVisible(entry.getKey(), requestedComponent))
  302. .forEach(entry -> set.put(entry.getKey(), entry.getValue()));
  303. filteredPropertySets.add(set);
  304. });
  305. return filteredPropertySets;
  306. }
  307. }
  308. private static class ValuesRequest {
  309. private String component;
  310. private List<String> keys;
  311. public ValuesRequest setComponent(@Nullable String component) {
  312. this.component = component;
  313. return this;
  314. }
  315. @CheckForNull
  316. public String getComponent() {
  317. return component;
  318. }
  319. public ValuesRequest setKeys(@Nullable List<String> keys) {
  320. this.keys = keys;
  321. return this;
  322. }
  323. @CheckForNull
  324. public List<String> getKeys() {
  325. return keys;
  326. }
  327. private static ValuesRequest from(Request request) {
  328. ValuesRequest result = new ValuesRequest()
  329. .setComponent(request.param(PARAM_COMPONENT));
  330. if (request.hasParam(PARAM_KEYS)) {
  331. result.setKeys(request.paramAsStrings(PARAM_KEYS));
  332. }
  333. return result;
  334. }
  335. }
  336. }