]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21121 Move Gitlab Config REST endpoints to Community edition
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>
Thu, 21 Dec 2023 16:19:14 +0000 (17:19 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 22 Dec 2023 20:03:03 +0000 (20:03 +0000)
23 files changed:
server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java
server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java
server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java
server/sonar-webserver-common/build.gradle
server/sonar-webserver-common/src/it/java/org/sonar/server/common/gitlab/config/GitlabConfigurationServiceIT.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfiguration.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfigurationService.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/SynchronizationType.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/UpdateGitlabConfigurationRequest.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/GitlabConfigurationSearchRestResponse.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java [new file with mode: 0644]

index 35038518b4d4c9ace6401306040cab71d1cd156c..ca17aa272bd1c3ce259b5e5266071b4e2e537dd0 100644 (file)
@@ -96,6 +96,12 @@ public class DelegatingManagedServices implements ManagedInstanceService, Manage
       .orElse(false);
   }
 
+  @Override
+  public void queueSynchronisationTask() {
+    findManagedInstanceService()
+      .ifPresent(ManagedInstanceService::queueSynchronisationTask);
+  }
+
   private Optional<ManagedInstanceService> findManagedInstanceService() {
     Set<ManagedInstanceService> managedInstanceServices = delegates.stream()
       .filter(ManagedInstanceService::isInstanceExternallyManaged)
index 628bfcb15491ea3bdb7a404e5091ba91bef1baf6..ae855ad9ff054c26ae5afa2cc1a9387fc895bb25 100644 (file)
@@ -43,4 +43,6 @@ public interface ManagedInstanceService {
 
   boolean isGroupManaged(DbSession dbSession, String groupUuid);
 
+  void queueSynchronisationTask();
+
 }
index 04bff1e2917c0d51a15a0f6a13953deabaabc64d..c93aa17f94262865194005fce34ed7f75b04d536 100644 (file)
@@ -211,6 +211,23 @@ public class DelegatingManagedServicesTest {
       true));
   }
 
+  @Test
+  public void queueSynchronisationTask_whenManagedNoInstanceServices_doesNotFail() {
+    assertThatNoException().isThrownBy(NO_MANAGED_SERVICES::queueSynchronisationTask);
+  }
+
+  @Test
+  public void queueSynchronisationTask_whenManagedInstanceServices_shouldDelegatesToRightService() {
+    NeverManagedInstanceService neverManagedInstanceService = spy(new NeverManagedInstanceService());
+    AlwaysManagedInstanceService alwaysManagedInstanceService = spy(new AlwaysManagedInstanceService());
+    Set<ManagedInstanceService> delegates = Set.of(neverManagedInstanceService, alwaysManagedInstanceService);
+    DelegatingManagedServices managedInstanceService = new DelegatingManagedServices(delegates);
+
+    managedInstanceService.queueSynchronisationTask();
+    verify(neverManagedInstanceService, never()).queueSynchronisationTask();
+    verify(alwaysManagedInstanceService).queueSynchronisationTask();
+  }
+
   private ManagedInstanceService getManagedInstanceService(Set<String> userUuids, Map<String, Boolean> uuidToManaged) {
     ManagedInstanceService anotherManagedInstanceService = mock(ManagedInstanceService.class);
     when(anotherManagedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
@@ -332,6 +349,11 @@ public class DelegatingManagedServicesTest {
       return false;
     }
 
+    @Override
+    public void queueSynchronisationTask() {
+
+    }
+
     @Override
     public Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids) {
       return null;
@@ -395,6 +417,11 @@ public class DelegatingManagedServicesTest {
       return true;
     }
 
+    @Override
+    public void queueSynchronisationTask() {
+
+    }
+
     @Override
     public Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids) {
       return null;
index 0061a41b513c1206906dab432db207a87298f4c1..dd5a07d43defe3b48cc44f95ffa6a5a3c1edd3e0 100644 (file)
@@ -11,8 +11,9 @@ dependencies {
     api project(':server:sonar-db-dao')
     api project(':server:sonar-webserver-auth')
     api project(':server:sonar-webserver-ws')
+  implementation project(path: ':server:sonar-auth-gitlab')
 
-    compileOnlyApi 'com.google.code.findbugs:jsr305'
+  compileOnlyApi 'com.google.code.findbugs:jsr305'
     compileOnlyApi 'javax.servlet:javax.servlet-api'
 
     testImplementation 'org.apache.logging.log4j:log4j-api'
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/gitlab/config/GitlabConfigurationServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/gitlab/config/GitlabConfigurationServiceIT.java
new file mode 100644 (file)
index 0000000..3929187
--- /dev/null
@@ -0,0 +1,487 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common.gitlab.config;
+
+import com.google.common.base.Strings;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.auth.gitlab.GitLabIdentityProvider;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.ExternalGroupDto;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.management.ManagedInstanceService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_ENABLED;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_GROUPS;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_TOKEN;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
+import static org.sonar.server.common.NonNullUpdatedValue.withValueOrThrow;
+import static org.sonar.server.common.UpdatedValue.withValue;
+import static org.sonar.server.common.gitlab.config.GitlabConfigurationService.UNIQUE_GITLAB_CONFIGURATION_ID;
+import static org.sonar.server.common.gitlab.config.UpdateGitlabConfigurationRequest.builder;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GitlabConfigurationServiceIT {
+
+  @Rule
+  public DbTester dbTester = DbTester.create();
+
+  @Mock
+  private ManagedInstanceService managedInstanceService;
+
+  private GitlabConfigurationService gitlabConfigurationService;
+
+  @Before
+  public void setUp() {
+    when(managedInstanceService.getProviderName()).thenReturn("gitlab");
+    gitlabConfigurationService = new GitlabConfigurationService(
+      managedInstanceService,
+      dbTester.getDbClient());
+  }
+
+  @Test
+  public void getConfiguration_whenIdIsNotGitlabConfiguration_throwsException() {
+    assertThatExceptionOfType(NotFoundException.class)
+      .isThrownBy(() -> gitlabConfigurationService.getConfiguration("not-gitlab-configuration"))
+      .withMessage("Gitlab configuration with id not-gitlab-configuration not found");
+  }
+
+  @Test
+  public void getConfiguration_whenNoConfiguration_throwsNotFoundException() {
+    assertThatThrownBy(() -> gitlabConfigurationService.getConfiguration("gitlab-configuration"))
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("GitLab configuration doesn't exist.");
+
+  }
+
+  @Test
+  public void getConfiguration_whenConfigurationSet_returnsConfig() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+
+    GitlabConfiguration configuration = gitlabConfigurationService.getConfiguration("gitlab-configuration");
+
+    assertConfigurationFields(configuration);
+  }
+
+  @Test
+  public void getConfiguration_whenConfigurationSetAndEmpty_returnsConfig() {
+    dbTester.properties().insertProperty(GITLAB_AUTH_ENABLED, "true", null);
+    dbTester.properties().insertProperty(GITLAB_AUTH_PROVISIONING_GROUPS, "", null);
+
+    GitlabConfiguration configuration = gitlabConfigurationService.getConfiguration("gitlab-configuration");
+
+    assertThat(configuration.id()).isEqualTo("gitlab-configuration");
+    assertThat(configuration.enabled()).isTrue();
+    assertThat(configuration.applicationId()).isEmpty();
+    assertThat(configuration.url()).isEmpty();
+    assertThat(configuration.secret()).isEmpty();
+    assertThat(configuration.synchronizeGroups()).isFalse();
+    assertThat(configuration.synchronizationType()).isEqualTo(SynchronizationType.JIT);
+    assertThat(configuration.allowUsersToSignUp()).isFalse();
+    assertThat(configuration.provisioningToken()).isNull();
+    assertThat(configuration.provisioningGroups()).isEmpty();
+  }
+
+  @Test
+  public void updateConfiguration_whenIdIsNotGitlabConfiguration_throwsException() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+    UpdateGitlabConfigurationRequest updateGitlabConfigurationRequest = builder().gitlabConfigurationId("not-gitlab-configuration").build();
+    assertThatExceptionOfType(NotFoundException.class)
+      .isThrownBy(() -> gitlabConfigurationService.updateConfiguration(updateGitlabConfigurationRequest))
+      .withMessage("Gitlab configuration with id not-gitlab-configuration not found");
+  }
+
+  @Test
+  public void updateConfiguration_whenConfigurationDoesntExist_throwsException() {
+    UpdateGitlabConfigurationRequest updateGitlabConfigurationRequest = builder().gitlabConfigurationId("gitlab-configuration").build();
+    assertThatExceptionOfType(NotFoundException.class)
+      .isThrownBy(() -> gitlabConfigurationService.updateConfiguration(updateGitlabConfigurationRequest))
+      .withMessage("GitLab configuration doesn't exist.");
+  }
+
+  @Test
+  public void updateConfiguration_whenAllUpdateFieldDefined_updatesEverything() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.JIT));
+
+    UpdateGitlabConfigurationRequest updateRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .enabled(withValueOrThrow(true))
+      .applicationId(withValueOrThrow("applicationId"))
+      .url(withValueOrThrow("url"))
+      .secret(withValueOrThrow("secret"))
+      .synchronizeGroups(withValueOrThrow(true))
+      .synchronizationType(withValueOrThrow(SynchronizationType.AUTO_PROVISIONING))
+      .allowUserToSignUp(withValueOrThrow(true))
+      .provisioningToken(withValueOrThrow("provisioningToken"))
+      .provisioningGroups(withValueOrThrow(new LinkedHashSet<>(List.of("group1", "group2", "group3"))))
+      .build();
+
+    GitlabConfiguration gitlabConfiguration = gitlabConfigurationService.updateConfiguration(updateRequest);
+
+    verifySettingWasSet(GITLAB_AUTH_ENABLED, "true");
+    verifySettingWasSet(GITLAB_AUTH_APPLICATION_ID, "applicationId");
+    verifySettingWasSet(GITLAB_AUTH_URL, "url");
+    verifySettingWasSet(GITLAB_AUTH_SECRET, "secret");
+    verifySettingWasSet(GITLAB_AUTH_SYNC_USER_GROUPS, "true");
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_ENABLED, "true");
+    verifySettingWasSet(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, "true");
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_TOKEN, "provisioningToken");
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_GROUPS, "group1,group2,group3");
+    verify(managedInstanceService).queueSynchronisationTask();
+
+    assertConfigurationFields(gitlabConfiguration);
+  }
+
+  @Test
+  public void updateConfiguration_whenAllUpdateFieldDefinedAndSetToFalse_updatesEverything() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+    verify(managedInstanceService).queueSynchronisationTask();
+    clearInvocations(managedInstanceService);
+
+    UpdateGitlabConfigurationRequest updateRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .enabled(withValueOrThrow(false))
+      .synchronizeGroups(withValueOrThrow(false))
+      .synchronizationType(withValueOrThrow(SynchronizationType.JIT))
+      .allowUserToSignUp(withValueOrThrow(false))
+      .build();
+
+    gitlabConfigurationService.updateConfiguration(updateRequest);
+
+    verifySettingWasSet(GITLAB_AUTH_ENABLED, "false");
+    verifySettingWasSet(GITLAB_AUTH_SYNC_USER_GROUPS, "false");
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_ENABLED, "false");
+    verifySettingWasSet(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, "false");
+    verifyNoMoreInteractions(managedInstanceService);
+
+  }
+
+  @Test
+  public void updateConfiguration_whenSwitchingFromAutoToJit_shouldNotScheduleSyncAndCallManagedInstanceChecker() {
+    DbSession dbSession = dbTester.getSession();
+    dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("12", "12", GitLabIdentityProvider.KEY));
+    dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("34", "34", GitLabIdentityProvider.KEY));
+    dbSession.commit();
+
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+    verify(managedInstanceService).queueSynchronisationTask();
+    reset(managedInstanceService);
+
+    UpdateGitlabConfigurationRequest updateRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .provisioningToken(withValue(null))
+      .synchronizationType(withValueOrThrow(SynchronizationType.JIT))
+      .build();
+
+    gitlabConfigurationService.updateConfiguration(updateRequest);
+
+    verifyNoMoreInteractions(managedInstanceService);
+    assertThat(dbTester.getDbClient().externalGroupDao().selectByIdentityProvider(dbTester.getSession(), GitLabIdentityProvider.KEY)).isEmpty();
+  }
+
+  @Test
+  public void updateConfiguration_whenSwitchingToAutoProvisioningAndTheConfigIsNotEnabled_shouldThrow() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.JIT));
+
+    UpdateGitlabConfigurationRequest disableRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .enabled(withValueOrThrow(false))
+      .build();
+
+    gitlabConfigurationService.updateConfiguration(disableRequest);
+
+    UpdateGitlabConfigurationRequest updateRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .synchronizationType(withValueOrThrow(SynchronizationType.AUTO_PROVISIONING))
+      .build();
+
+    assertThatThrownBy(() -> gitlabConfigurationService.updateConfiguration(updateRequest))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("GitLab authentication must be turned on to enable GitLab provisioning.");
+    verify(managedInstanceService, times(0)).queueSynchronisationTask();
+  }
+
+  @Test
+  public void updateConfiguration_whenSwitchingToAutoProvisioningAndProvisioningTokenIsNotDefined_shouldThrow() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.JIT));
+
+    UpdateGitlabConfigurationRequest removeTokenRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .provisioningToken(withValue(null))
+      .build();
+
+    gitlabConfigurationService.updateConfiguration(removeTokenRequest);
+
+    UpdateGitlabConfigurationRequest updateRequest = builder()
+      .gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID)
+      .synchronizationType(withValueOrThrow(SynchronizationType.AUTO_PROVISIONING))
+      .build();
+
+    assertThatThrownBy(() -> gitlabConfigurationService.updateConfiguration(updateRequest))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Provisioning token must be set to enable GitLab provisioning.");
+    verify(managedInstanceService, times(0)).queueSynchronisationTask();
+  }
+
+  private static void assertConfigurationFields(GitlabConfiguration configuration) {
+    assertThat(configuration).isNotNull();
+    assertThat(configuration.id()).isEqualTo("gitlab-configuration");
+    assertThat(configuration.enabled()).isTrue();
+    assertThat(configuration.applicationId()).isEqualTo("applicationId");
+    assertThat(configuration.url()).isEqualTo("url");
+    assertThat(configuration.secret()).isEqualTo("secret");
+    assertThat(configuration.synchronizeGroups()).isTrue();
+    assertThat(configuration.synchronizationType()).isEqualTo(SynchronizationType.AUTO_PROVISIONING);
+    assertThat(configuration.allowUsersToSignUp()).isTrue();
+    assertThat(configuration.provisioningToken()).isEqualTo("provisioningToken");
+    assertThat(configuration.provisioningGroups()).containsExactlyInAnyOrder("group1", "group2", "group3");
+  }
+
+  @Test
+  public void createConfiguration_whenConfigurationAlreadyExists_shouldThrow() {
+    GitlabConfiguration gitlabConfiguration = buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING);
+    gitlabConfigurationService.createConfiguration(gitlabConfiguration);
+
+    assertThatThrownBy(() -> gitlabConfigurationService.createConfiguration(gitlabConfiguration))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("GitLab configuration already exists. Only one Gitlab configuration is supported.");
+  }
+
+  @Test
+  public void createConfiguration_whenAutoProvisioning_shouldCreateCorrectConfigurationAndScheduleSync() {
+    GitlabConfiguration configuration = buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING);
+
+    GitlabConfiguration createdConfiguration = gitlabConfigurationService.createConfiguration(configuration);
+
+    assertThat(createdConfiguration).isEqualTo(configuration);
+
+    verifyCommonSettings(configuration);
+
+    verify(managedInstanceService).queueSynchronisationTask();
+
+  }
+
+  @Test
+  public void createConfiguration_whenAutoProvisioningConfigIsIncorrect_shouldThrow() {
+    GitlabConfiguration configuration = new GitlabConfiguration(
+      UNIQUE_GITLAB_CONFIGURATION_ID,
+      true,
+      "applicationId",
+      "url",
+      "secret",
+      true,
+      SynchronizationType.AUTO_PROVISIONING,
+      true,
+      null,
+      Set.of("group1", "group2", "group3"));
+
+    assertThatThrownBy(() -> gitlabConfigurationService.createConfiguration(configuration))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Provisioning token must be set to enable GitLab provisioning.");
+
+  }
+
+  @Test
+  public void createConfiguration_whenInstanceIsExternallyManaged_shouldThrow() {
+    GitlabConfiguration configuration = buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING);
+
+    when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
+    when(managedInstanceService.getProviderName()).thenReturn("not-gitlab");
+
+    assertThatIllegalStateException()
+      .isThrownBy(() -> gitlabConfigurationService.createConfiguration(configuration))
+      .withMessage("It is not possible to synchronize SonarQube using GitLab, as it is already managed by not-gitlab.");
+
+  }
+
+  @Test
+  public void createConfiguration_whenJitProvisioning_shouldCreateCorrectConfiguration() {
+    GitlabConfiguration configuration = buildGitlabConfiguration(SynchronizationType.JIT);
+
+    GitlabConfiguration createdConfiguration = gitlabConfigurationService.createConfiguration(configuration);
+
+    assertThat(createdConfiguration).isEqualTo(configuration);
+
+    verifyCommonSettings(configuration);
+    verifyNoInteractions(managedInstanceService);
+
+  }
+
+  @Test
+  public void createConfiguration_whenJitProvisioningAndProvisioningTokenNotSet_shouldCreateCorrectConfiguration() {
+    GitlabConfiguration configuration = new GitlabConfiguration(
+      UNIQUE_GITLAB_CONFIGURATION_ID,
+      true,
+      "applicationId",
+      "url",
+      "secret",
+      true,
+      SynchronizationType.JIT,
+      true,
+      null,
+      Set.of("group1", "group2", "group3"));
+
+    GitlabConfiguration createdConfiguration = gitlabConfigurationService.createConfiguration(configuration);
+
+    assertThat(createdConfiguration).isEqualTo(configuration);
+
+    verifyCommonSettings(configuration);
+    verifyNoInteractions(managedInstanceService);
+
+  }
+
+  private void verifyCommonSettings(GitlabConfiguration configuration) {
+    verifySettingWasSet(GITLAB_AUTH_ENABLED, String.valueOf(configuration.enabled()));
+    verifySettingWasSet(GITLAB_AUTH_APPLICATION_ID, configuration.applicationId());
+    verifySettingWasSet(GITLAB_AUTH_URL, configuration.url());
+    verifySettingWasSet(GITLAB_AUTH_SECRET, configuration.secret());
+    verifySettingWasSet(GITLAB_AUTH_SYNC_USER_GROUPS, String.valueOf(configuration.synchronizeGroups()));
+    verifySettingWasSet(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, String.valueOf(configuration.allowUsersToSignUp()));
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_TOKEN, Strings.nullToEmpty(configuration.provisioningToken()));
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_GROUPS, String.join(",", configuration.provisioningGroups()));
+    verifySettingWasSet(GITLAB_AUTH_PROVISIONING_ENABLED,
+      String.valueOf(configuration.synchronizationType().equals(SynchronizationType.AUTO_PROVISIONING)));
+  }
+
+  private void verifySettingWasSet(String setting, @Nullable String value) {
+    assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty(setting).getValue()).isEqualTo(value);
+  }
+
+  @Test
+  public void deleteConfiguration_whenIdIsNotGitlabConfiguration_throwsException() {
+    assertThatThrownBy(() -> gitlabConfigurationService.deleteConfiguration("not-gitlab-configuration"))
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("Gitlab configuration with id not-gitlab-configuration not found");
+  }
+
+  @Test
+  public void deleteConfiguration_whenConfigurationDoesntExist_throwsException() {
+    assertThatThrownBy(() -> gitlabConfigurationService.deleteConfiguration("gitlab-configuration"))
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("GitLab configuration doesn't exist.");
+  }
+
+  @Test
+  public void deleteConfiguration_whenConfigurationExists_shouldDeleteConfiguration() {
+    DbSession dbSession = dbTester.getSession();
+    dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("12", "12", GitLabIdentityProvider.KEY));
+    dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("34", "34", GitLabIdentityProvider.KEY));
+    dbSession.commit();
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+    gitlabConfigurationService.deleteConfiguration("gitlab-configuration");
+
+    assertPropertyIsDeleted(GITLAB_AUTH_ENABLED);
+    assertPropertyIsDeleted(GITLAB_AUTH_APPLICATION_ID);
+    assertPropertyIsDeleted(GITLAB_AUTH_URL);
+    assertPropertyIsDeleted(GITLAB_AUTH_SECRET);
+    assertPropertyIsDeleted(GITLAB_AUTH_SYNC_USER_GROUPS);
+    assertPropertyIsDeleted(GITLAB_AUTH_PROVISIONING_ENABLED);
+    assertPropertyIsDeleted(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP);
+    assertPropertyIsDeleted(GITLAB_AUTH_PROVISIONING_TOKEN);
+    assertPropertyIsDeleted(GITLAB_AUTH_PROVISIONING_GROUPS);
+
+    assertThat(dbTester.getDbClient().externalGroupDao().selectByIdentityProvider(dbTester.getSession(), GitLabIdentityProvider.KEY)).isEmpty();
+  }
+
+  private void assertPropertyIsDeleted(String property) {
+    assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty(property)).isNull();
+  }
+
+  @Test
+  public void triggerRun_whenConfigIsCorrect_shouldTriggerSync() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING));
+    reset(managedInstanceService);
+
+    gitlabConfigurationService.triggerRun();
+
+    verify(managedInstanceService).queueSynchronisationTask();
+  }
+
+  @Test
+  public void triggerRun_whenConfigIsForJit_shouldThrow() {
+    gitlabConfigurationService.createConfiguration(buildGitlabConfiguration(SynchronizationType.JIT));
+
+    assertThatIllegalStateException()
+      .isThrownBy(() -> gitlabConfigurationService.triggerRun())
+      .withMessage("Auto provisioning must be activated");
+  }
+
+  @Test
+  public void triggerRun_whenConfigIsDisabled_shouldThrow() {
+    GitlabConfiguration gitlabConfiguration = buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING);
+    gitlabConfigurationService.createConfiguration(gitlabConfiguration);
+    gitlabConfigurationService.updateConfiguration(builder().gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID).enabled(withValueOrThrow(false)).build());
+
+    assertThatIllegalStateException()
+      .isThrownBy(() -> gitlabConfigurationService.triggerRun())
+      .withMessage("GitLab authentication must be turned on to enable GitLab provisioning.");
+  }
+
+  @Test
+  public void triggerRun_whenProvisioningTokenIsNotSet_shouldThrow() {
+    GitlabConfiguration gitlabConfiguration = buildGitlabConfiguration(SynchronizationType.AUTO_PROVISIONING);
+    gitlabConfigurationService.createConfiguration(gitlabConfiguration);
+    gitlabConfigurationService.updateConfiguration(builder().gitlabConfigurationId(UNIQUE_GITLAB_CONFIGURATION_ID).provisioningToken(withValue(null)).build());
+
+    assertThatIllegalStateException()
+      .isThrownBy(() -> gitlabConfigurationService.triggerRun())
+      .withMessage("Provisioning token must be set to enable GitLab provisioning.");
+  }
+
+  private static GitlabConfiguration buildGitlabConfiguration(SynchronizationType synchronizationType) {
+    return new GitlabConfiguration(
+      UNIQUE_GITLAB_CONFIGURATION_ID,
+      true,
+      "applicationId",
+      "url",
+      "secret",
+      true,
+      synchronizationType,
+      true,
+      "provisioningToken",
+      Set.of("group1", "group2", "group3"));
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfiguration.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfiguration.java
new file mode 100644 (file)
index 0000000..a0f6ab7
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common.gitlab.config;
+
+import java.util.Set;
+import javax.annotation.Nullable;
+
+public record GitlabConfiguration(
+  String id,
+
+  boolean enabled,
+
+  String applicationId,
+
+  String url,
+
+  String secret,
+
+  boolean synchronizeGroups,
+
+  SynchronizationType synchronizationType,
+
+  boolean allowUsersToSignUp,
+
+  @Nullable
+  String provisioningToken,
+
+  Set<String> provisioningGroups
+) {
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfigurationService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/GitlabConfigurationService.java
new file mode 100644 (file)
index 0000000..3de2fa1
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common.gitlab.config;
+
+import com.google.common.base.Strings;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.Preconditions;
+import org.sonar.auth.gitlab.GitLabIdentityProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.common.UpdatedValue;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.management.ManagedInstanceService;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+import static org.sonar.api.utils.Preconditions.checkState;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_ENABLED;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_GROUPS;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_PROVISIONING_TOKEN;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
+import static org.sonar.server.common.gitlab.config.SynchronizationType.AUTO_PROVISIONING;
+import static org.sonar.server.exceptions.NotFoundException.checkFound;
+
+public class GitlabConfigurationService {
+
+  private static final List<String> GITLAB_CONFIGURATION_PROPERTIES = List.of(
+    GITLAB_AUTH_ENABLED,
+    GITLAB_AUTH_APPLICATION_ID,
+    GITLAB_AUTH_URL,
+    GITLAB_AUTH_SECRET,
+    GITLAB_AUTH_SYNC_USER_GROUPS,
+    GITLAB_AUTH_PROVISIONING_ENABLED,
+    GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP,
+    GITLAB_AUTH_PROVISIONING_TOKEN,
+    GITLAB_AUTH_PROVISIONING_GROUPS);
+
+  public static final String UNIQUE_GITLAB_CONFIGURATION_ID = "gitlab-configuration";
+  private final ManagedInstanceService managedInstanceService;
+  private final DbClient dbClient;
+
+  public GitlabConfigurationService(ManagedInstanceService managedInstanceService, DbClient dbClient) {
+    this.managedInstanceService = managedInstanceService;
+    this.dbClient = dbClient;
+  }
+
+  public GitlabConfiguration updateConfiguration(UpdateGitlabConfigurationRequest updateRequest) {
+    UpdatedValue<Boolean> provisioningEnabled =
+      updateRequest.synchronizationType().map(GitlabConfigurationService::shouldEnableAutoProvisioning);
+    try (DbSession dbSession = dbClient.openSession(true)) {
+      throwIfConfigurationDoesntExist(dbSession);
+      GitlabConfiguration currentConfiguration = getConfiguration(updateRequest.gitlabConfigurationId(), dbSession);
+      setIfDefined(dbSession, GITLAB_AUTH_ENABLED, updateRequest.enabled().map(String::valueOf));
+      setIfDefined(dbSession, GITLAB_AUTH_APPLICATION_ID, updateRequest.applicationId());
+      setIfDefined(dbSession, GITLAB_AUTH_URL, updateRequest.url());
+      setIfDefined(dbSession, GITLAB_AUTH_SECRET, updateRequest.secret());
+      setIfDefined(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, updateRequest.synchronizeGroups().map(String::valueOf));
+      setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, provisioningEnabled.map(String::valueOf));
+      setIfDefined(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, updateRequest.allowUsersToSignUp().map(String::valueOf));
+      setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, updateRequest.provisioningToken());
+      setIfDefined(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, updateRequest.provisioningGroups().map(groups -> String.join(",", groups)));
+      boolean shouldTriggerProvisioning =
+        provisioningEnabled.orElse(false) && !currentConfiguration.synchronizationType().equals(AUTO_PROVISIONING);
+      deleteExternalGroupsWhenDisablingAutoProvisioning(dbSession, currentConfiguration, updateRequest.synchronizationType());
+      GitlabConfiguration updatedConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
+      if (shouldTriggerProvisioning) {
+        triggerRun(updatedConfiguration);
+      }
+      dbSession.commit();
+      return updatedConfiguration;
+    }
+  }
+
+  private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
+    value
+      .map(definedValue -> new PropertyDto().setKey(propertyName).setValue(definedValue))
+      .applyIfDefined(property -> dbClient.propertiesDao().saveProperty(dbSession, property));
+  }
+
+  private void deleteExternalGroupsWhenDisablingAutoProvisioning(
+    DbSession dbSession,
+    GitlabConfiguration currentConfiguration,
+    UpdatedValue<SynchronizationType> synchronizationTypeFromUpdate) {
+    boolean disableAutoProvisioning = synchronizationTypeFromUpdate.map(synchronizationType -> synchronizationType.equals(SynchronizationType.JIT)).orElse(false)
+      && currentConfiguration.synchronizationType().equals(AUTO_PROVISIONING);
+    if (disableAutoProvisioning) {
+      dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
+    }
+  }
+
+  public GitlabConfiguration getConfiguration(String id) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      throwIfNotUniqueConfigurationId(id);
+      throwIfConfigurationDoesntExist(dbSession);
+      return getConfiguration(id, dbSession);
+    }
+  }
+
+  public Optional<GitlabConfiguration> findConfigurations() {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      if (dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED) == null) {
+        return Optional.empty();
+      }
+      return Optional.of(getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession));
+    }
+  }
+
+  private Boolean getBooleanOrFalse(DbSession dbSession, String property) {
+    return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+      .map(dto -> Boolean.valueOf(dto.getValue())).orElse(false);
+  }
+
+  private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
+    return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+      .map(PropertyDto::getValue).orElse("");
+  }
+
+  private String getStringPropertyOrNull(DbSession dbSession, String property) {
+    return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+      .map(dto -> Strings.emptyToNull(dto.getValue())).orElse(null);
+  }
+
+  private static void throwIfNotUniqueConfigurationId(String id) {
+    if (!UNIQUE_GITLAB_CONFIGURATION_ID.equals(id)) {
+      throw new NotFoundException(format("Gitlab configuration with id %s not found", id));
+    }
+  }
+
+  public void deleteConfiguration(String id) {
+    throwIfNotUniqueConfigurationId(id);
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      throwIfConfigurationDoesntExist(dbSession);
+      GITLAB_CONFIGURATION_PROPERTIES.forEach(property -> dbClient.propertiesDao().deleteGlobalProperty(property, dbSession));
+      dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitLabIdentityProvider.KEY);
+      dbSession.commit();
+    }
+  }
+
+  private void throwIfConfigurationDoesntExist(DbSession dbSession) {
+    checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_ENABLED), "GitLab configuration doesn't exist.");
+  }
+
+  private static SynchronizationType toSynchronizationType(boolean provisioningEnabled) {
+    return provisioningEnabled ? AUTO_PROVISIONING : SynchronizationType.JIT;
+  }
+
+  public GitlabConfiguration createConfiguration(GitlabConfiguration configuration) {
+    throwIfConfigurationAlreadyExists();
+
+    boolean enableAutoProvisioning = shouldEnableAutoProvisioning(configuration.synchronizationType());
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      setProperty(dbSession, GITLAB_AUTH_ENABLED, String.valueOf(configuration.enabled()));
+      setProperty(dbSession, GITLAB_AUTH_APPLICATION_ID, configuration.applicationId());
+      setProperty(dbSession, GITLAB_AUTH_URL, configuration.url());
+      setProperty(dbSession, GITLAB_AUTH_SECRET, configuration.secret());
+      setProperty(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS, String.valueOf(configuration.synchronizeGroups()));
+      setProperty(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED, String.valueOf(enableAutoProvisioning));
+      setProperty(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, String.valueOf(configuration.allowUsersToSignUp()));
+      setProperty(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN, configuration.provisioningToken());
+      setProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS, String.join(",", configuration.provisioningGroups()));
+      if (enableAutoProvisioning) {
+        triggerRun(configuration);
+      }
+      GitlabConfiguration createdConfiguration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID, dbSession);
+      dbSession.commit();
+      return createdConfiguration;
+    }
+
+  }
+
+  private void throwIfConfigurationAlreadyExists() {
+    Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(GITLAB_AUTH_ENABLED)).ifPresent(property -> {
+      throw BadRequestException.create("GitLab configuration already exists. Only one Gitlab configuration is supported.");
+    });
+  }
+
+  private static boolean shouldEnableAutoProvisioning(SynchronizationType synchronizationType) {
+    return AUTO_PROVISIONING.equals(synchronizationType);
+  }
+
+  private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
+    dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
+  }
+
+  private GitlabConfiguration getConfiguration(String id, DbSession dbSession) {
+    throwIfNotUniqueConfigurationId(id);
+    throwIfConfigurationDoesntExist(dbSession);
+    return new GitlabConfiguration(
+      UNIQUE_GITLAB_CONFIGURATION_ID,
+      getBooleanOrFalse(dbSession, GITLAB_AUTH_ENABLED),
+      getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_APPLICATION_ID),
+      getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_URL),
+      getStringPropertyOrEmpty(dbSession, GITLAB_AUTH_SECRET),
+      getBooleanOrFalse(dbSession, GITLAB_AUTH_SYNC_USER_GROUPS),
+      toSynchronizationType(getBooleanOrFalse(dbSession, GITLAB_AUTH_PROVISIONING_ENABLED)),
+      getBooleanOrFalse(dbSession, GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP),
+      getStringPropertyOrNull(dbSession, GITLAB_AUTH_PROVISIONING_TOKEN),
+      getProvisioningGroups(dbSession)
+    );
+  }
+
+  private Set<String> getProvisioningGroups(DbSession dbSession) {
+    return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITLAB_AUTH_PROVISIONING_GROUPS))
+      .map(dto -> Arrays.stream(dto.getValue().split(","))
+        .filter(s -> !s.isEmpty())
+        .collect(Collectors.toSet())
+      ).orElse(Set.of());
+  }
+
+  public void triggerRun() {
+    GitlabConfiguration configuration = getConfiguration(UNIQUE_GITLAB_CONFIGURATION_ID);
+    triggerRun(configuration);
+  }
+
+  private void triggerRun(GitlabConfiguration gitlabConfiguration) {
+    throwIfConfigIncompleteOrInstanceAlreadyManaged(gitlabConfiguration);
+    managedInstanceService.queueSynchronisationTask();
+
+  }
+
+  private void throwIfConfigIncompleteOrInstanceAlreadyManaged(GitlabConfiguration configuration) {
+    checkInstanceNotManagedByAnotherProvider();
+    Preconditions.checkState(AUTO_PROVISIONING.equals(configuration.synchronizationType()), "Auto provisioning must be activated");
+    Preconditions.checkState(configuration.enabled(), getErrorMessage("GitLab authentication must be turned on"));
+    checkState(isNotBlank(configuration.provisioningToken()), getErrorMessage("Provisioning token must be set"));
+  }
+
+  private void checkInstanceNotManagedByAnotherProvider() {
+    if (managedInstanceService.isInstanceExternallyManaged()) {
+      Optional.of(managedInstanceService.getProviderName()).filter(providerName -> !"gitlab".equals(providerName))
+        .ifPresent(providerName -> {
+          throw new IllegalStateException("It is not possible to synchronize SonarQube using GitLab, as it is already managed by "
+            + providerName + ".");
+        });
+    }
+  }
+
+  private static String getErrorMessage(String prefix) {
+    return format("%s to enable GitLab provisioning.", prefix);
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/SynchronizationType.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/SynchronizationType.java
new file mode 100644 (file)
index 0000000..3e57592
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common.gitlab.config;
+
+public enum SynchronizationType {
+  JIT,
+  AUTO_PROVISIONING
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/UpdateGitlabConfigurationRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/UpdateGitlabConfigurationRequest.java
new file mode 100644 (file)
index 0000000..9e120a5
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.common.gitlab.config;
+
+import java.util.Set;
+import org.sonar.server.common.NonNullUpdatedValue;
+import org.sonar.server.common.UpdatedValue;
+
+public record UpdateGitlabConfigurationRequest(
+  String gitlabConfigurationId,
+  NonNullUpdatedValue<Boolean> enabled,
+  NonNullUpdatedValue<String> applicationId,
+  NonNullUpdatedValue<String> url,
+  NonNullUpdatedValue<String> secret,
+  NonNullUpdatedValue<Boolean> synchronizeGroups,
+  NonNullUpdatedValue<SynchronizationType> synchronizationType,
+  NonNullUpdatedValue<Boolean> allowUsersToSignUp,
+  UpdatedValue<String> provisioningToken,
+  NonNullUpdatedValue<Set<String>> provisioningGroups
+) {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static final class Builder {
+    private String gitlabConfigurationId;
+    private NonNullUpdatedValue<Boolean> enabled = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<String> applicationId = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<String> url = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<String> secret = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<Boolean> synchronizeGroups = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<SynchronizationType> synchronizationType = NonNullUpdatedValue.undefined();
+    private NonNullUpdatedValue<Boolean> allowUserToSignUp = NonNullUpdatedValue.undefined();
+    private UpdatedValue<String> provisioningToken = UpdatedValue.undefined();
+    private NonNullUpdatedValue<Set<String>> provisioningGroups = NonNullUpdatedValue.undefined();
+
+    private Builder() {
+    }
+
+    public Builder gitlabConfigurationId(String gitlabConfigurationId) {
+      this.gitlabConfigurationId = gitlabConfigurationId;
+      return this;
+    }
+
+    public Builder enabled(NonNullUpdatedValue<Boolean> enabled) {
+      this.enabled = enabled;
+      return this;
+    }
+
+    public Builder applicationId(NonNullUpdatedValue<String> applicationId) {
+      this.applicationId = applicationId;
+      return this;
+    }
+
+    public Builder url(NonNullUpdatedValue<String> url) {
+      this.url = url;
+      return this;
+    }
+
+    public Builder secret(NonNullUpdatedValue<String> secret) {
+      this.secret = secret;
+      return this;
+    }
+
+    public Builder synchronizeGroups(NonNullUpdatedValue<Boolean> synchronizeGroups) {
+      this.synchronizeGroups = synchronizeGroups;
+      return this;
+    }
+
+    public Builder synchronizationType(NonNullUpdatedValue<SynchronizationType> synchronizationType) {
+      this.synchronizationType = synchronizationType;
+      return this;
+    }
+
+    public Builder allowUserToSignUp(NonNullUpdatedValue<Boolean> allowUserToSignUp) {
+      this.allowUserToSignUp = allowUserToSignUp;
+      return this;
+    }
+
+    public Builder provisioningToken(UpdatedValue<String> provisioningToken) {
+      this.provisioningToken = provisioningToken;
+      return this;
+    }
+
+    public Builder provisioningGroups(NonNullUpdatedValue<Set<String>> provisioningGroups) {
+      this.provisioningGroups = provisioningGroups;
+      return this;
+    }
+
+    public UpdateGitlabConfigurationRequest build() {
+      return new UpdateGitlabConfigurationRequest(gitlabConfigurationId, enabled, applicationId, url, secret, synchronizeGroups, synchronizationType, allowUserToSignUp,
+        provisioningToken, provisioningGroups);
+    }
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/gitlab/config/package-info.java
new file mode 100644 (file)
index 0000000..96e7be5
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common.gitlab.config;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index bfb2288591b767448c184cadad27a33143918bc1..268269977109d9d5fa85ed30ccab6dee839c812b 100644 (file)
@@ -36,6 +36,8 @@ public class WebApiEndpoints {
   public static final String CLEAN_CODE_POLICY_DOMAIN = "/clean-code-policy";
   public static final String RULES_ENDPOINT = CLEAN_CODE_POLICY_DOMAIN + "/rules";
 
+  public static final String GITLAB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/gitlab-configurations";
+
   public static final String INTERNAL = "internal";
 
   private WebApiEndpoints() {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java
new file mode 100644 (file)
index 0000000..588457f
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.controller;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.sonar.server.common.gitlab.config.GitlabConfiguration;
+import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
+import org.sonar.server.common.gitlab.config.SynchronizationType;
+import org.sonar.server.common.gitlab.config.UpdateGitlabConfigurationRequest;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.gitlab.config.request.GitlabConfigurationCreateRestRequest;
+import org.sonar.server.v2.api.gitlab.config.request.GitlabConfigurationUpdateRestRequest;
+import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
+import org.sonar.server.v2.api.gitlab.config.response.GitlabConfigurationSearchRestResponse;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+import static org.sonar.server.common.gitlab.config.GitlabConfigurationService.UNIQUE_GITLAB_CONFIGURATION_ID;
+
+public class DefaultGitlabConfigurationController implements GitlabConfigurationController {
+
+  private final UserSession userSession;
+  private final GitlabConfigurationService gitlabConfigurationService;
+
+  public DefaultGitlabConfigurationController(UserSession userSession, GitlabConfigurationService gitlabConfigurationService) {
+    this.userSession = userSession;
+    this.gitlabConfigurationService = gitlabConfigurationService;
+  }
+
+  @Override
+  public GitlabConfigurationResource getGitlabConfiguration(String id) {
+    userSession.checkIsSystemAdministrator();
+    return getGitlabConfigurationResource(id);
+  }
+
+  @Override
+  public GitlabConfigurationSearchRestResponse searchGitlabConfiguration() {
+    userSession.checkIsSystemAdministrator();
+
+    List<GitlabConfigurationResource> gitlabConfigurationResources = gitlabConfigurationService.findConfigurations()
+      .stream()
+      .map(DefaultGitlabConfigurationController::toGitLabConfigurationResource)
+      .toList();
+
+    PageRestResponse pageRestResponse = new PageRestResponse(1, 1000, gitlabConfigurationResources.size());
+    return new GitlabConfigurationSearchRestResponse(gitlabConfigurationResources, pageRestResponse);
+  }
+
+  @Override
+  public GitlabConfigurationResource create(GitlabConfigurationCreateRestRequest createRequest) {
+    userSession.checkIsSystemAdministrator();
+    GitlabConfiguration createdConfiguration = gitlabConfigurationService.createConfiguration(toGitlabConfiguration(createRequest));
+    return toGitLabConfigurationResource(createdConfiguration);
+  }
+
+
+  private static GitlabConfiguration toGitlabConfiguration(GitlabConfigurationCreateRestRequest createRestRequest) {
+    return new GitlabConfiguration(
+      UNIQUE_GITLAB_CONFIGURATION_ID,
+      createRestRequest.enabled(),
+      createRestRequest.applicationId(),
+      createRestRequest.url(),
+      createRestRequest.secret(),
+      createRestRequest.synchronizeGroups(),
+      toSynchronizationType(createRestRequest.synchronizationType()),
+      createRestRequest.allowUsersToSignUp() != null && createRestRequest.allowUsersToSignUp(),
+      createRestRequest.provisioningToken(),
+      createRestRequest.provisioningGroups() == null ? Set.of() : Set.copyOf(createRestRequest.provisioningGroups()));
+  }
+
+  private GitlabConfigurationResource getGitlabConfigurationResource(String id) {
+    return toGitLabConfigurationResource(gitlabConfigurationService.getConfiguration(id));
+  }
+
+  @Override
+  public GitlabConfigurationResource updateGitlabConfiguration(String id, GitlabConfigurationUpdateRestRequest updateRequest) {
+    userSession.checkIsSystemAdministrator();
+    UpdateGitlabConfigurationRequest updateGitlabConfigurationRequest = toUpdateGitlabConfigurationRequest(id, updateRequest);
+    return toGitLabConfigurationResource(gitlabConfigurationService.updateConfiguration(updateGitlabConfigurationRequest));
+  }
+
+  private static UpdateGitlabConfigurationRequest toUpdateGitlabConfigurationRequest(String id,
+    GitlabConfigurationUpdateRestRequest updateRequest) {
+    return UpdateGitlabConfigurationRequest.builder()
+      .gitlabConfigurationId(id)
+      .enabled(updateRequest.getEnabled().toNonNullUpdatedValue())
+      .applicationId(updateRequest.getApplicationId().toNonNullUpdatedValue())
+      .url(updateRequest.getUrl().toNonNullUpdatedValue())
+      .secret(updateRequest.getSecret().toNonNullUpdatedValue())
+      .synchronizeGroups(updateRequest.getSynchronizeGroups().toNonNullUpdatedValue())
+      .synchronizationType(updateRequest.getSynchronizationType().map(DefaultGitlabConfigurationController::toSynchronizationType).toNonNullUpdatedValue())
+      .allowUserToSignUp(updateRequest.getAllowUsersToSignUp().toNonNullUpdatedValue())
+      .provisioningToken(updateRequest.getProvisioningToken().toUpdatedValue())
+      .provisioningGroups(updateRequest.getProvisioningGroups().map(DefaultGitlabConfigurationController::getGroups).toNonNullUpdatedValue())
+      .build();
+  }
+
+  private static Set<String> getGroups(List<String> groups) {
+    return new HashSet<>(groups);
+  }
+
+  private static GitlabConfigurationResource toGitLabConfigurationResource(GitlabConfiguration configuration) {
+    return new GitlabConfigurationResource(
+      configuration.id(),
+      configuration.enabled(),
+      configuration.applicationId(),
+      configuration.url(),
+      configuration.synchronizeGroups(),
+      toRestSynchronizationType(configuration),
+      configuration.allowUsersToSignUp(),
+      sortGroups(configuration.provisioningGroups()));
+  }
+
+  private static SynchronizationType toRestSynchronizationType(GitlabConfiguration configuration) {
+    return SynchronizationType.valueOf(configuration.synchronizationType().name());
+  }
+
+  private static SynchronizationType toSynchronizationType(SynchronizationType synchronizationType) {
+    return SynchronizationType.valueOf(synchronizationType.name());
+  }
+
+  private static List<String> sortGroups(Set<String> groups) {
+    return groups.stream().sorted().toList();
+  }
+
+  @Override
+  public void deleteGitlabConfiguration(String id) {
+    userSession.checkIsSystemAdministrator();
+    gitlabConfigurationService.deleteConfiguration(id);
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/GitlabConfigurationController.java
new file mode 100644 (file)
index 0000000..474da3d
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import javax.validation.Valid;
+import org.sonar.server.v2.api.gitlab.config.request.GitlabConfigurationCreateRestRequest;
+import org.sonar.server.v2.api.gitlab.config.request.GitlabConfigurationUpdateRestRequest;
+import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
+import org.sonar.server.v2.api.gitlab.config.response.GitlabConfigurationSearchRestResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.GITLAB_CONFIGURATION_ENDPOINT;
+import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
+import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+
+@RequestMapping(GITLAB_CONFIGURATION_ENDPOINT)
+@RestController
+public interface GitlabConfigurationController {
+
+  @GetMapping(path = "/{id}")
+  @ResponseStatus(HttpStatus.OK)
+  @Operation(summary = "Fetch a GitLab configuration", description = """
+    Fetch a GitLab configuration. Requires 'Administer System' permission.
+    """,
+    extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+  GitlabConfigurationResource getGitlabConfiguration(
+    @PathVariable("id") @Parameter(description = "The id of the configuration to fetch.", required = true, in = ParameterIn.PATH) String id);
+
+  @GetMapping
+  @Operation(summary = "Search GitLab configs", description = """
+      Get the list of GitLab configurations.
+      Note that a single configuration is supported at this time.
+      Requires 'Administer System' permission.
+    """,
+    extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+  GitlabConfigurationSearchRestResponse searchGitlabConfiguration();
+
+  @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+  @ResponseStatus(HttpStatus.OK)
+  @Operation(summary = "Update a Gitlab configuration", description = """
+    Update a Gitlab configuration. Requires 'Administer System' permission.
+    """,
+    extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+  GitlabConfigurationResource updateGitlabConfiguration(@PathVariable("id") String id, @Valid @RequestBody GitlabConfigurationUpdateRestRequest updateRequest);
+
+  @PostMapping
+  @Operation(summary = "Create Gitlab configuration", description = """
+      Create a new Gitlab configuration.
+      Note that only a single configuration can exist at a time.
+      Requires 'Administer System' permission.
+    """,
+    extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+  GitlabConfigurationResource create(@Valid @RequestBody GitlabConfigurationCreateRestRequest createRequest);
+
+  @DeleteMapping(path = "/{id}")
+  @ResponseStatus(HttpStatus.NO_CONTENT)
+  @Operation(summary = "Delete a GitLab configuration", description = """
+    Delete a GitLab configuration.
+    Requires 'Administer System' permission.
+    """,
+    extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+  void deleteGitlabConfiguration(
+    @PathVariable("id") @Parameter(description = "The id of the configuration to delete.", required = true, in = ParameterIn.PATH) String id);
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/package-info.java
new file mode 100644 (file)
index 0000000..5f2a9b2
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.gitlab.config.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java
new file mode 100644 (file)
index 0000000..dadbe48
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.request;
+
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import org.sonar.server.common.gitlab.config.SynchronizationType;
+
+public record GitlabConfigurationCreateRestRequest(
+
+  @NotNull
+  @Schema(description = "Enable Gitlab authentication")
+  boolean enabled,
+
+  @NotEmpty
+  @Schema(description = "Gitlab Application id")
+  String applicationId,
+  @NotEmpty
+  @Schema(description = "Url of Gitlab instance for authentication (for instance https://gitlab.com)")
+  String url,
+  @NotEmpty
+  @Schema(accessMode = Schema.AccessMode.WRITE_ONLY,  description = "Secret of the application")
+  String secret,
+
+  @NotNull
+  @Schema(description = "Set whether to synchronize groups")
+  Boolean synchronizeGroups,
+
+  @NotNull
+  @Schema(description = "Type of synchronization")
+  SynchronizationType synchronizationType,
+  @Nullable
+  @Schema(accessMode = Schema.AccessMode.WRITE_ONLY,  description = "Gitlab token for provisioning")
+  String provisioningToken,
+
+  @Schema(description = "Allow user to sign up")
+  @Nullable
+  Boolean allowUsersToSignUp,
+
+  @ArraySchema(arraySchema = @Schema(description = "Root GitLab groups to provision."))
+  @Nullable
+  List<String> provisioningGroups
+) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java
new file mode 100644 (file)
index 0000000..5616a86
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.request;
+
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.validation.constraints.Size;
+import org.sonar.server.common.gitlab.config.SynchronizationType;
+import org.sonar.server.v2.common.model.UpdateField;
+
+public class GitlabConfigurationUpdateRestRequest {
+
+  private UpdateField<Boolean> enabled = UpdateField.undefined();
+  private UpdateField<String> applicationId = UpdateField.undefined();
+  private UpdateField<String> url = UpdateField.undefined();
+  private UpdateField<String> secret = UpdateField.undefined();
+  private UpdateField<Boolean> synchronizeGroups = UpdateField.undefined();
+  private UpdateField<SynchronizationType> synchronizationType = UpdateField.undefined();
+  private UpdateField<Boolean> allowUsersToSignUp = UpdateField.undefined();
+  private UpdateField<String> provisioningToken = UpdateField.undefined();
+  private UpdateField<List<String>> provisioningGroups = UpdateField.undefined();
+
+  @Schema(implementation = Boolean.class, description = "Enable Gitlab authentication")
+  public UpdateField<Boolean> getEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(Boolean enabled) {
+    this.enabled = UpdateField.withValue(enabled);
+  }
+
+  @Schema(implementation = String.class, description = "Gitlab Application id")
+  public UpdateField<String> getApplicationId() {
+    return applicationId;
+  }
+
+  public void setApplicationId(String applicationId) {
+    this.applicationId = UpdateField.withValue(applicationId);
+  }
+
+  @Schema(implementation = String.class, description = "Url of Gitlab instance for authentication (for instance https://gitlab.com/api/v4)")
+  public UpdateField<String> getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = UpdateField.withValue(url);
+  }
+
+  @Schema(implementation = String.class, description = "Secret of the application", nullable = true)
+  public UpdateField<String> getSecret() {
+    return secret;
+  }
+
+  public void setSecret(String secret) {
+    this.secret = UpdateField.withValue(secret);
+  }
+
+  @Schema(implementation = Boolean.class, description = "Set whether to synchronize groups")
+  public UpdateField<Boolean> getSynchronizeGroups() {
+    return synchronizeGroups;
+  }
+
+  public void setSynchronizeGroups(Boolean synchronizeGroups) {
+    this.synchronizeGroups = UpdateField.withValue(synchronizeGroups);
+  }
+
+  @Schema(implementation = SynchronizationType.class, description = "Type of synchronization")
+  public UpdateField<SynchronizationType> getSynchronizationType() {
+    return synchronizationType;
+  }
+
+  public void setSynchronizationType(SynchronizationType synchronizationType) {
+    this.synchronizationType = UpdateField.withValue(synchronizationType);
+  }
+
+  @Schema(implementation = Boolean.class, description = "Allow user to sign up")
+  public UpdateField<Boolean> getAllowUsersToSignUp() {
+    return allowUsersToSignUp;
+  }
+
+  public void setAllowUsersToSignUp(Boolean allowUsersToSignUp) {
+    this.allowUsersToSignUp = UpdateField.withValue(allowUsersToSignUp);
+  }
+
+  @Size(min = 1)
+  @Schema(implementation = String.class, description = "Gitlab token for provisioning", nullable = true)
+  public UpdateField<String> getProvisioningToken() {
+    return provisioningToken;
+  }
+
+  public void setProvisioningToken(String provisioningToken) {
+    this.provisioningToken = UpdateField.withValue(provisioningToken);
+  }
+
+  @ArraySchema(arraySchema = @Schema(description = "Root gitlab groups to provision."), schema = @Schema(implementation = String.class))
+  public UpdateField<List<String>> getProvisioningGroups() {
+    return provisioningGroups;
+  }
+
+  public void setProvisioningGroups(List<String> provisioningGroups) {
+    this.provisioningGroups = UpdateField.withValue(provisioningGroups);
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/package-info.java
new file mode 100644 (file)
index 0000000..9557f9a
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.gitlab.config.request;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java
new file mode 100644 (file)
index 0000000..eca624c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.resource;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import org.sonar.server.common.gitlab.config.SynchronizationType;
+
+public record GitlabConfigurationResource(
+
+  @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+  String id,
+
+  boolean enabled,
+
+  @Schema(implementation = String.class, description = "Gitlab Application id")
+  String applicationId,
+
+  @Schema(description = "Url of Gitlab instance for authentication (for instance https://gitlab.com/api/v4)")
+  String url,
+
+  boolean synchronizeGroups,
+
+  SynchronizationType synchronizationType,
+
+  boolean allowUsersToSignUp,
+
+  @Schema(description = "Root Gitlab groups to provision")
+  List<String> provisioningGroups
+) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/package-info.java
new file mode 100644 (file)
index 0000000..47e2902
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.gitlab.config.resource;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/GitlabConfigurationSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/GitlabConfigurationSearchRestResponse.java
new file mode 100644 (file)
index 0000000..ddadc8c
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config.response;
+
+import java.util.List;
+import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+public record GitlabConfigurationSearchRestResponse(List<GitlabConfigurationResource> gitlabConfigurations, PageRestResponse page) {}
+
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/response/package-info.java
new file mode 100644 (file)
index 0000000..1d3e083
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.gitlab.config.response;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 3df7ae8ebe1b13986fa81081f0235d22d7bf030e..c1b63631b08899f9c72819ff7467ef2ba2275365 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.v2.config;
 import javax.annotation.Nullable;
 import org.sonar.api.resources.Languages;
 import org.sonar.db.DbClient;
+import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
 import org.sonar.server.common.group.service.GroupMembershipService;
 import org.sonar.server.common.group.service.GroupService;
 import org.sonar.server.common.health.CeStatusNodeCheck;
@@ -35,10 +36,13 @@ import org.sonar.server.common.rule.service.RuleService;
 import org.sonar.server.common.text.MacroInterpreter;
 import org.sonar.server.common.user.service.UserService;
 import org.sonar.server.health.HealthChecker;
+import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.platform.NodeInformation;
 import org.sonar.server.rule.RuleDescriptionFormatter;
 import org.sonar.server.user.SystemPasscode;
 import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController;
+import org.sonar.server.v2.api.gitlab.config.controller.GitlabConfigurationController;
 import org.sonar.server.v2.api.group.controller.DefaultGroupController;
 import org.sonar.server.v2.api.group.controller.GroupController;
 import org.sonar.server.v2.api.membership.controller.DefaultGroupMembershipController;
@@ -121,4 +125,14 @@ public class PlatformLevel4WebConfig {
     return handlerMapping;
   }
 
+  @Bean
+  public GitlabConfigurationService gitlabConfigurationService(ManagedInstanceService managedInstanceService, DbClient dbClient) {
+    return new GitlabConfigurationService( managedInstanceService, dbClient);
+  }
+
+  @Bean
+  public GitlabConfigurationController gitlabConfigurationController(UserSession userSession, GitlabConfigurationService gitlabConfigurationService) {
+    return new DefaultGitlabConfigurationController(userSession, gitlabConfigurationService);
+  }
+
 }
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java
new file mode 100644 (file)
index 0000000..bcaea1b
--- /dev/null
@@ -0,0 +1,432 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.v2.api.gitlab.config;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.common.NonNullUpdatedValue;
+import org.sonar.server.common.UpdatedValue;
+import org.sonar.server.common.gitlab.config.GitlabConfiguration;
+import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
+import org.sonar.server.common.gitlab.config.SynchronizationType;
+import org.sonar.server.common.gitlab.config.UpdateGitlabConfigurationRequest;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController;
+import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
+import org.sonar.server.v2.api.gitlab.config.response.GitlabConfigurationSearchRestResponse;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.common.gitlab.config.SynchronizationType.AUTO_PROVISIONING;
+import static org.sonar.server.common.gitlab.config.SynchronizationType.JIT;
+import static org.sonar.server.v2.WebApiEndpoints.GITLAB_CONFIGURATION_ENDPOINT;
+import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultGitlabConfigurationControllerTest {
+  private static final Gson GSON = new GsonBuilder().create();
+
+  private static final GitlabConfiguration GITLAB_CONFIGURATION = new GitlabConfiguration(
+    "existing-id",
+    true,
+    "application-id",
+    "www.url.com",
+    "secret",
+    true,
+    AUTO_PROVISIONING,
+    true,
+    "provisioning-token",
+    Set.of("provisioning-group2", "provisioning-group1"));
+  private static final GitlabConfigurationResource EXPECTED_GITLAB_CONF_RESOURCE = new GitlabConfigurationResource(
+    GITLAB_CONFIGURATION.id(),
+    GITLAB_CONFIGURATION.enabled(),
+    GITLAB_CONFIGURATION.applicationId(),
+    GITLAB_CONFIGURATION.url(),
+    GITLAB_CONFIGURATION.synchronizeGroups(),
+    SynchronizationType.valueOf(GITLAB_CONFIGURATION.synchronizationType().name()),
+    GITLAB_CONFIGURATION.allowUsersToSignUp(),
+    List.of("provisioning-group1", "provisioning-group2"));
+  private static final String EXPECTED_CONFIGURATION = """
+    {
+      "id": "existing-id",
+      "enabled": true,
+      "applicationId": "application-id",
+      "url": "www.url.com",
+      "synchronizeGroups": true,
+      "synchronizationType": "AUTO_PROVISIONING",
+      "allowUsersToSignUp": true,
+      "provisioningGroups": [
+        "provisioning-group2",
+        "provisioning-group1"
+      ]
+    }
+    """;
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  private final GitlabConfigurationService gitlabConfigurationService = mock();
+  private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultGitlabConfigurationController(userSession, gitlabConfigurationService));
+
+  @Test
+  public void fetchConfiguration_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    userSession.logIn().setNonSystemAdministrator();
+
+    mockMvc.perform(get(GITLAB_CONFIGURATION_ENDPOINT + "/1"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void fetchConfiguration_whenConfigNotFound_throws() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.getConfiguration("not-existing")).thenThrow(new NotFoundException("bla"));
+
+    mockMvc.perform(get(GITLAB_CONFIGURATION_ENDPOINT + "/not-existing"))
+      .andExpectAll(
+        status().isNotFound(),
+        content().json("{\"message\":\"bla\"}"));
+  }
+
+  @Test
+  public void fetchConfiguration_whenConfigFound_returnsIt() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.getConfiguration("existing-id")).thenReturn(GITLAB_CONFIGURATION);
+
+    mockMvc.perform(get(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id"))
+      .andExpectAll(
+        status().isOk(),
+        content().json(EXPECTED_CONFIGURATION));
+  }
+
+  @Test
+  public void search_whenNoParameters_shouldUseDefaultAndForwardToGroupMembershipService() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.findConfigurations()).thenReturn(Optional.of(GITLAB_CONFIGURATION));
+
+    MvcResult mvcResult = mockMvc.perform(get(GITLAB_CONFIGURATION_ENDPOINT))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    GitlabConfigurationSearchRestResponse gitlabConfigurationResource = GSON.fromJson(mvcResult.getResponse().getContentAsString(), GitlabConfigurationSearchRestResponse.class);
+
+    assertThat(gitlabConfigurationResource.page().pageSize()).isEqualTo(1000);
+    assertThat(gitlabConfigurationResource.page().pageIndex()).isEqualTo(1);
+    assertThat(gitlabConfigurationResource.page().total()).isEqualTo(1);
+    assertThat(gitlabConfigurationResource.gitlabConfigurations()).containsExactly(EXPECTED_GITLAB_CONF_RESOURCE);
+  }
+
+  @Test
+  public void search_whenNoParametersAndNoConfig_shouldReturnEmptyList() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.findConfigurations()).thenReturn(Optional.empty());
+
+    MvcResult mvcResult = mockMvc.perform(get(GITLAB_CONFIGURATION_ENDPOINT))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    GitlabConfigurationSearchRestResponse gitlabConfigurationResource = GSON.fromJson(mvcResult.getResponse().getContentAsString(), GitlabConfigurationSearchRestResponse.class);
+
+    assertThat(gitlabConfigurationResource.page().pageSize()).isEqualTo(1000);
+    assertThat(gitlabConfigurationResource.page().pageIndex()).isEqualTo(1);
+    assertThat(gitlabConfigurationResource.page().total()).isZero();
+    assertThat(gitlabConfigurationResource.gitlabConfigurations()).isEmpty();
+  }
+
+  @Test
+  public void updateConfiguration_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    userSession.logIn().setNonSystemAdministrator();
+
+    mockMvc.perform(patch(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id")
+      .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+      .content("{}"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void updateConfiguration_whenAllFieldsUpdated_performUpdates() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.updateConfiguration(any())).thenReturn(GITLAB_CONFIGURATION);
+
+    String payload = """
+      {
+            "enabled": true,
+            "applicationId": "application-id",
+            "url": "www.url.com",
+            "secret": "newSecret",
+            "synchronizeGroups": true,
+            "synchronizationType": "AUTO_PROVISIONING",
+            "allowUsersToSignUp": true,
+            "provisioningToken": "token",
+            "provisioningGroups": [
+              "provisioning-group2",
+              "provisioning-group1"
+            ]
+      }
+      """;
+
+    mockMvc.perform(patch(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id")
+      .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+      .content(payload))
+      .andExpectAll(
+        status().isOk(),
+        content().json(EXPECTED_CONFIGURATION));
+
+    verify(gitlabConfigurationService).updateConfiguration(new UpdateGitlabConfigurationRequest(
+      "existing-id",
+      NonNullUpdatedValue.withValueOrThrow(true),
+      NonNullUpdatedValue.withValueOrThrow("application-id"),
+      NonNullUpdatedValue.withValueOrThrow("www.url.com"),
+      NonNullUpdatedValue.withValueOrThrow("newSecret"),
+      NonNullUpdatedValue.withValueOrThrow(true),
+      NonNullUpdatedValue.withValueOrThrow(AUTO_PROVISIONING),
+      NonNullUpdatedValue.withValueOrThrow(true),
+      UpdatedValue.withValue("token"),
+      NonNullUpdatedValue.withValueOrThrow(Set.of("provisioning-group2", "provisioning-group1"))));
+  }
+
+  @Test
+  public void updateConfiguration_whenSomeFieldsUpdated_performUpdates() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.updateConfiguration(any())).thenReturn(GITLAB_CONFIGURATION);
+
+    String payload = """
+      {
+            "enabled": false,
+            "synchronizationType": "JIT",
+            "allowUsersToSignUp": false,
+            "provisioningToken": null
+      }
+      """;
+
+    mockMvc.perform(patch(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id")
+      .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+      .content(payload))
+      .andExpectAll(
+        status().isOk(),
+        content().json(EXPECTED_CONFIGURATION));
+
+    verify(gitlabConfigurationService).updateConfiguration(new UpdateGitlabConfigurationRequest(
+      "existing-id",
+      NonNullUpdatedValue.withValueOrThrow(false),
+      NonNullUpdatedValue.undefined(),
+      NonNullUpdatedValue.undefined(),
+      NonNullUpdatedValue.undefined(),
+      NonNullUpdatedValue.undefined(),
+      NonNullUpdatedValue.withValueOrThrow(JIT),
+      NonNullUpdatedValue.withValueOrThrow(false),
+      UpdatedValue.withValue(null),
+      NonNullUpdatedValue.undefined()));
+  }
+
+  @Test
+  public void create_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    userSession.logIn().setNonSystemAdministrator();
+
+    mockMvc.perform(
+      post(GITLAB_CONFIGURATION_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON_VALUE)
+        .content("""
+            {
+               "enabled": true,
+               "applicationId": "application-id",
+               "url": "www.url.com",
+               "secret": "123",
+               "synchronizeGroups": true,
+               "synchronizationType": "AUTO_PROVISIONING",
+               "allowUsersToSignUp": true,
+               "provisioningGroups": [
+                 "provisioning-group2",
+                 "provisioning-group1"
+               ]
+             }
+          """))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void create_whenConfigCreated_returnsIt() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.createConfiguration(any())).thenReturn(GITLAB_CONFIGURATION);
+
+    mockMvc.perform(
+      post(GITLAB_CONFIGURATION_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON_VALUE)
+        .content("""
+            {
+              "enabled": true,
+              "applicationId": "application-id",
+              "secret": "123",
+              "url": "www.url.com",
+              "synchronizeGroups": true,
+              "synchronizationType": "AUTO_PROVISIONING",
+              "allowUsersToSignUp": true,
+              "provisioningGroups": [
+                "provisioning-group2",
+                "provisioning-group1"
+              ]
+            }
+
+          """))
+      .andExpectAll(
+        status().isOk(),
+        content().json("""
+          {
+            "id": "existing-id",
+            "enabled": true,
+            "applicationId": "application-id",
+            "url": "www.url.com",
+            "synchronizeGroups": true,
+            "synchronizationType": "AUTO_PROVISIONING",
+            "allowUsersToSignUp": true,
+            "provisioningGroups": [
+              "provisioning-group2",
+              "provisioning-group1"
+            ]
+          }
+          """));
+
+  }
+  @Test
+  public void create_whenConfigCreatedWithoutOptionalParams_returnsIt() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(gitlabConfigurationService.createConfiguration(any())).thenReturn(GITLAB_CONFIGURATION);
+
+    mockMvc.perform(
+      post(GITLAB_CONFIGURATION_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON_VALUE)
+        .content("""
+            {
+              "enabled": true,
+              "applicationId": "application-id",
+              "secret": "123",
+              "url": "www.url.com",
+              "synchronizeGroups": true,
+              "synchronizationType": "AUTO_PROVISIONING"
+            }
+
+          """))
+      .andExpectAll(
+        status().isOk(),
+        content().json("""
+          {
+            "id": "existing-id",
+            "enabled": true,
+            "applicationId": "application-id",
+            "url": "www.url.com",
+            "synchronizeGroups": true,
+            "synchronizationType": "AUTO_PROVISIONING",
+            "allowUsersToSignUp": true,
+            "provisioningGroups": [
+              "provisioning-group2",
+              "provisioning-group1"
+            ]
+          }
+          """));
+
+  }
+
+  @Test
+  public void create_whenRequiredParameterIsMissing_shouldReturnBadRequest() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(
+      post(GITLAB_CONFIGURATION_ENDPOINT)
+        .contentType(MediaType.APPLICATION_JSON_VALUE)
+        .content("""
+          {
+            "enabled": true,
+            "applicationId": "application-id",
+            "url": "www.url.com",
+            "synchronizeGroups": true,
+            "synchronizationType": "AUTO_PROVISIONING",
+            "allowUsersToSignUp": true,
+            "provisioningGroups": [
+              "provisioning-group2",
+              "provisioning-group1"
+            ]
+          }
+          """))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json(
+          "{\"message\":\"Value {} for field secret was rejected. Error: must not be empty.\"}"));
+
+  }
+
+  @Test
+  public void delete_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    userSession.logIn().setNonSystemAdministrator();
+
+    mockMvc.perform(
+      delete(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void delete_whenConfigIsDeleted_returnsNoContent() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(
+      delete(GITLAB_CONFIGURATION_ENDPOINT + "/existing-id"))
+      .andExpectAll(
+        status().isNoContent());
+
+    verify(gitlabConfigurationService).deleteConfiguration("existing-id");
+  }
+
+  @Test
+  public void delete_whenConfigNotFound_returnsNotFound() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    doThrow(new NotFoundException("Not found")).when(gitlabConfigurationService).deleteConfiguration("not-existing");
+
+    mockMvc.perform(
+      delete(GITLAB_CONFIGURATION_ENDPOINT + "/not-existing"))
+      .andExpectAll(
+        status().isNotFound(),
+        content().json("{\"message\":\"Not found\"}"));
+  }
+
+}