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;
40 import static java.lang.String.format;
41 import static org.apache.commons.lang.StringUtils.isNotBlank;
42 import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.AUTH_ONLY;
43 import static org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator.ValidationMode.COMPLETE;
44 import static org.sonar.api.utils.Preconditions.checkState;
45 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
46 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
47 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
48 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_ENABLED;
49 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_GROUPS;
50 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_TOKEN;
51 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
52 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
53 import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
54 import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
55 import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
56 import static org.sonar.server.exceptions.NotFoundException.checkFound;
59 public class GitlabConfigurationService {
61 private static final List<String> GITLAB_CONFIGURATION_PROPERTIES = List.of(
63 GITLAB_AUTH_APPLICATION_ID,
66 GITLAB_AUTH_SYNC_USER_GROUPS,
67 GITLAB_AUTH_PROVISIONING_ENABLED,
68 GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP,
69 GITLAB_AUTH_PROVISIONING_TOKEN,
70 GITLAB_AUTH_PROVISIONING_GROUPS);
72 public static final String UNIQUE_GITLAB_CONFIGURATION_ID = "gitlab-configuration";
73 private final DbClient dbClient;
74 private final ManagedInstanceService managedInstanceService;
75 private final GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator;
77 public GitlabConfigurationService(DbClient dbClient,
78 ManagedInstanceService managedInstanceService, GitlabGlobalSettingsValidator gitlabGlobalSettingsValidator) {
79 this.dbClient = dbClient;
80 this.managedInstanceService = managedInstanceService;
81 this.gitlabGlobalSettingsValidator = gitlabGlobalSettingsValidator;
84 public GitlabConfiguration updateConfiguration(UpdateGitlabConfigurationRequest updateRequest) {
85 UpdatedValue<Boolean> provisioningEnabled =
86 updateRequest.provisioningType().map(GitlabConfigurationService::shouldEnableAutoProvisioning);
87 try (DbSession dbSession = dbClient.openSession(true)) {
88 throwIfConfigurationDoesntExist(dbSession);
89 GitlabConfiguration currentConfiguration = getConfiguration(updateRequest.gitlabConfigurationId(), dbSession);
90 setIfDefined(dbSession, GITLAB_AUTH_ENABLED, updateRequest.enabled().map(String::valueOf));
91 setIfDefined(dbSession, GITLAB_AUTH_APPLICATION_ID, updateRequest.applicationId());
92 setIfDefined(dbSession, GITLAB_AUTH_URL, updateRequest.url());
93 setIfDefined(dbSession, GITLAB_AUTH_SECRET, updateRequest.secret());
94 setIfDefined(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, updateRequest.synchronizeGroups().map(String::valueOf));
95 setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, provisioningEnabled.map(String::valueOf));
96 setIfDefined(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, updateRequest.allowUsersToSignUp().map(String::valueOf));
97 setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, updateRequest.provisioningToken());
98 setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, updateRequest.provisioningGroups().map(groups -> String.join(",", groups)));
99 boolean shouldTriggerProvisioning =
100 provisioningEnabled.orElse(false) && !currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
101 deleteExternalGroupsWhenDisablingAutoProvisioning(dbSession, currentConfiguration, updateRequest.provisioningType());
102 GitlabConfiguration updatedConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
103 if (shouldTriggerProvisioning) {
104 triggerRun(updatedConfiguration);
107 return updatedConfiguration;
111 private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
113 .map(definedValue -> new PropertyDto().setKey(propertyName).setValue(definedValue))
114 .applyIfDefined(property -> dbClient.propertiesDao().saveProperty(dbSession, property));
117 private void deleteExternalGroupsWhenDisablingAutoProvisioning(
119 GitlabConfiguration currentConfiguration,
120 UpdatedValue<ProvisioningType> provisioningTypeFromUpdate) {
121 boolean disableAutoProvisioning =
122 provisioningTypeFromUpdate.map(provisioningType -> provisioningType.equals(JIT)).orElse(false)
123 && currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
124 if (disableAutoProvisioning) {
125 dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
129 public GitlabConfiguration getConfiguration(String id) {
130 try (DbSession dbSession = dbClient.openSession(false)) {
131 throwIfNotUniqueConfigurationId(id);
132 throwIfConfigurationDoesntExist(dbSession);
133 return getConfiguration(id, dbSession);
137 public Optional<GitlabConfiguration> findConfigurations() {
138 try (DbSession dbSession = dbClient.openSession(false)) {
139 if (dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED) == null) {
140 return Optional.empty();
142 return Optional.of(getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession));
146 private Boolean getBooleanOrFalse(DbSession dbSession, String property) {
147 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
148 .map(dto -> Boolean.valueOf(dto.getValue())).orElse(false);
151 private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
152 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
153 .map(PropertyDto::getValue).orElse("");
156 private String getStringPropertyOrNull(DbSession dbSession, String property) {
157 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
158 .map(dto -> Strings.emptyToNull(dto.getValue())).orElse(null);
161 private static void throwIfNotUniqueConfigurationId(String id) {
162 if (!UNIQUE_GITLAB_CONFIGURATION_ID.equals(id)) {
163 throw new NotFoundException(format("Gitlab configuration with id %s not found", id));
167 public void deleteConfiguration(String id) {
168 throwIfNotUniqueConfigurationId(id);
169 try (DbSession dbSession = dbClient.openSession(false)) {
170 throwIfConfigurationDoesntExist(dbSession);
171 GITLAB_CONFIGURATION_PROPERTIES.forEach(property -> dbClient.propertiesDao().deleteGlobalProperty(property, dbSession));
172 dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
177 private void throwIfConfigurationDoesntExist(DbSession dbSession) {
178 checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED), "GitLab configuration doesn't exist.");
181 private static ProvisioningType toProvisioningType(boolean provisioningEnabled) {
182 return provisioningEnabled ? AUTO_PROVISIONING : JIT;
185 public GitlabConfiguration createConfiguration(GitlabConfiguration configuration) {
186 throwIfConfigurationAlreadyExists();
188 boolean enableAutoProvisioning = shouldEnableAutoProvisioning(configuration.provisioningType());
189 try (DbSession dbSession = dbClient.openSession(false)) {
190 setProperty(dbSession, GITLAB_AUTH_ENABLED, String.valueOf(configuration.enabled()));
191 setProperty(dbSession, GITLAB_AUTH_APPLICATION_ID, configuration.applicationId());
192 setProperty(dbSession, GITLAB_AUTH_URL, configuration.url());
193 setProperty(dbSession, GITLAB_AUTH_SECRET, configuration.secret());
194 setProperty(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, String.valueOf(configuration.synchronizeGroups()));
195 setProperty(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, String.valueOf(enableAutoProvisioning));
196 setProperty(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, String.valueOf(configuration.allowUsersToSignUp()));
197 setProperty(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, configuration.provisioningToken());
198 setProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, String.join(",", configuration.provisioningGroups()));
199 if (enableAutoProvisioning) {
200 triggerRun(configuration);
202 GitlabConfiguration createdConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
204 return createdConfiguration;
209 private void throwIfConfigurationAlreadyExists() {
210 Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(GITLAB_AUTH_ENABLED)).ifPresent(property -> {
211 throw BadRequestException.create("GitLab configuration already exists. Only one Gitlab configuration is supported.");
215 private static boolean shouldEnableAutoProvisioning(ProvisioningType provisioningType) {
216 return AUTO_PROVISIONING.equals(provisioningType);
219 private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
220 dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
223 private GitlabConfiguration getConfiguration(String id, DbSession dbSession) {
224 throwIfNotUniqueConfigurationId(id);
225 throwIfConfigurationDoesntExist(dbSession);
226 return new GitlabConfiguration(
227 UNIQUE_GITLAB_CONFIGURATION_ID,
228 getBooleanOrFalse(dbSession, GITLAB_AUTH_ENABLED),
229 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_APPLICATION_ID),
230 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_URL),
231 getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_SECRET),
232 getBooleanOrFalse(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS),
233 toProvisioningType(getBooleanOrFalse(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED)),
234 getBooleanOrFalse(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP),
235 getStringPropertyOrNull(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN),
236 getProvisioningGroups(dbSession)
240 private Set<String> getProvisioningGroups(DbSession dbSession) {
241 return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS))
242 .map(dto -> Arrays.stream(dto.getValue().split(","))
243 .filter(s -> !s.isEmpty())
244 .collect(Collectors.toSet())
248 public void triggerRun() {
249 GitlabConfiguration configuration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID);
250 triggerRun(configuration);
253 private void triggerRun(GitlabConfiguration gitlabConfiguration) {
254 throwIfConfigIncompleteOrInstanceAlreadyManaged(gitlabConfiguration);
255 managedInstanceService.queueSynchronisationTask();
258 private void throwIfConfigIncompleteOrInstanceAlreadyManaged(GitlabConfiguration configuration) {
259 checkInstanceNotManagedByAnotherProvider();
260 checkState(AUTO_PROVISIONING.equals(configuration.provisioningType()), "Auto provisioning must be activated");
261 checkState(configuration.enabled(), getErrorMessage("GitLab authentication must be turned on"));
262 checkState(isNotBlank(configuration.provisioningToken()), getErrorMessage("Provisioning token must be set"));
265 private void checkInstanceNotManagedByAnotherProvider() {
266 if (managedInstanceService.isInstanceExternallyManaged()) {
267 Optional.of(managedInstanceService.getProviderName()).filter(providerName -> !"gitlab".equals(providerName))
268 .ifPresent(providerName -> {
269 throw new IllegalStateException("It is not possible to synchronize SonarQube using GitLab, as it is already managed by "
270 + providerName + ".");
275 private static String getErrorMessage(String prefix) {
276 return format("%s to enable GitLab provisioning.", prefix);
279 public Optional<String> validate(GitlabConfiguration gitlabConfiguration) {
280 if (!gitlabConfiguration.enabled()) {
281 return Optional.empty();
283 String url = (gitlabConfiguration.url() + "/api/v4").replace("//", "/");
285 gitlabGlobalSettingsValidator.validate(
286 toValidationMode(gitlabConfiguration.provisioningType()),
288 gitlabConfiguration.provisioningToken()
290 } catch (Exception e) {
291 return Optional.of(e.getMessage());
293 return Optional.empty();
296 private static GitlabGlobalSettingsValidator.ValidationMode toValidationMode(ProvisioningType provisioningType) {
297 return AUTO_PROVISIONING.equals(provisioningType) ? COMPLETE : AUTH_ONLY;