3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.server.common.gitlab.config;
22 import com.google.common.base.Strings;
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.Optional;
27 import java.util.stream.Collectors;
28 import javax.annotation.Nullable;
29 import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
30 import org.sonar.api.server.ServerSide;
31 import org.sonar.auth.gitlab.GitLabIdentityProvider;
32 import org.sonar.db.DbClient;
33 import org.sonar.db.DbSession;
34 import org.sonar.db.property.PropertyDto;
35 import org.sonar.server.common.UpdatedValue;
36 import org.sonar.server.exceptions.BadRequestException;
37 import org.sonar.server.exceptions.NotFoundException;
38 import org.sonar.server.management.ManagedInstanceService;
39 import org.sonar.server.setting.ThreadLocalSettings;
41 import static java.lang.String.format;
42 import static org.apache.commons.lang.StringUtils.isNotBlank;
43 import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.AUTH_ONLY;
44 import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.COMPLETE;
45 import static org.sonar.api.utils.Preconditions.checkState;
46 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOWED_GROUPS;
47 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
48 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
49 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
50 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_ENABLED;
51 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_TOKEN;
52 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
53 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
54 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
55 import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
56 import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
57 import static org.sonar.server.exceptions.NotFoundException.checkFound;
60 public class GitlabConfigurationService {
62 private static final List<String> GITLAB_CONFIGURATION_PROPERTIES = List.of(
64 GITLAB_AUTH_APPLICATION_ID,
67 GITLAB_AUTH_SYNC_USER_GROUPS,
68 GITLAB_AUTH_ALLOWED_GROUPS,
69 GITLAB_AUTH_PROVISIONING_ENABLED,
70 GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP,
71 GITLAB_AUTH_PROVISIONING_TOKEN);
73 public static final String UNIQUE_GITLAB_CONFIGURATION_ID = "gitlab-configuration";
74 private final DbClient dbClient;
75 private final ManagedInstanceService managedInstanceService;
76 private final GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator;
77 private final ThreadLocalSettings threadLocalSettings;
79 public GitlabConfigurationService(DbClient dbClient,
80 ManagedInstanceService managedInstanceService, GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator, ThreadLocalSettings threadLocalSettings) {
81 this.dbClient = dbClient;
82 this.managedInstanceService = managedInstanceService;
83 this.gitlabGlobalSettingsValidator = gitlabGlobalSettingsValidator;
84 this.threadLocalSettings = threadLocalSettings;
87 public GitlabConfiguration updateConfiguration(UpdateGitlabConfigurationRequest updateRequest) {
88 UpdatedValue<Boolean> provisioningEnabled = updateRequest.provisioningType().map(GitlabConfigurationService::shouldEnableAutoProvisioning);
89 try (DbSession dbSession = dbClient.openSession(true)) {
90 throwIfConfigurationDoesntExist(dbSession);
91 GitlabConfiguration currentConfiguration = getConfiguration(updateRequest.gitlabConfigurationId(), dbSession);
93 ProvisioningType provisioningType = updateRequest.provisioningType().orElse(currentConfiguration.provisioningType());
94 Set<String> allowedGroups = updateRequest.allowedGroups().orElse(currentConfiguration.allowedGroups());
95 throwIfAllowedGroupsEmptyAndAutoProvisioning(provisioningType, allowedGroups);
97 setIfDefined(dbSession, GITLAB_AUTH_ENABLED, updateRequest.enabled().map(String::valueOf));
98 setIfDefined(dbSession, GITLAB_AUTH_APPLICATION_ID, updateRequest.applicationId());
99 setIfDefined(dbSession, GITLAB_AUTH_URL, updateRequest.url());
100 setIfDefined(dbSession, GITLAB_AUTH_SECRET, updateRequest.secret());
101 setIfDefined(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, updateRequest.synchronizeGroups().map(String::valueOf));
102 setIfDefined(dbSession, GITLAB_AUTH_ALLOWED_GROUPS, updateRequest.allowedGroups().map(groups -> String.join(",", groups)));
103 setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, provisioningEnabled.map(String::valueOf));
104 setIfDefined(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, updateRequest.allowUsersToSignUp().map(String::valueOf));
105 setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, updateRequest.provisioningToken());
106 boolean shouldTriggerProvisioning = provisioningEnabled.orElse(false) && !currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
107 deleteExternalGroupsWhenDisablingAutoProvisioning(dbSession, currentConfiguration, updateRequest.provisioningType());
108 GitlabConfiguration updatedConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
109 if (shouldTriggerProvisioning) {
110 triggerRun(updatedConfiguration);
113 return updatedConfiguration;
117 private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
119 .map(definedValue -> new PropertyDto().setKey(propertyName).setValue(definedValue))
120 .applyIfDefined(property -> dbClient.propertiesDao().saveProperty(dbSession, property));
121 threadLocalSettings.setProperty(propertyName, value.orElse(null));
124 private void deleteExternalGroupsWhenDisablingAutoProvisioning(
126 GitlabConfiguration currentConfiguration,
127 UpdatedValue<ProvisioningType> provisioningTypeFromUpdate) {
128 boolean disableAutoProvisioning = provisioningTypeFromUpdate.map(provisioningType -> provisioningType.equals(JIT)).orElse(false)
129 && currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
130 if (disableAutoProvisioning) {
131 dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
135 public GitlabConfiguration getConfiguration(String id) {
136 try (DbSession dbSession = dbClient.openSession(false)) {
137 throwIfNotUniqueConfigurationId(id);
138 throwIfConfigurationDoesntExist(dbSession);
139 return getConfiguration(id, dbSession);
143 public Optional<GitlabConfiguration> findConfigurations() {
144 try (DbSession dbSession = dbClient.openSession(false)) {
145 if (dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED) == null) {
146 return Optional.empty();
148 return Optional.of(getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession));
152 private Boolean getBooleanOrFalse(DbSession dbSession, String property) {
153 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
154 .map(dto -> Boolean.valueOf(dto.getValue())).orElse(false);
157 private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
158 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
159 .map(PropertyDto::getValue).orElse("");
162 private String getStringPropertyOrNull(DbSession dbSession, String property) {
163 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
164 .map(dto -> Strings.emptyToNull(dto.getValue())).orElse(null);
167 private static void throwIfNotUniqueConfigurationId(String id) {
168 if (!UNIQUE_GITLAB_CONFIGURATION_ID.equals(id)) {
169 throw new NotFoundException(format("Gitlab configuration with id %s not found", id));
173 public void deleteConfiguration(String id) {
174 throwIfNotUniqueConfigurationId(id);
175 try (DbSession dbSession = dbClient.openSession(false)) {
176 throwIfConfigurationDoesntExist(dbSession);
177 GITLAB_CONFIGURATION_PROPERTIES.forEach(property -> dbClient.propertiesDao().deleteGlobalProperty(property, dbSession));
178 dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
183 private void throwIfConfigurationDoesntExist(DbSession dbSession) {
184 checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED), "GitLab configuration doesn't exist.");
187 private static ProvisioningType toProvisioningType(boolean provisioningEnabled) {
188 return provisioningEnabled ? AUTO_PROVISIONING : JIT;
191 public GitlabConfiguration createConfiguration(GitlabConfiguration configuration) {
192 throwIfConfigurationAlreadyExists();
193 throwIfAllowedGroupsEmptyAndAutoProvisioning(configuration.provisioningType(), configuration.allowedGroups());
195 boolean enableAutoProvisioning = shouldEnableAutoProvisioning(configuration.provisioningType());
196 try (DbSession dbSession = dbClient.openSession(false)) {
197 setProperty(dbSession, GITLAB_AUTH_ENABLED, String.valueOf(configuration.enabled()));
198 setProperty(dbSession, GITLAB_AUTH_APPLICATION_ID, configuration.applicationId());
199 setProperty(dbSession, GITLAB_AUTH_URL, configuration.url());
200 setProperty(dbSession, GITLAB_AUTH_SECRET, configuration.secret());
201 setProperty(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, String.valueOf(configuration.synchronizeGroups()));
202 setProperty(dbSession, GITLAB_AUTH_ALLOWED_GROUPS, String.join(",", configuration.allowedGroups()));
203 setProperty(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, String.valueOf(enableAutoProvisioning));
204 setProperty(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, String.valueOf(configuration.allowUsersToSignUp()));
205 setProperty(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, configuration.provisioningToken());
206 if (enableAutoProvisioning) {
207 triggerRun(configuration);
209 GitlabConfiguration createdConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
211 return createdConfiguration;
216 private void throwIfConfigurationAlreadyExists() {
217 Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(GITLAB_AUTH_ENABLED)).ifPresent(property -> {
218 throw BadRequestException.create("GitLab configuration already exists. Only one Gitlab configuration is supported.");
222 private static void throwIfAllowedGroupsEmptyAndAutoProvisioning(ProvisioningType provisioningType, Set<String> allowedGroups) {
223 if (provisioningType == AUTO_PROVISIONING && allowedGroups.isEmpty()) {
224 throw new IllegalArgumentException("allowedGroups cannot be empty when Auto-provisioning is enabled.");
228 private static boolean shouldEnableAutoProvisioning(ProvisioningType provisioningType) {
229 return AUTO_PROVISIONING.equals(provisioningType);
232 private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
233 dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
236 private GitlabConfiguration getConfiguration(String id, DbSession dbSession) {
237 throwIfNotUniqueConfigurationId(id);
238 throwIfConfigurationDoesntExist(dbSession);
239 return new GitlabConfiguration(
240 UNIQUE_GITLAB_CONFIGURATION_ID,
241 getBooleanOrFalse(dbSession, GITLAB_AUTH_ENABLED),
242 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_APPLICATION_ID),
243 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_URL),
244 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_SECRET),
245 getBooleanOrFalse(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS),
246 getAllowedGroups(dbSession),
247 getBooleanOrFalse(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP),
248 toProvisioningType(getBooleanOrFalse(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED)),
249 getStringPropertyOrNull(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN));
252 private Set<String> getAllowedGroups(DbSession dbSession) {
253 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ALLOWED_GROUPS))
254 .map(dto -> Arrays.stream(dto.getValue().split(","))
255 .filter(s -> !s.isEmpty())
256 .collect(Collectors.toSet()))
260 public void triggerRun() {
261 GitlabConfiguration configuration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID);
262 triggerRun(configuration);
265 private void triggerRun(GitlabConfiguration gitlabConfiguration) {
266 throwIfConfigIncompleteOrInstanceAlreadyManaged(gitlabConfiguration);
267 managedInstanceService.queueSynchronisationTask();
270 private void throwIfConfigIncompleteOrInstanceAlreadyManaged(GitlabConfiguration configuration) {
271 checkInstanceNotManagedByAnotherProvider();
272 checkState(AUTO_PROVISIONING.equals(configuration.provisioningType()), "Auto provisioning must be activated");
273 checkState(configuration.enabled(), getErrorMessage("GitLab authentication must be turned on"));
274 checkState(isNotBlank(configuration.provisioningToken()), getErrorMessage("Provisioning token must be set"));
277 private void checkInstanceNotManagedByAnotherProvider() {
278 if (managedInstanceService.isInstanceExternallyManaged()) {
279 Optional.of(managedInstanceService.getProviderName()).filter(providerName -> !"gitlab".equals(providerName))
280 .ifPresent(providerName -> {
281 throw new IllegalStateException("It is not possible to synchronize SonarQube using GitLab, as it is already managed by "
282 + providerName + ".");
287 private static String getErrorMessage(String prefix) {
288 return format("%s to enable GitLab provisioning.", prefix);
291 public Optional<String> validate(GitlabConfiguration gitlabConfiguration) {
292 if (!gitlabConfiguration.enabled()) {
293 return Optional.empty();
295 String url = (gitlabConfiguration.url() + "/api/v4").replace("//", "/");
297 gitlabGlobalSettingsValidator.validate(
298 toValidationMode(gitlabConfiguration.provisioningType()),
300 gitlabConfiguration.provisioningToken());
301 } catch (Exception e) {
302 return Optional.of(e.getMessage());
304 return Optional.empty();
307 private static GitlabGlobalSettingsValidator.ValidationMode toValidationMode(ProvisioningType provisioningType) {
308 return AUTO_PROVISIONING.equals(provisioningType) ? COMPLETE : AUTH_ONLY;