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.

GitlabConfigurationService.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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.common.gitlab.config;
  21. import com.google.common.base.Strings;
  22. import java.util.Arrays;
  23. import java.util.List;
  24. import java.util.Optional;
  25. import java.util.Set;
  26. import java.util.stream.Collectors;
  27. import javax.annotation.Nullable;
  28. import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
  29. import org.sonar.api.server.ServerSide;
  30. import org.sonar.auth.gitlab.GitLabIdentityProvider;
  31. import org.sonar.db.DbClient;
  32. import org.sonar.db.DbSession;
  33. import org.sonar.db.property.PropertyDto;
  34. import org.sonar.server.common.UpdatedValue;
  35. import org.sonar.server.exceptions.BadRequestException;
  36. import org.sonar.server.exceptions.NotFoundException;
  37. import org.sonar.server.management.ManagedInstanceService;
  38. import org.sonar.server.setting.ThreadLocalSettings;
  39. import static java.lang.String.format;
  40. import static org.apache.commons.lang.StringUtils.isNotBlank;
  41. import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.AUTH_ONLY;
  42. import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.COMPLETE;
  43. import static org.sonar.api.utils.Preconditions.checkState;
  44. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
  45. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
  46. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
  47. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_ENABLED;
  48. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_GROUPS;
  49. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_TOKEN;
  50. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
  51. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
  52. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
  53. import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
  54. import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
  55. import static org.sonar.server.exceptions.NotFoundException.checkFound;
  56. @ServerSide
  57. public class GitlabConfigurationService {
  58. private static final List<String> GITLAB_CONFIGURATION_PROPERTIES = List.of(
  59. GITLAB_AUTH_ENABLED,
  60. GITLAB_AUTH_APPLICATION_ID,
  61. GITLAB_AUTH_URL,
  62. GITLAB_AUTH_SECRET,
  63. GITLAB_AUTH_SYNC_USER_GROUPS,
  64. GITLAB_AUTH_PROVISIONING_ENABLED,
  65. GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP,
  66. GITLAB_AUTH_PROVISIONING_TOKEN,
  67. GITLAB_AUTH_PROVISIONING_GROUPS);
  68. public static final String UNIQUE_GITLAB_CONFIGURATION_ID = "gitlab-configuration";
  69. private final DbClient dbClient;
  70. private final ManagedInstanceService managedInstanceService;
  71. private final GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator;
  72. private final ThreadLocalSettings threadLocalSettings;
  73. public GitlabConfigurationService(DbClient dbClient,
  74. ManagedInstanceService managedInstanceService, GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator, ThreadLocalSettings threadLocalSettings) {
  75. this.dbClient = dbClient;
  76. this.managedInstanceService = managedInstanceService;
  77. this.gitlabGlobalSettingsValidator = gitlabGlobalSettingsValidator;
  78. this.threadLocalSettings = threadLocalSettings;
  79. }
  80. public GitlabConfiguration updateConfiguration(UpdateGitlabConfigurationRequest updateRequest) {
  81. UpdatedValue<Boolean> provisioningEnabled =
  82. updateRequest.provisioningType().map(GitlabConfigurationService::shouldEnableAutoProvisioning);
  83. try (DbSession dbSession = dbClient.openSession(true)) {
  84. throwIfConfigurationDoesntExist(dbSession);
  85. GitlabConfiguration currentConfiguration = getConfiguration(updateRequest.gitlabConfigurationId(), dbSession);
  86. setIfDefined(dbSession, GITLAB_AUTH_ENABLED, updateRequest.enabled().map(String::valueOf));
  87. setIfDefined(dbSession, GITLAB_AUTH_APPLICATION_ID, updateRequest.applicationId());
  88. setIfDefined(dbSession, GITLAB_AUTH_URL, updateRequest.url());
  89. setIfDefined(dbSession, GITLAB_AUTH_SECRET, updateRequest.secret());
  90. setIfDefined(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, updateRequest.synchronizeGroups().map(String::valueOf));
  91. setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, provisioningEnabled.map(String::valueOf));
  92. setIfDefined(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, updateRequest.allowUsersToSignUp().map(String::valueOf));
  93. setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, updateRequest.provisioningToken());
  94. setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, updateRequest.provisioningGroups().map(groups -> String.join(",", groups)));
  95. boolean shouldTriggerProvisioning =
  96. provisioningEnabled.orElse(false) && !currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
  97. deleteExternalGroupsWhenDisablingAutoProvisioning(dbSession, currentConfiguration, updateRequest.provisioningType());
  98. GitlabConfiguration updatedConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
  99. if (shouldTriggerProvisioning) {
  100. triggerRun(updatedConfiguration);
  101. }
  102. dbSession.commit();
  103. return updatedConfiguration;
  104. }
  105. }
  106. private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
  107. value
  108. .map(definedValue -> new PropertyDto().setKey(propertyName).setValue(definedValue))
  109. .applyIfDefined(property -> dbClient.propertiesDao().saveProperty(dbSession, property));
  110. threadLocalSettings.setProperty(propertyName, value.orElse(null));
  111. }
  112. private void deleteExternalGroupsWhenDisablingAutoProvisioning(
  113. DbSession dbSession,
  114. GitlabConfiguration currentConfiguration,
  115. UpdatedValue<ProvisioningType> provisioningTypeFromUpdate) {
  116. boolean disableAutoProvisioning =
  117. provisioningTypeFromUpdate.map(provisioningType -> provisioningType.equals(JIT)).orElse(false)
  118. && currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
  119. if (disableAutoProvisioning) {
  120. dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
  121. }
  122. }
  123. public GitlabConfiguration getConfiguration(String id) {
  124. try (DbSession dbSession = dbClient.openSession(false)) {
  125. throwIfNotUniqueConfigurationId(id);
  126. throwIfConfigurationDoesntExist(dbSession);
  127. return getConfiguration(id, dbSession);
  128. }
  129. }
  130. public Optional<GitlabConfiguration> findConfigurations() {
  131. try (DbSession dbSession = dbClient.openSession(false)) {
  132. if (dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED) == null) {
  133. return Optional.empty();
  134. }
  135. return Optional.of(getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession));
  136. }
  137. }
  138. private Boolean getBooleanOrFalse(DbSession dbSession, String property) {
  139. return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
  140. .map(dto -> Boolean.valueOf(dto.getValue())).orElse(false);
  141. }
  142. private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
  143. return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
  144. .map(PropertyDto::getValue).orElse("");
  145. }
  146. private String getStringPropertyOrNull(DbSession dbSession, String property) {
  147. return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
  148. .map(dto -> Strings.emptyToNull(dto.getValue())).orElse(null);
  149. }
  150. private static void throwIfNotUniqueConfigurationId(String id) {
  151. if (!UNIQUE_GITLAB_CONFIGURATION_ID.equals(id)) {
  152. throw new NotFoundException(format("Gitlab configuration with id %s not found", id));
  153. }
  154. }
  155. public void deleteConfiguration(String id) {
  156. throwIfNotUniqueConfigurationId(id);
  157. try (DbSession dbSession = dbClient.openSession(false)) {
  158. throwIfConfigurationDoesntExist(dbSession);
  159. GITLAB_CONFIGURATION_PROPERTIES.forEach(property -> dbClient.propertiesDao().deleteGlobalProperty(property, dbSession));
  160. dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
  161. dbSession.commit();
  162. }
  163. }
  164. private void throwIfConfigurationDoesntExist(DbSession dbSession) {
  165. checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED), "GitLab configuration doesn't exist.");
  166. }
  167. private static ProvisioningType toProvisioningType(boolean provisioningEnabled) {
  168. return provisioningEnabled ? AUTO_PROVISIONING : JIT;
  169. }
  170. public GitlabConfiguration createConfiguration(GitlabConfiguration configuration) {
  171. throwIfConfigurationAlreadyExists();
  172. boolean enableAutoProvisioning = shouldEnableAutoProvisioning(configuration.provisioningType());
  173. try (DbSession dbSession = dbClient.openSession(false)) {
  174. setProperty(dbSession, GITLAB_AUTH_ENABLED, String.valueOf(configuration.enabled()));
  175. setProperty(dbSession, GITLAB_AUTH_APPLICATION_ID, configuration.applicationId());
  176. setProperty(dbSession, GITLAB_AUTH_URL, configuration.url());
  177. setProperty(dbSession, GITLAB_AUTH_SECRET, configuration.secret());
  178. setProperty(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, String.valueOf(configuration.synchronizeGroups()));
  179. setProperty(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, String.valueOf(enableAutoProvisioning));
  180. setProperty(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, String.valueOf(configuration.allowUsersToSignUp()));
  181. setProperty(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, configuration.provisioningToken());
  182. setProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, String.join(",", configuration.provisioningGroups()));
  183. if (enableAutoProvisioning) {
  184. triggerRun(configuration);
  185. }
  186. GitlabConfiguration createdConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
  187. dbSession.commit();
  188. return createdConfiguration;
  189. }
  190. }
  191. private void throwIfConfigurationAlreadyExists() {
  192. Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(GITLAB_AUTH_ENABLED)).ifPresent(property -> {
  193. throw BadRequestException.create("GitLab configuration already exists. Only one Gitlab configuration is supported.");
  194. });
  195. }
  196. private static boolean shouldEnableAutoProvisioning(ProvisioningType provisioningType) {
  197. return AUTO_PROVISIONING.equals(provisioningType);
  198. }
  199. private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
  200. dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
  201. }
  202. private GitlabConfiguration getConfiguration(String id, DbSession dbSession) {
  203. throwIfNotUniqueConfigurationId(id);
  204. throwIfConfigurationDoesntExist(dbSession);
  205. return new GitlabConfiguration(
  206. UNIQUE_GITLAB_CONFIGURATION_ID,
  207. getBooleanOrFalse(dbSession, GITLAB_AUTH_ENABLED),
  208. getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_APPLICATION_ID),
  209. getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_URL),
  210. getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_SECRET),
  211. getBooleanOrFalse(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS),
  212. toProvisioningType(getBooleanOrFalse(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED)),
  213. getBooleanOrFalse(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP),
  214. getStringPropertyOrNull(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN),
  215. getProvisioningGroups(dbSession)
  216. );
  217. }
  218. private Set<String> getProvisioningGroups(DbSession dbSession) {
  219. return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS))
  220. .map(dto -> Arrays.stream(dto.getValue().split(","))
  221. .filter(s -> !s.isEmpty())
  222. .collect(Collectors.toSet())
  223. ).orElse(Set.of());
  224. }
  225. public void triggerRun() {
  226. GitlabConfiguration configuration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID);
  227. triggerRun(configuration);
  228. }
  229. private void triggerRun(GitlabConfiguration gitlabConfiguration) {
  230. throwIfConfigIncompleteOrInstanceAlreadyManaged(gitlabConfiguration);
  231. managedInstanceService.queueSynchronisationTask();
  232. }
  233. private void throwIfConfigIncompleteOrInstanceAlreadyManaged(GitlabConfiguration configuration) {
  234. checkInstanceNotManagedByAnotherProvider();
  235. checkState(AUTO_PROVISIONING.equals(configuration.provisioningType()), "Auto provisioning must be activated");
  236. checkState(configuration.enabled(), getErrorMessage("GitLab authentication must be turned on"));
  237. checkState(isNotBlank(configuration.provisioningToken()), getErrorMessage("Provisioning token must be set"));
  238. }
  239. private void checkInstanceNotManagedByAnotherProvider() {
  240. if (managedInstanceService.isInstanceExternallyManaged()) {
  241. Optional.of(managedInstanceService.getProviderName()).filter(providerName -> !"gitlab".equals(providerName))
  242. .ifPresent(providerName -> {
  243. throw new IllegalStateException("It is not possible to synchronize SonarQube using GitLab, as it is already managed by "
  244. + providerName + ".");
  245. });
  246. }
  247. }
  248. private static String getErrorMessage(String prefix) {
  249. return format("%s to enable GitLab provisioning.", prefix);
  250. }
  251. public Optional<String> validate(GitlabConfiguration gitlabConfiguration) {
  252. if (!gitlabConfiguration.enabled()) {
  253. return Optional.empty();
  254. }
  255. String url = (gitlabConfiguration.url() + "/api/v4").replace("//", "/");
  256. try {
  257. gitlabGlobalSettingsValidator.validate(
  258. toValidationMode(gitlabConfiguration.provisioningType()),
  259. url,
  260. gitlabConfiguration.provisioningToken()
  261. );
  262. } catch (Exception e) {
  263. return Optional.of(e.getMessage());
  264. }
  265. return Optional.empty();
  266. }
  267. private static GitlabGlobalSettingsValidator.ValidationMode toValidationMode(ProvisioningType provisioningType) {
  268. return AUTO_PROVISIONING.equals(provisioningType) ? COMPLETE : AUTH_ONLY;
  269. }
  270. }