]> source.dussan.org Git - sonarqube.git/blob
25d1a4977aa0ce7cf9d15adc93a760bbfe15ee20
[sonarqube.git] /
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
22 import com.google.common.base.Strings;
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.Optional;
26 import java.util.Set;
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;
40
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;
58
59 @ServerSide
60 public class GitlabConfigurationService {
61
62   private static final List<String> GITLAB_CONFIGURATION_PROPERTIES = List.of(
63     GITLAB_AUTH_ENABLED,
64     GITLAB_AUTH_APPLICATION_ID,
65     GITLAB_AUTH_URL,
66     GITLAB_AUTH_SECRET,
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);
72
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;
78
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;
85   }
86
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);
92
93       ProvisioningType provisioningType = updateRequest.provisioningType().orElse(currentConfiguration.provisioningType());
94       Set<String> allowedGroups = updateRequest.allowedGroups().orElse(currentConfiguration.allowedGroups());
95       throwIfAllowedGroupsEmptyAndAutoProvisioning(provisioningType, allowedGroups);
96
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);
111       }
112       dbSession.commit();
113       return updatedConfiguration;
114     }
115   }
116
117   private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
118     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));
122   }
123
124   private void deleteExternalGroupsWhenDisablingAutoProvisioning(
125     DbSession dbSession,
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);
132     }
133   }
134
135   public GitlabConfiguration getConfiguration(String id) {
136     try (DbSession dbSession = dbClient.openSession(false)) {
137       throwIfNotUniqueConfigurationId(id);
138       throwIfConfigurationDoesntExist(dbSession);
139       return getConfiguration(id, dbSession);
140     }
141   }
142
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();
147       }
148       return Optional.of(getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession));
149     }
150   }
151
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);
155   }
156
157   private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
158     return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
159       .map(PropertyDto::getValue).orElse("");
160   }
161
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);
165   }
166
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));
170     }
171   }
172
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);
179       dbSession.commit();
180     }
181   }
182
183   private void throwIfConfigurationDoesntExist(DbSession dbSession) {
184     checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED), "GitLab configuration doesn't exist.");
185   }
186
187   private static ProvisioningType toProvisioningType(boolean provisioningEnabled) {
188     return provisioningEnabled ? AUTO_PROVISIONING : JIT;
189   }
190
191   public GitlabConfiguration createConfiguration(GitlabConfiguration configuration) {
192     throwIfConfigurationAlreadyExists();
193     throwIfAllowedGroupsEmptyAndAutoProvisioning(configuration.provisioningType(), configuration.allowedGroups());
194
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);
208       }
209       GitlabConfiguration createdConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
210       dbSession.commit();
211       return createdConfiguration;
212     }
213
214   }
215
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.");
219     });
220   }
221
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.");
225     }
226   }
227
228   private static boolean shouldEnableAutoProvisioning(ProvisioningType provisioningType) {
229     return AUTO_PROVISIONING.equals(provisioningType);
230   }
231
232   private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
233     dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
234   }
235
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));
250   }
251
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()))
257       .orElse(Set.of());
258   }
259
260   public void triggerRun() {
261     GitlabConfiguration configuration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID);
262     triggerRun(configuration);
263   }
264
265   private void triggerRun(GitlabConfiguration gitlabConfiguration) {
266     throwIfConfigIncompleteOrInstanceAlreadyManaged(gitlabConfiguration);
267     managedInstanceService.queueSynchronisationTask();
268   }
269
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"));
275   }
276
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 + ".");
283         });
284     }
285   }
286
287   private static String getErrorMessage(String prefix) {
288     return format("%s to enable GitLab provisioning.", prefix);
289   }
290
291   public Optional<String> validate(GitlabConfiguration gitlabConfiguration) {
292     if (!gitlabConfiguration.enabled()) {
293       return Optional.empty();
294     }
295     String url = (gitlabConfiguration.url() + "/api/v4").replace("//", "/");
296     try {
297       gitlabGlobalSettingsValidator.validate(
298         toValidationMode(gitlabConfiguration.provisioningType()),
299         url,
300         gitlabConfiguration.provisioningToken());
301     } catch (Exception e) {
302       return Optional.of(e.getMessage());
303     }
304     return Optional.empty();
305   }
306
307   private static GitlabGlobalSettingsValidator.ValidationMode toValidationMode(ProvisioningType provisioningType) {
308     return AUTO_PROVISIONING.equals(provisioningType) ? COMPLETE : AUTH_ONLY;
309   }
310 }