--- /dev/null
+package org.sonar.server.platform.db.migration.version.v102;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.SQLException;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.Upsert;
+
+public class AddUserConsentRequiredIfGithubAutoProvisioningEnabled extends DataChange {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AddUserConsentRequiredIfGithubAutoProvisioningEnabled.class);
+ @VisibleForTesting
+ static final String PROVISIONING_GITHUB_ENABLED_PROP_KEY = "provisioning.github.enabled";
+
+ @VisibleForTesting
+ static final String PROP_KEY = "sonar.auth.github.userConsentForPermissionProvisioningRequired";
+
+ private static final String INSERT_QUERY = """
+ INSERT INTO PROPERTIES (UUID, PROP_KEY, IS_EMPTY, CREATED_AT)
+ VALUES (?, ?, ?, ?)
+ """;
+
+ private final System2 system2;
+ private final UuidFactory uuidFactory;
+
+ public AddUserConsentRequiredIfGithubAutoProvisioningEnabled(Database db, System2 system2, UuidFactory uuidFactory) {
+ super(db);
+ this.system2 = system2;
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ protected void execute(DataChange.Context context) throws SQLException {
+ if (!isGithubAutoProvisioningEnabled(context)) {
+ return;
+ }
+ if (isUserConsentAlreadyRequired(context)) {
+ return;
+ }
+ LOG.warn("Automatic synchronization was previously activated for GitHub. It requires user consent to continue working as new " +
+ " features were added with the synchronization. Please read the upgrade notes.");
+ Upsert upsert = context.prepareUpsert(INSERT_QUERY);
+ upsert
+ .setString(1, uuidFactory.create())
+ .setString(2, PROP_KEY)
+ .setBoolean(3, true)
+ .setLong(4, system2.now())
+ .execute()
+ .commit();
+ }
+
+ private static boolean isUserConsentAlreadyRequired(Context context) throws SQLException {
+ return Optional.ofNullable(context.prepareSelect("select count(*) from properties where prop_key = ?")
+ .setString(1, PROP_KEY)
+ .get(t -> 1 == t.getInt(1)))
+ .orElseThrow();
+ }
+
+ private static boolean isGithubAutoProvisioningEnabled(Context context) throws SQLException {
+ return Optional.ofNullable(context.prepareSelect("select count(*) from internal_properties where kee = ? and text_value = ?")
+ .setString(1, PROVISIONING_GITHUB_ENABLED_PROP_KEY)
+ .setString(2, "true")
+ .get(t -> 1 == t.getInt(1)))
+ .orElseThrow();
+ }
+
+}
.add(10_2_052, "Create index 'wd_task_uuid_created_at' in 'webhook_deliveries'", CreateIndexTaskUuidCreatedAtInWebhookDeliveries.class)
.add(10_2_053, "Create index 'wd_created_at' in 'webhook_deliveries'", CreateIndexCreatedAtInWebhookDeliveries.class)
+ .add(10_2_054, "Insert property github.userConsentementForPermissionProvisioningRequired", AddUserConsentRequiredIfGithubAutoProvisioningEnabled.class)
;
}
}
--- /dev/null
+/*
+ * 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.platform.db.migration.version.v102;
+
+import java.sql.SQLException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTester;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.platform.db.migration.version.v102.AddUserConsentRequiredIfGithubAutoProvisioningEnabled.PROP_KEY;
+import static org.sonar.server.platform.db.migration.version.v102.AddUserConsentRequiredIfGithubAutoProvisioningEnabled.PROVISIONING_GITHUB_ENABLED_PROP_KEY;
+
+public class AddUserConsentRequiredIfGithubAutoProvisioningEnabledTest {
+
+ @Rule
+ public LogTester logger = new LogTester();
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(AddUserConsentRequiredIfGithubAutoProvisioningEnabledTest.class, "schema.sql");
+
+ private final DataChange underTest = new AddUserConsentRequiredIfGithubAutoProvisioningEnabled(db.database(), new System2(), UuidFactoryFast.getInstance());
+
+ @Before
+ public void before() {
+ logger.clear();
+ }
+
+ @Test
+ public void migration_whenGitHubAutoProvisioningPropertyNotPresent_shouldNotRequireConsent() throws SQLException {
+ underTest.execute();
+
+ assertThat(logger.logs(Level.WARN)).isEmpty();
+ assertThat(isConsentRequired()).isFalse();
+ }
+
+ @Test
+ public void migration_whenGitHubAutoProvisioningDisabled_shouldNotRequireConsent() throws SQLException {
+ disableGithubProvisioning();
+ underTest.execute();
+
+ assertThat(logger.logs(Level.WARN)).isEmpty();
+ assertThat(isConsentRequired()).isFalse();
+ }
+
+ @Test
+ public void migration_whenGitHubAutoProvisioningEnabled_shouldRequireConsent() throws SQLException {
+ enableGithubProvisioning();
+
+ underTest.execute();
+
+ assertThat(logger.logs(Level.WARN)).containsExactly("Automatic synchronization was previously activated for GitHub. It requires user consent to continue working as new"
+ + " features were added with the synchronization. Please read the upgrade notes.");
+ assertThat(isConsentRequired()).isTrue();
+ }
+
+ @Test
+ public void migration_is_reentrant() throws SQLException {
+ enableGithubProvisioning();
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThat(logger.logs(Level.WARN)).containsExactly("Automatic synchronization was previously activated for GitHub. It requires user consent to continue working as new"
+ + " features were added with the synchronization. Please read the upgrade notes.");
+ assertThat(isConsentRequired()).isTrue();
+ }
+
+ private void disableGithubProvisioning() {
+ toggleGithubProvisioning(false);
+ }
+ private void enableGithubProvisioning() {
+ toggleGithubProvisioning(true);
+ }
+
+ private boolean isConsentRequired() {
+ return db.countSql("select count(*) from properties where prop_key = '" + PROP_KEY + "'") >= 1;
+ }
+
+ private void toggleGithubProvisioning(boolean enabled) {
+ db.executeInsert("internal_properties", "kee", PROVISIONING_GITHUB_ENABLED_PROP_KEY, "text_value", String.valueOf(enabled), "is_empty", true, "created_at", 0);
+ }
+}
--- /dev/null
+CREATE TABLE "INTERNAL_PROPERTIES"(
+ "KEE" CHARACTER VARYING(40) NOT NULL,
+ "IS_EMPTY" BOOLEAN NOT NULL,
+ "TEXT_VALUE" CHARACTER VARYING(4000),
+ "CLOB_VALUE" CHARACTER LARGE OBJECT,
+ "CREATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "INTERNAL_PROPERTIES" ADD CONSTRAINT "PK_INTERNAL_PROPERTIES" PRIMARY KEY("KEE");
+
+CREATE TABLE "PROPERTIES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "PROP_KEY" CHARACTER VARYING(512) NOT NULL,
+ "IS_EMPTY" BOOLEAN NOT NULL,
+ "TEXT_VALUE" CHARACTER VARYING(4000),
+ "CLOB_VALUE" CHARACTER LARGE OBJECT,
+ "CREATED_AT" BIGINT NOT NULL,
+ "ENTITY_UUID" CHARACTER VARYING(40),
+ "USER_UUID" CHARACTER VARYING(255)
+);
+ALTER TABLE "PROPERTIES" ADD CONSTRAINT "PK_PROPERTIES" PRIMARY KEY("UUID");
+CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES"("PROP_KEY" NULLS FIRST);