From 9b8ced5b19fc4bffa942c1159305e2d1e76ee729 Mon Sep 17 00:00:00 2001 From: Kodai Doki <52027276+KodaiD@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:47:22 +0900 Subject: [PATCH 1/2] Add Cassandra permission test (#2822) --- .github/workflows/permission-check.yaml | 88 ++++ core/build.gradle | 31 ++ ...ssandraAdminPermissionIntegrationTest.java | 141 ++++++ .../db/storage/cassandra/CassandraEnv.java | 17 + .../CassandraPermissionIntegrationTest.java | 61 +++ .../CassandraPermissionTestUtils.java | 55 +++ ...ageAdminPermissionIntegrationTestBase.java | 429 ++++++++++++++++++ ...dStoragePermissionIntegrationTestBase.java | 388 ++++++++++++++++ .../scalar/db/util/PermissionTestUtils.java | 12 + 9 files changed, 1222 insertions(+) create mode 100644 .github/workflows/permission-check.yaml create mode 100644 core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java create mode 100644 core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java create mode 100644 core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java create mode 100644 integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java create mode 100644 integration-test/src/main/java/com/scalar/db/api/DistributedStoragePermissionIntegrationTestBase.java create mode 100644 integration-test/src/main/java/com/scalar/db/util/PermissionTestUtils.java diff --git a/.github/workflows/permission-check.yaml b/.github/workflows/permission-check.yaml new file mode 100644 index 0000000000..5740a084d4 --- /dev/null +++ b/.github/workflows/permission-check.yaml @@ -0,0 +1,88 @@ +name: Test Permissions + +on: + workflow_dispatch: + +env: + TERM: dumb + JAVA_VERSION: '8' + JAVA_VENDOR: 'temurin' + +jobs: + integration-test-permission-cassandra-3-0: + name: Cassandra 3.0 Permission Integration Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} (${{ env.JAVA_VENDOR }}) + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_VENDOR }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Start Cassandra with authentication enabled + run: | + docker run -d --name cassandra \ + -p 9042:9042 \ + -e CASSANDRA_PASSWORD_SEEDER=yes \ + -e CASSANDRA_PASSWORD=cassandra \ + -e CASSANDRA_AUTHENTICATOR=PasswordAuthenticator \ + -e CASSANDRA_AUTHORIZER=CassandraAuthorizer \ + bitnami/cassandra:3.0 + + - name: Wait for Cassandra to be ready + run: sleep 30 + + - name: Execute Gradle 'integrationTestCassandraPermission' task + run: ./gradlew integrationTestCassandraPermission + + - name: Upload Gradle test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: cassandra_3.0_permission_integration_test_reports + path: core/build/reports/tests/integrationTestCassandraPermission + + integration-test-permission-cassandra-3-11: + name: Cassandra 3.11 Permission Integration Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} (${{ env.JAVA_VENDOR }}) + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_VENDOR }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Start Cassandra with authentication enabled + run: | + docker run -d --name cassandra \ + -p 9042:9042 \ + -e CASSANDRA_PASSWORD_SEEDER=yes \ + -e CASSANDRA_PASSWORD=cassandra \ + -e CASSANDRA_AUTHENTICATOR=PasswordAuthenticator \ + -e CASSANDRA_AUTHORIZER=CassandraAuthorizer \ + bitnami/cassandra:3.11 + + - name: Wait for Cassandra to be ready + run: sleep 30 + + - name: Execute Gradle 'integrationTestCassandraPermission' task + run: ./gradlew integrationTestCassandraPermission + + - name: Upload Gradle test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: cassandra_3.11_permission_integration_test_reports + path: core/build/reports/tests/integrationTestCassandraPermission diff --git a/core/build.gradle b/core/build.gradle index 921d1dfaa8..5017ebaa16 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -22,6 +22,9 @@ sourceSets { srcDir file('src/integration-test/java') include '**/com/scalar/db/common/*.java' include '**/com/scalar/db/storage/cassandra/*.java' + exclude '**/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java' + exclude '**/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java' + exclude '**/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java' } resources.srcDir file('src/integration-test/resources') } @@ -67,6 +70,20 @@ sourceSets { } resources.srcDir file('src/integration-test/resources') } + integrationTestCassandraPermission { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/java') + include '**/com/scalar/db/common/*.java' + include '**/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java' + include '**/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java' + include '**/com/scalar/db/storage/cassandra/CassandraEnv.java' + include '**/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java' + include '**/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java' + } + resources.srcDir file('src/integration-test/resources') + } } configurations { @@ -88,6 +105,9 @@ configurations { integrationTestMultiStorageImplementation.extendsFrom testImplementation integrationTestMultiStorageRuntimeOnly.extendsFrom testRuntimeOnly integrationTestMultiStorageCompileOnly.extendsFrom testCompileOnly + integrationTestCassandraPermissionImplementation.extendsFrom testImplementation + integrationTestCassandraPermissionRuntimeOnly.extendsFrom testRuntimeOnly + integrationTestCassandraPermissionCompileOnly.extendsFrom testCompileOnly } dependencies { @@ -200,6 +220,17 @@ task integrationTestMultiStorage(type: Test) { } } +task integrationTestCassandraPermission(type: Test) { + description = 'Runs the integration tests for Cassandra permissions.' + group = 'verification' + testClassesDirs = sourceSets.integrationTestCassandraPermission.output.classesDirs + classpath = sourceSets.integrationTestCassandraPermission.runtimeClasspath + outputs.upToDateWhen { false } // ensures integration tests are run every time when called + options { + systemProperties(System.getProperties().findAll { it.key.toString().startsWith("scalardb") }) + } +} + spotless { java { target 'src/*/java/**/*.java' diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java new file mode 100644 index 0000000000..e54ad7052a --- /dev/null +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminPermissionIntegrationTest.java @@ -0,0 +1,141 @@ +package com.scalar.db.storage.cassandra; + +import static com.scalar.db.storage.cassandra.CassandraPermissionTestUtils.MAX_RETRY_COUNT; +import static com.scalar.db.storage.cassandra.CassandraPermissionTestUtils.SLEEP_BETWEEN_RETRIES_SECONDS; + +import com.google.common.util.concurrent.Uninterruptibles; +import com.scalar.db.api.DistributedStorageAdminPermissionIntegrationTestBase; +import com.scalar.db.util.AdminTestUtils; +import com.scalar.db.util.PermissionTestUtils; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class CassandraAdminPermissionIntegrationTest + extends DistributedStorageAdminPermissionIntegrationTestBase { + @Override + protected Properties getProperties(String testName) { + return CassandraEnv.getProperties(testName); + } + + @Override + protected Properties getPropertiesForNormalUser(String testName) { + return CassandraEnv.getPropertiesForNormalUser(testName); + } + + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CassandraAdminTestUtils(getProperties(testName)); + } + + @Override + protected PermissionTestUtils getPermissionTestUtils(String testName) { + return new CassandraPermissionTestUtils(getProperties(testName)); + } + + @Override + protected Map getCreationOptions() { + return Collections.singletonMap(CassandraAdmin.REPLICATION_FACTOR, "1"); + } + + @Override + protected void waitForNamespaceCreation() { + try { + AdminTestUtils utils = getAdminTestUtils(TEST_NAME); + int retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + if (utils.namespaceExists(NAMESPACE)) { + utils.close(); + return; + } + Uninterruptibles.sleepUninterruptibly( + SLEEP_BETWEEN_RETRIES_SECONDS, java.util.concurrent.TimeUnit.SECONDS); + retryCount++; + } + utils.close(); + throw new RuntimeException("Namespace was not created after " + MAX_RETRY_COUNT + " retries"); + } catch (Exception e) { + throw new RuntimeException("Failed to wait for namespace creation", e); + } + } + + @Override + protected void waitForTableCreation() { + try { + AdminTestUtils utils = getAdminTestUtils(TEST_NAME); + int retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + if (utils.tableExists(NAMESPACE, TABLE)) { + utils.close(); + return; + } + Uninterruptibles.sleepUninterruptibly( + SLEEP_BETWEEN_RETRIES_SECONDS, java.util.concurrent.TimeUnit.SECONDS); + retryCount++; + } + utils.close(); + throw new RuntimeException("Table was not created after " + MAX_RETRY_COUNT + " retries"); + } catch (Exception e) { + throw new RuntimeException("Failed to wait for table creation", e); + } + } + + @Override + protected void waitForNamespaceDeletion() { + try { + AdminTestUtils utils = getAdminTestUtils(TEST_NAME); + int retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + if (!utils.namespaceExists(NAMESPACE)) { + utils.close(); + return; + } + Uninterruptibles.sleepUninterruptibly( + SLEEP_BETWEEN_RETRIES_SECONDS, java.util.concurrent.TimeUnit.SECONDS); + retryCount++; + } + utils.close(); + throw new RuntimeException("Namespace was not deleted after " + MAX_RETRY_COUNT + " retries"); + } catch (Exception e) { + throw new RuntimeException("Failed to wait for namespace deletion", e); + } + } + + @Override + protected void waitForTableDeletion() { + try { + AdminTestUtils utils = getAdminTestUtils(TEST_NAME); + int retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + if (!utils.tableExists(NAMESPACE, TABLE)) { + utils.close(); + return; + } + Uninterruptibles.sleepUninterruptibly( + SLEEP_BETWEEN_RETRIES_SECONDS, java.util.concurrent.TimeUnit.SECONDS); + retryCount++; + } + utils.close(); + throw new RuntimeException("Table was not deleted after " + MAX_RETRY_COUNT + " retries"); + } catch (Exception e) { + throw new RuntimeException("Failed to wait for table deletion", e); + } + } + + @Test + @Override + @Disabled("Import-related functionality is not supported in Cassandra") + public void getImportTableMetadata_WithSufficientPermission_ShouldSucceed() {} + + @Test + @Override + @Disabled("Import-related functionality is not supported in Cassandra") + public void addRawColumnToTable_WithSufficientPermission_ShouldSucceed() {} + + @Test + @Override + @Disabled("Import-related functionality is not supported in Cassandra") + public void importTable_WithSufficientPermission_ShouldSucceed() {} +} diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraEnv.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraEnv.java index 75237781b1..2f55b91ec2 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraEnv.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraEnv.java @@ -7,10 +7,14 @@ public final class CassandraEnv { private static final String PROP_CASSANDRA_CONTACT_POINTS = "scalardb.cassandra.contact_points"; private static final String PROP_CASSANDRA_USERNAME = "scalardb.cassandra.username"; private static final String PROP_CASSANDRA_PASSWORD = "scalardb.cassandra.password"; + private static final String PROP_CASSANDRA_NORMAL_USERNAME = "scalardb.cassandra.normal_username"; + private static final String PROP_CASSANDRA_NORMAL_PASSWORD = "scalardb.cassandra.normal_password"; private static final String DEFAULT_CASSANDRA_CONTACT_POINTS = "localhost"; private static final String DEFAULT_CASSANDRA_USERNAME = "cassandra"; private static final String DEFAULT_CASSANDRA_PASSWORD = "cassandra"; + private static final String DEFAULT_CASSANDRA_NORMAL_USERNAME = "test"; + private static final String DEFAULT_CASSANDRA_NORMAL_PASSWORD = "test"; private CassandraEnv() {} @@ -29,4 +33,17 @@ public static Properties getProperties(@SuppressWarnings("unused") String testNa props.setProperty(DatabaseConfig.CROSS_PARTITION_SCAN_ORDERING, "false"); return props; } + + public static Properties getPropertiesForNormalUser(String testName) { + Properties properties = getProperties(testName); + + String username = + System.getProperty(PROP_CASSANDRA_NORMAL_USERNAME, DEFAULT_CASSANDRA_NORMAL_USERNAME); + String password = + System.getProperty(PROP_CASSANDRA_NORMAL_PASSWORD, DEFAULT_CASSANDRA_NORMAL_PASSWORD); + properties.setProperty(DatabaseConfig.USERNAME, username); + properties.setProperty(DatabaseConfig.PASSWORD, password); + + return properties; + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java new file mode 100644 index 0000000000..7c599ed34a --- /dev/null +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionIntegrationTest.java @@ -0,0 +1,61 @@ +package com.scalar.db.storage.cassandra; + +import static com.scalar.db.storage.cassandra.CassandraPermissionTestUtils.MAX_RETRY_COUNT; +import static com.scalar.db.storage.cassandra.CassandraPermissionTestUtils.SLEEP_BETWEEN_RETRIES_SECONDS; + +import com.google.common.util.concurrent.Uninterruptibles; +import com.scalar.db.api.DistributedStoragePermissionIntegrationTestBase; +import com.scalar.db.util.AdminTestUtils; +import com.scalar.db.util.PermissionTestUtils; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class CassandraPermissionIntegrationTest + extends DistributedStoragePermissionIntegrationTestBase { + @Override + protected Properties getProperties(String testName) { + return CassandraEnv.getProperties(testName); + } + + @Override + protected Properties getPropertiesForNormalUser(String testName) { + return CassandraEnv.getPropertiesForNormalUser(testName); + } + + @Override + protected PermissionTestUtils getPermissionTestUtils(String testName) { + return new CassandraPermissionTestUtils(getProperties(testName)); + } + + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CassandraAdminTestUtils(getProperties(testName)); + } + + @Override + protected Map getCreationOptions() { + return Collections.singletonMap(CassandraAdmin.REPLICATION_FACTOR, "1"); + } + + @Override + protected void waitForTableCreation() { + try { + AdminTestUtils utils = getAdminTestUtils(TEST_NAME); + int retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + if (utils.tableExists(NAMESPACE, TABLE)) { + utils.close(); + return; + } + Uninterruptibles.sleepUninterruptibly(SLEEP_BETWEEN_RETRIES_SECONDS, TimeUnit.SECONDS); + retryCount++; + } + utils.close(); + throw new RuntimeException("Table was not created after " + MAX_RETRY_COUNT + " retries"); + } catch (Exception e) { + throw new RuntimeException("Failed to wait for table creation", e); + } + } +} diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java new file mode 100644 index 0000000000..5ef1ebfa48 --- /dev/null +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraPermissionTestUtils.java @@ -0,0 +1,55 @@ +package com.scalar.db.storage.cassandra; + +import com.datastax.driver.core.Session; +import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.PermissionTestUtils; +import java.util.Properties; + +public class CassandraPermissionTestUtils implements PermissionTestUtils { + public static final int SLEEP_BETWEEN_RETRIES_SECONDS = 3; + public static final int MAX_RETRY_COUNT = 10; + + private final ClusterManager clusterManager; + + public CassandraPermissionTestUtils(Properties properties) { + DatabaseConfig databaseConfig = new DatabaseConfig(properties); + clusterManager = new ClusterManager(databaseConfig); + } + + @Override + public void createNormalUser(String userName, String password) { + clusterManager + .getSession() + .execute( + String.format( + "CREATE ROLE %s WITH PASSWORD = '%s' AND LOGIN = true", userName, password)); + } + + @Override + public void dropNormalUser(String userName) { + clusterManager.getSession().execute(String.format("DROP ROLE %s", userName)); + } + + @Override + public void grantRequiredPermission(String userName) { + Session session = clusterManager.getSession(); + for (String grantStatement : getGrantPermissionStatements(userName)) { + session.execute(grantStatement); + } + } + + private String[] getGrantPermissionStatements(String userName) { + return new String[] { + String.format("GRANT CREATE ON ALL KEYSPACES TO %s", userName), + String.format("GRANT DROP ON ALL KEYSPACES TO %s", userName), + String.format("GRANT ALTER ON ALL KEYSPACES TO %s", userName), + String.format("GRANT SELECT ON ALL KEYSPACES TO %s", userName), + String.format("GRANT MODIFY ON ALL KEYSPACES TO %s", userName) + }; + } + + @Override + public void close() { + clusterManager.close(); + } +} diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java new file mode 100644 index 0000000000..8e74667b3f --- /dev/null +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java @@ -0,0 +1,429 @@ +package com.scalar.db.api; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; +import com.scalar.db.service.StorageFactory; +import com.scalar.db.util.AdminTestUtils; +import com.scalar.db.util.PermissionTestUtils; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class DistributedStorageAdminPermissionIntegrationTestBase { + protected static final String TEST_NAME = "storage_admin"; + protected static final String NAMESPACE = "test_" + TEST_NAME + "_1"; + protected static final String TABLE = "test_table_1"; + + private static final Logger logger = + LoggerFactory.getLogger(DistributedStorageAdminPermissionIntegrationTestBase.class); + private static final String COL_NAME1 = "c1"; + private static final String COL_NAME2 = "c2"; + private static final String COL_NAME3 = "c3"; + private static final String COL_NAME4 = "c4"; + private static final String RAW_COL_NAME = "raw_col"; + private static final String NEW_COL_NAME = "new_col"; + private static final TableMetadata TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(COL_NAME1, DataType.INT) + .addColumn(COL_NAME2, DataType.TEXT) + .addColumn(COL_NAME3, DataType.TEXT) + .addColumn(COL_NAME4, DataType.INT) + .addPartitionKey(COL_NAME1) + .addClusteringKey(COL_NAME2, Scan.Ordering.Order.ASC) + .addSecondaryIndex(COL_NAME4) + .build(); + + private DistributedStorageAdmin adminForRootUser; + private DistributedStorageAdmin adminForNormalUser; + private String normalUserName; + private String normalUserPassword; + + @BeforeAll + public void beforeAll() throws Exception { + Properties propertiesForRootUser = getProperties(TEST_NAME); + Properties propertiesForNormalUser = getPropertiesForNormalUser(TEST_NAME); + + // Initialize the admin for root user + StorageFactory factoryForRootUser = StorageFactory.create(propertiesForRootUser); + adminForRootUser = factoryForRootUser.getStorageAdmin(); + + // Create normal user and give permissions + DatabaseConfig config = new DatabaseConfig(propertiesForNormalUser); + normalUserName = getUserNameFromConfig(config); + normalUserPassword = getPasswordFromConfig(config); + setUpNormalUser(); + + // Initialize the admin for normal user + StorageFactory factoryForNormalUser = StorageFactory.create(propertiesForNormalUser); + adminForNormalUser = factoryForNormalUser.getStorageAdmin(); + } + + @AfterAll + public void afterAll() throws Exception { + try { + adminForRootUser.dropTable(NAMESPACE, TABLE, true); + adminForRootUser.dropNamespace(NAMESPACE, true); + } catch (Exception e) { + logger.warn("Failed to clean up resources", e); + } + + try { + if (adminForRootUser != null) { + adminForRootUser.close(); + } + } catch (Exception e) { + logger.warn("Failed to close admin for root user", e); + } + + try { + if (adminForNormalUser != null) { + adminForNormalUser.close(); + } + } catch (Exception e) { + logger.warn("Failed to close admin for normal user", e); + } + + cleanUpNormalUser(); + } + + @BeforeEach + public void beforeEach() throws ExecutionException { + dropTableByRootIfExists(); + dropNamespaceByRootIfExists(); + } + + @AfterEach + public void afterEach() { + sleepBetweenTests(); + } + + @Test + public void getImportTableMetadata_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.getImportTableMetadata(NAMESPACE, TABLE)) + .doesNotThrowAnyException(); + } + + @Test + public void addRawColumnToTable_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode( + () -> + adminForNormalUser.addRawColumnToTable( + NAMESPACE, TABLE, RAW_COL_NAME, DataType.INT)) + .doesNotThrowAnyException(); + } + + @Test + public void createNamespace_WithSufficientPermission_ShouldSucceed() { + // Arrange + // Act Assert + assertThatCode(() -> adminForNormalUser.createNamespace(NAMESPACE, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void createTable_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + // Act Assert + assertThatCode( + () -> + adminForNormalUser.createTable( + NAMESPACE, TABLE, TABLE_METADATA, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void dropTable_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.dropTable(NAMESPACE, TABLE)).doesNotThrowAnyException(); + } + + @Test + public void dropNamespace_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.dropNamespace(NAMESPACE, true)) + .doesNotThrowAnyException(); + } + + @Test + public void truncateTable_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.truncateTable(NAMESPACE, TABLE)) + .doesNotThrowAnyException(); + } + + @Test + public void createIndex_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode( + () -> adminForNormalUser.createIndex(NAMESPACE, TABLE, COL_NAME3, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void dropIndex_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.dropIndex(NAMESPACE, TABLE, COL_NAME4)) + .doesNotThrowAnyException(); + } + + @Test + public void indexExists_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.indexExists(NAMESPACE, TABLE, COL_NAME4)) + .doesNotThrowAnyException(); + } + + @Test + public void getTableMetadata_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.getTableMetadata(NAMESPACE, TABLE)) + .doesNotThrowAnyException(); + } + + @Test + public void getNamespaceTableNames_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.getNamespaceTableNames(NAMESPACE)) + .doesNotThrowAnyException(); + } + + @Test + public void namespaceExists_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.namespaceExists(NAMESPACE)).doesNotThrowAnyException(); + } + + @Test + public void tableExists_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.tableExists(NAMESPACE, TABLE)) + .doesNotThrowAnyException(); + } + + @Test + public void repairNamespace_WithSufficientPermission_ShouldSucceed() throws Exception { + // Arrange + createNamespaceByRoot(); + // Drop the namespaces table to simulate a repair scenario + AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); + try { + adminTestUtils.dropNamespacesTable(); + } finally { + adminTestUtils.close(); + } + // Act Assert + assertThatCode(() -> adminForNormalUser.repairNamespace(NAMESPACE, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void repairTable_WithSufficientPermission_ShouldSucceed() throws Exception { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Drop the metadata table to simulate a repair scenario + AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); + try { + adminTestUtils.dropMetadataTable(); + } finally { + adminTestUtils.close(); + } + // Act Assert + assertThatCode( + () -> + adminForNormalUser.repairTable( + NAMESPACE, TABLE, TABLE_METADATA, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void addNewColumnToTable_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + // Act Assert + assertThatCode( + () -> + adminForNormalUser.addNewColumnToTable( + NAMESPACE, TABLE, NEW_COL_NAME, DataType.INT)) + .doesNotThrowAnyException(); + } + + @Test + public void importTable_WithSufficientPermission_ShouldSucceed() throws Exception { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); + try { + adminTestUtils.dropNamespacesTable(); + adminTestUtils.dropMetadataTable(); + } finally { + adminTestUtils.close(); + } + // Act Assert + assertThatCode(() -> adminForNormalUser.importTable(NAMESPACE, TABLE, getCreationOptions())) + .doesNotThrowAnyException(); + } + + @Test + public void getNamespaceNames_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + createNamespaceByRoot(); + // Act Assert + assertThatCode(() -> adminForNormalUser.getNamespaceNames()).doesNotThrowAnyException(); + } + + @Test + public void upgrade_WithSufficientPermission_ShouldSucceed() throws Exception { + // Arrange + createNamespaceByRoot(); + createTableByRoot(); + AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); + try { + adminTestUtils.dropNamespacesTable(); + } finally { + adminTestUtils.close(); + } + // Act Assert + assertThatCode(() -> adminForNormalUser.upgrade(getCreationOptions())) + .doesNotThrowAnyException(); + } + + protected abstract Properties getProperties(String testName); + + protected abstract Properties getPropertiesForNormalUser(String testName); + + protected abstract AdminTestUtils getAdminTestUtils(String testName); + + protected abstract PermissionTestUtils getPermissionTestUtils(String testName); + + protected Map getCreationOptions() { + return Collections.emptyMap(); + } + + protected void waitForTableCreation() { + // Default do nothing + } + + protected void waitForNamespaceCreation() { + // Default do nothing + } + + protected void waitForTableDeletion() { + // Default do nothing + } + + protected void waitForNamespaceDeletion() { + // Default do nothing + } + + protected void sleepBetweenTests() { + // Default do nothing + } + + private void createNamespaceByRoot() throws ExecutionException { + adminForRootUser.createNamespace(NAMESPACE, getCreationOptions()); + waitForNamespaceCreation(); + } + + private void createTableByRoot() throws ExecutionException { + adminForRootUser.createTable(NAMESPACE, TABLE, TABLE_METADATA, getCreationOptions()); + waitForTableCreation(); + } + + private void dropNamespaceByRootIfExists() throws ExecutionException { + adminForRootUser.dropNamespace(NAMESPACE, true); + waitForNamespaceDeletion(); + } + + private void dropTableByRootIfExists() throws ExecutionException { + adminForRootUser.dropTable(NAMESPACE, TABLE, true); + waitForTableDeletion(); + } + + private String getUserNameFromConfig(DatabaseConfig config) { + return config + .getUsername() + .orElseThrow(() -> new IllegalArgumentException("Username must be set in the properties")); + } + + private String getPasswordFromConfig(DatabaseConfig config) { + return config + .getPassword() + .orElseThrow(() -> new IllegalArgumentException("Password must be set in the properties")); + } + + private void setUpNormalUser() { + PermissionTestUtils permissionTestUtils = getPermissionTestUtils(TEST_NAME); + try { + permissionTestUtils.createNormalUser(normalUserName, normalUserPassword); + permissionTestUtils.grantRequiredPermission(normalUserName); + } finally { + permissionTestUtils.close(); + } + } + + private void cleanUpNormalUser() { + PermissionTestUtils permissionTestUtils = getPermissionTestUtils(TEST_NAME); + try { + permissionTestUtils.dropNormalUser(normalUserName); + } finally { + permissionTestUtils.close(); + } + } +} diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStoragePermissionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStoragePermissionIntegrationTestBase.java new file mode 100644 index 0000000000..f28e41a60c --- /dev/null +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStoragePermissionIntegrationTestBase.java @@ -0,0 +1,388 @@ +package com.scalar.db.api; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; +import com.scalar.db.io.IntColumn; +import com.scalar.db.io.Key; +import com.scalar.db.service.StorageFactory; +import com.scalar.db.util.AdminTestUtils; +import com.scalar.db.util.PermissionTestUtils; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class DistributedStoragePermissionIntegrationTestBase { + protected static final String TEST_NAME = "storage"; + protected static final String NAMESPACE = "int_test_" + TEST_NAME; + protected static final String TABLE = "test_table"; + + private static final Logger logger = + LoggerFactory.getLogger(DistributedStoragePermissionIntegrationTestBase.class); + private static final String COL_NAME1 = "c1"; + private static final String COL_NAME2 = "c2"; + private static final String COL_NAME3 = "c3"; + private static final String COL_NAME4 = "c4"; + private static final int PARTITION_KEY_VALUE = 1; + private static final String CLUSTERING_KEY_VALUE1 = "value1"; + private static final String CLUSTERING_KEY_VALUE2 = "value2"; + private static final int INT_COLUMN_VALUE1 = 1; + private static final int INT_COLUMN_VALUE2 = 1; + + private DistributedStorage storageForNormalUser; + private DistributedStorageAdmin adminForRootUser; + private String namespace; + private String normalUserName; + private String normalUserPassword; + + @BeforeAll + public void beforeAll() throws Exception { + Properties propertiesForRootUser = getProperties(TEST_NAME); + Properties propertiesForNormalUser = getPropertiesForNormalUser(TEST_NAME); + + // Create admin for root user + StorageFactory factoryForRootUser = StorageFactory.create(propertiesForRootUser); + adminForRootUser = factoryForRootUser.getStorageAdmin(); + + // Create normal user and give permissions + DatabaseConfig config = new DatabaseConfig(propertiesForNormalUser); + normalUserName = getUserNameFromConfig(config); + normalUserPassword = getPasswordFromConfig(config); + setUpNormalUser(); + + // Create storage for normal user + StorageFactory factoryForNormalUser = StorageFactory.create(propertiesForNormalUser); + storageForNormalUser = factoryForNormalUser.getStorage(); + + namespace = getNamespace(); + createTable(); + waitForTableCreation(); + } + + @BeforeEach + public void setUp() throws Exception { + truncateTable(); + } + + @AfterAll + public void afterAll() throws Exception { + try { + dropTable(); + } catch (Exception e) { + logger.warn("Failed to drop table", e); + } + + try { + if (adminForRootUser != null) { + adminForRootUser.close(); + } + } catch (Exception e) { + logger.warn("Failed to close admin for root user", e); + } + + try { + if (storageForNormalUser != null) { + storageForNormalUser.close(); + } + } catch (Exception e) { + logger.warn("Failed to close storage for normal user", e); + } + + cleanUpNormalUser(); + } + + @Test + public void get_WithSufficientPermission_ShouldSucceed() { + // Arrange + Get get = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(COL_NAME1, PARTITION_KEY_VALUE)) + .clusteringKey(Key.ofText(COL_NAME2, CLUSTERING_KEY_VALUE1)) + .build(); + // Act Assert + assertThatCode(() -> storageForNormalUser.get(get)).doesNotThrowAnyException(); + } + + @Test + public void get_WithIndexKey_WithSufficientPermission_ShouldSucceed() { + // Arrange + Get get = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .indexKey(Key.ofInt("c3", INT_COLUMN_VALUE1)) + .build(); + // Act Assert + assertThatCode(() -> storageForNormalUser.get(get)).doesNotThrowAnyException(); + } + + @Test + public void scan_WithSufficientPermission_ShouldSucceed() { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(COL_NAME1, PARTITION_KEY_VALUE)) + .build(); + // Act Assert + assertThatCode(() -> storageForNormalUser.scan(scan).close()).doesNotThrowAnyException(); + } + + @Test + public void scan_WithIndexKey_WithSufficientPermission_ShouldSucceed() { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(namespace) + .table(TABLE) + .indexKey(Key.ofInt("c3", INT_COLUMN_VALUE1)) + .build(); + // Act Assert + assertThatCode(() -> storageForNormalUser.scan(scan).close()).doesNotThrowAnyException(); + } + + @Test + public void scanAll_WithSufficientPermission_ShouldSucceed() { + // Arrange + Scan scan = Scan.newBuilder().namespace(namespace).table(TABLE).all().build(); + // Act Assert + assertThatCode(() -> storageForNormalUser.scan(scan).close()).doesNotThrowAnyException(); + } + + @Test + public void put_WithoutCondition_WithSufficientPermission_ShouldSucceed() { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + // Act Assert + assertThatCode(() -> storageForNormalUser.put(put)).doesNotThrowAnyException(); + } + + @Test + public void put_WithPutIfNotExists_WithSufficientPermission_ShouldSucceed() { + // Arrange + Put putWithPutIfNotExists = + createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, ConditionBuilder.putIfNotExists()); + // Act Assert + assertThatCode(() -> storageForNormalUser.put(putWithPutIfNotExists)) + .doesNotThrowAnyException(); + } + + @Test + public void put_WithPutIfExists_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + storageForNormalUser.put(put); + Put putWithPutIfExists = + createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE2, ConditionBuilder.putIfExists()); + // Act Assert + assertThatCode(() -> storageForNormalUser.put(putWithPutIfExists)).doesNotThrowAnyException(); + } + + @Test + public void put_WithPutIf_WithSufficientPermission_ShouldSucceed() throws ExecutionException { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + storageForNormalUser.put(put); + ConditionalExpression conditionalExpression = + ConditionBuilder.buildConditionalExpression( + IntColumn.of(COL_NAME3, INT_COLUMN_VALUE1), ConditionalExpression.Operator.EQ); + Put putWithPutIf = + createPut( + CLUSTERING_KEY_VALUE1, + INT_COLUMN_VALUE2, + ConditionBuilder.putIf(conditionalExpression).build()); + // Act Assert + assertThatCode(() -> storageForNormalUser.put(putWithPutIf)).doesNotThrowAnyException(); + } + + @Test + public void put_WithMultiplePuts_WithSufficientPermission_ShouldSucceed() { + // Arrange + Put put1 = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + Put put2 = createPut(CLUSTERING_KEY_VALUE2, INT_COLUMN_VALUE2, null); + // Act Assert + assertThatCode(() -> storageForNormalUser.put(Arrays.asList(put1, put2))) + .doesNotThrowAnyException(); + } + + @Test + public void delete_WithSufficientPermission_ShouldSucceed() { + // Arrange + Delete delete = createDelete(CLUSTERING_KEY_VALUE1, null); + // Act Assert + assertThatCode(() -> storageForNormalUser.delete(delete)).doesNotThrowAnyException(); + } + + @Test + public void delete_WithDeleteIfExists_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + storageForNormalUser.put(put); + Delete delete = createDelete(CLUSTERING_KEY_VALUE1, ConditionBuilder.deleteIfExists()); + // Act Assert + assertThatCode(() -> storageForNormalUser.delete(delete)).doesNotThrowAnyException(); + } + + @Test + public void delete_WithDeleteIf_WithSufficientPermission_ShouldSucceed() + throws ExecutionException { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + storageForNormalUser.put(put); + ConditionalExpression conditionalExpression = + ConditionBuilder.buildConditionalExpression( + IntColumn.of(COL_NAME3, INT_COLUMN_VALUE1), ConditionalExpression.Operator.EQ); + Delete delete = + createDelete( + CLUSTERING_KEY_VALUE1, ConditionBuilder.deleteIf(conditionalExpression).build()); + // Act Assert + assertThatCode(() -> storageForNormalUser.delete(delete)).doesNotThrowAnyException(); + } + + @Test + public void delete_WithMultipleDeletes_WithSufficientPermission_ShouldSucceed() { + // Arrange + Delete delete1 = createDelete(CLUSTERING_KEY_VALUE1, null); + Delete delete2 = createDelete(CLUSTERING_KEY_VALUE2, null); + // Act Assert + assertThatCode(() -> storageForNormalUser.delete(Arrays.asList(delete1, delete2))) + .doesNotThrowAnyException(); + } + + @Test + public void mutate_WithSufficientPermission_ShouldSucceed() { + // Arrange + Put put = createPut(CLUSTERING_KEY_VALUE1, INT_COLUMN_VALUE1, null); + Delete delete = createDelete(CLUSTERING_KEY_VALUE2, null); + // Act Assert + assertThatCode(() -> storageForNormalUser.mutate(Arrays.asList(put, delete))) + .doesNotThrowAnyException(); + } + + protected abstract Properties getProperties(String testName); + + protected abstract Properties getPropertiesForNormalUser(String testName); + + protected abstract PermissionTestUtils getPermissionTestUtils(String testName); + + protected abstract AdminTestUtils getAdminTestUtils(String testName); + + protected String getNamespace() { + return NAMESPACE; + } + + protected Map getCreationOptions() { + return Collections.emptyMap(); + } + + protected void waitForTableCreation() { + // Default do nothing + } + + private void createTable() throws ExecutionException { + Map options = getCreationOptions(); + adminForRootUser.createNamespace(namespace, true, options); + adminForRootUser.createTable( + namespace, + TABLE, + TableMetadata.newBuilder() + .addColumn(COL_NAME1, DataType.INT) + .addColumn(COL_NAME2, DataType.TEXT) + .addColumn(COL_NAME3, DataType.INT) + .addColumn(COL_NAME4, DataType.INT) + .addPartitionKey(COL_NAME1) + .addClusteringKey(COL_NAME2) + .addSecondaryIndex(COL_NAME3) + .build(), + true, + options); + } + + private void truncateTable() throws ExecutionException { + adminForRootUser.truncateTable(namespace, TABLE); + } + + private void dropTable() throws ExecutionException { + adminForRootUser.dropTable(namespace, TABLE); + adminForRootUser.dropNamespace(namespace); + } + + private Put createPut(String clusteringKey, int intColumnValue, MutationCondition condition) { + PutBuilder.Buildable buildable = + Put.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey( + Key.ofInt( + COL_NAME1, DistributedStoragePermissionIntegrationTestBase.PARTITION_KEY_VALUE)) + .clusteringKey(Key.ofText(COL_NAME2, clusteringKey)) + .intValue(COL_NAME3, intColumnValue) + .intValue(COL_NAME4, intColumnValue); + if (condition != null) { + buildable.condition(condition); + } + return buildable.build(); + } + + private Delete createDelete(String clusteringKey, MutationCondition condition) { + DeleteBuilder.Buildable buildable = + Delete.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey( + Key.ofInt( + COL_NAME1, DistributedStoragePermissionIntegrationTestBase.PARTITION_KEY_VALUE)) + .clusteringKey(Key.ofText(COL_NAME2, clusteringKey)); + if (condition != null) { + buildable.condition(condition); + } + return buildable.build(); + } + + private String getUserNameFromConfig(DatabaseConfig config) { + return config + .getUsername() + .orElseThrow(() -> new IllegalArgumentException("Username must be set in the properties")); + } + + private String getPasswordFromConfig(DatabaseConfig config) { + return config + .getPassword() + .orElseThrow(() -> new IllegalArgumentException("Password must be set in the properties")); + } + + private void setUpNormalUser() { + PermissionTestUtils permissionTestUtils = getPermissionTestUtils(TEST_NAME); + try { + permissionTestUtils.createNormalUser(normalUserName, normalUserPassword); + permissionTestUtils.grantRequiredPermission(normalUserName); + } finally { + permissionTestUtils.close(); + } + } + + private void cleanUpNormalUser() { + PermissionTestUtils permissionTestUtils = getPermissionTestUtils(TEST_NAME); + try { + permissionTestUtils.dropNormalUser(normalUserName); + } finally { + permissionTestUtils.close(); + } + } +} diff --git a/integration-test/src/main/java/com/scalar/db/util/PermissionTestUtils.java b/integration-test/src/main/java/com/scalar/db/util/PermissionTestUtils.java new file mode 100644 index 0000000000..085ffa37b0 --- /dev/null +++ b/integration-test/src/main/java/com/scalar/db/util/PermissionTestUtils.java @@ -0,0 +1,12 @@ +package com.scalar.db.util; + +public interface PermissionTestUtils { + + void createNormalUser(String userName, String password); + + void dropNormalUser(String userName); + + void grantRequiredPermission(String userName); + + void close(); +} From 6f2afd95fada6d31199362f6105d29e27650eba7 Mon Sep 17 00:00:00 2001 From: Kodai Doki Date: Thu, 10 Jul 2025 14:32:18 +0900 Subject: [PATCH 2/2] Update AdminTestUtils --- .../cassandra/CassandraAdminTestUtils.java | 19 ++++ .../storage/cosmos/CosmosAdminTestUtils.java | 31 ++++++ .../storage/dynamo/DynamoAdminTestUtils.java | 14 ++- .../db/storage/jdbc/JdbcAdminTestUtils.java | 66 ++++++++++++- .../MultiStorageAdminTestUtils.java | 98 ++++++++++++++++++- ...ageAdminPermissionIntegrationTestBase.java | 33 ------- .../com/scalar/db/util/AdminTestUtils.java | 28 ++++++ 7 files changed, 250 insertions(+), 39 deletions(-) diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java index 6c939f2eab..24c1b2ad8f 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java @@ -1,12 +1,16 @@ package com.scalar.db.storage.cassandra; +import com.scalar.db.config.DatabaseConfig; import com.scalar.db.util.AdminTestUtils; import java.util.Properties; public class CassandraAdminTestUtils extends AdminTestUtils { + private final ClusterManager clusterManager; public CassandraAdminTestUtils(Properties properties) { super(properties); + DatabaseConfig databaseConfig = new DatabaseConfig(properties); + clusterManager = new ClusterManager(databaseConfig); } @Override @@ -23,4 +27,19 @@ public void truncateMetadataTable() { public void corruptMetadata(String namespace, String table) { // Do nothing } + + @Override + public boolean namespaceExists(String namespace) { + return clusterManager.getSession().getCluster().getMetadata().getKeyspace(namespace) != null; + } + + @Override + public boolean tableExists(String namespace, String table) { + return clusterManager.getMetadata(namespace, table) != null; + } + + @Override + public void close() { + clusterManager.close(); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java index 6b79ecdbd7..c4e62f960a 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java @@ -90,4 +90,35 @@ public CosmosStoredProcedure getTableStoredProcedure(String namespace, String ta .getScripts() .getStoredProcedure(CosmosAdmin.STORED_PROCEDURE_FILE_NAME); } + + @Override + public boolean namespaceExists(String namespace) { + try { + client.getDatabase(namespace).read(); + } catch (CosmosException e) { + if (e.getStatusCode() == CosmosErrorCode.NOT_FOUND.get()) { + return false; + } + throw e; + } + return true; + } + + @Override + public boolean tableExists(String namespace, String table) { + try { + client.getDatabase(namespace).getContainer(table).read(); + } catch (CosmosException e) { + if (e.getStatusCode() == CosmosErrorCode.NOT_FOUND.get()) { + return false; + } + throw e; + } + return true; + } + + @Override + public void close() { + client.close(); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java index 4a9e6d8788..74e80302fb 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java @@ -78,7 +78,8 @@ private boolean waitForTableDeletion(String namespace, String table) { return false; } - private boolean tableExists(String nonPrefixedNamespace, String table) { + @Override + public boolean tableExists(String nonPrefixedNamespace, String table) { try { client.describeTable( DescribeTableRequest.builder() @@ -137,4 +138,15 @@ public void corruptMetadata(String namespace, String table) { .item(itemValues) .build()); } + + @Override + public boolean namespaceExists(String namespace) throws Exception { + // Dynamo has no concept of namespace + return true; + } + + @Override + public void close() { + client.close(); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java index 8ac46353fc..0fbcb9b909 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java @@ -6,22 +6,26 @@ import com.scalar.db.util.AdminTestUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Properties; import org.apache.commons.dbcp2.BasicDataSource; public class JdbcAdminTestUtils extends AdminTestUtils { - private final JdbcConfig config; private final String metadataSchema; private final RdbEngineStrategy rdbEngine; + private final BasicDataSource dataSource; public JdbcAdminTestUtils(Properties properties) { super(properties); - config = new JdbcConfig(new DatabaseConfig(properties)); + JdbcConfig config = new JdbcConfig(new DatabaseConfig(properties)); metadataSchema = config.getTableMetadataSchema().orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); rdbEngine = RdbEngineFactory.create(config); + dataSource = JdbcUtils.initDataSourceForAdmin(config, rdbEngine); } @Override @@ -53,9 +57,63 @@ public void corruptMetadata(String namespace, String table) throws Exception { } private void execute(String sql) throws SQLException { - try (BasicDataSource dataSource = JdbcUtils.initDataSourceForAdmin(config, rdbEngine); - Connection connection = dataSource.getConnection()) { + try (Connection connection = dataSource.getConnection()) { JdbcAdmin.execute(connection, sql); } } + + @Override + public boolean namespaceExists(String namespace) throws SQLException { + String sql; + if (JdbcTestUtils.isMysql(rdbEngine)) { + sql = "SELECT 1 FROM information_schema.schemata WHERE schema_name = ?"; + } else if (JdbcTestUtils.isOracle(rdbEngine)) { + sql = "SELECT 1 FROM all_users WHERE username = ?"; + } else if (JdbcTestUtils.isPostgresql(rdbEngine)) { + sql = "SELECT 1 FROM pg_namespace WHERE nspname = ?"; + } else if (JdbcTestUtils.isSqlite(rdbEngine)) { + // SQLite has no concept of namespace + return true; + } else if (JdbcTestUtils.isSqlServer(rdbEngine)) { + sql = "SELECT 1 FROM sys.schemas WHERE name = ?"; + } else if (JdbcTestUtils.isDb2(rdbEngine)) { + sql = "SELECT 1 FROM syscat.schemata WHERE schemaname = ?"; + } else { + throw new AssertionError("Unsupported engine : " + rdbEngine.getClass().getSimpleName()); + } + + try (Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setString(1, namespace); + ResultSet resultSet = preparedStatement.executeQuery(); + + return resultSet.next(); + } + } + + @Override + public boolean tableExists(String namespace, String table) throws Exception { + String fullTableName = rdbEngine.encloseFullTableName(namespace, table); + String sql = rdbEngine.tableExistsInternalTableCheckSql(fullTableName); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute(sql); + return true; + } catch (SQLException e) { + // An exception will be thrown if the table does not exist when executing the select + // query + if (rdbEngine.isUndefinedTableError(e)) { + return false; + } + throw new Exception( + String.format( + "Checking if the %s table exists failed", getFullTableName(namespace, table)), + e); + } + } + + @Override + public void close() throws SQLException { + dataSource.close(); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java index 699a010b4f..3b4d05beb7 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java @@ -3,8 +3,10 @@ import static com.scalar.db.util.ScalarDbUtils.getFullTableName; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.storage.cassandra.ClusterManager; import com.scalar.db.storage.jdbc.JdbcAdmin; import com.scalar.db.storage.jdbc.JdbcConfig; +import com.scalar.db.storage.jdbc.JdbcTestUtils; import com.scalar.db.storage.jdbc.JdbcUtils; import com.scalar.db.storage.jdbc.RdbEngineFactory; import com.scalar.db.storage.jdbc.RdbEngineOracle; @@ -12,25 +14,33 @@ import com.scalar.db.util.AdminTestUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.apache.commons.dbcp2.BasicDataSource; public class MultiStorageAdminTestUtils extends AdminTestUtils { - + // for Cassandra + private final ClusterManager clusterManager; + // for JDBC private final JdbcConfig jdbcConfig; private final String jdbcMetadataSchema; private final RdbEngineStrategy rdbEngine; + private final BasicDataSource dataSource; public MultiStorageAdminTestUtils(Properties cassandraProperties, Properties jdbcProperties) { // Cassandra has the coordinator tables super(cassandraProperties); + clusterManager = new ClusterManager(new DatabaseConfig(cassandraProperties)); + // for JDBC jdbcConfig = new JdbcConfig(new DatabaseConfig(jdbcProperties)); jdbcMetadataSchema = jdbcConfig.getTableMetadataSchema().orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); rdbEngine = RdbEngineFactory.create(jdbcConfig); + dataSource = JdbcUtils.initDataSourceForAdmin(jdbcConfig, rdbEngine); } @Override @@ -77,6 +87,86 @@ public void corruptMetadata(String namespace, String table) throws Exception { execute(insertCorruptedMetadataStatement); } + @Override + public boolean namespaceExists(String namespace) throws SQLException { + boolean existsOnCassandra = namespaceExistsOnCassandra(namespace); + boolean existsOnJdbc = namespaceExistsOnJdbc(namespace); + + if (existsOnCassandra && existsOnJdbc) { + throw new IllegalStateException( + "The " + namespace + " namespace should not exist on both storages"); + } + return existsOnCassandra || existsOnJdbc; + } + + private boolean namespaceExistsOnCassandra(String namespace) { + return clusterManager.getSession().getCluster().getMetadata().getKeyspace(namespace) != null; + } + + private boolean namespaceExistsOnJdbc(String namespace) throws SQLException { + String sql; + // RdbEngine classes are not publicly exposed, so we test the type using hard coded class name + if (JdbcTestUtils.isMysql(rdbEngine)) { + sql = "SELECT 1 FROM information_schema.schemata WHERE schema_name = ?"; + } else if (JdbcTestUtils.isPostgresql(rdbEngine)) { + sql = "SELECT 1 FROM pg_namespace WHERE nspname = ?"; + } else if (JdbcTestUtils.isOracle(rdbEngine)) { + sql = "SELECT 1 FROM all_users WHERE username = ?"; + } else if (JdbcTestUtils.isSqlServer(rdbEngine)) { + sql = "SELECT 1 FROM sys.schemas WHERE name = ?"; + } else if (JdbcTestUtils.isSqlite(rdbEngine)) { + // SQLite has no concept of namespace + return true; + } else { + throw new AssertionError("Unsupported engine : " + rdbEngine.getClass().getSimpleName()); + } + + try (Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setString(1, namespace); + ResultSet resultSet = preparedStatement.executeQuery(); + + return resultSet.next(); + } + } + + @Override + public boolean tableExists(String namespace, String table) throws Exception { + boolean existsOnCassandra = tableExistsOnCassandra(namespace, table); + boolean existsOnJdbc = tableExistsOnJdbc(namespace, table); + if (existsOnCassandra && existsOnJdbc) { + throw new IllegalStateException( + String.format( + "The %s table should not exist on both storages", + getFullTableName(namespace, table))); + } + return existsOnCassandra || existsOnJdbc; + } + + private boolean tableExistsOnCassandra(String namespace, String table) { + return clusterManager.getMetadata(namespace, table) != null; + } + + private boolean tableExistsOnJdbc(String namespace, String table) throws Exception { + String fullTableName = rdbEngine.encloseFullTableName(namespace, table); + String sql = rdbEngine.tableExistsInternalTableCheckSql(fullTableName); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute(sql); + return true; + } catch (SQLException e) { + // An exception will be thrown if the table does not exist when executing the select + // query + if (rdbEngine.isUndefinedTableError(e)) { + return false; + } + throw new Exception( + String.format( + "Checking if the %s table exists failed", getFullTableName(namespace, table)), + e); + } + } + private void execute(String sql) throws SQLException { try (BasicDataSource dataSource = JdbcUtils.initDataSourceForAdmin(jdbcConfig, rdbEngine); Connection connection = dataSource.getConnection(); @@ -84,4 +174,10 @@ private void execute(String sql) throws SQLException { stmt.execute(sql); } } + + @Override + public void close() throws SQLException { + clusterManager.close(); + dataSource.close(); + } } diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java index 8e74667b3f..69b97d6d2b 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminPermissionIntegrationTestBase.java @@ -253,22 +253,6 @@ public void tableExists_WithSufficientPermission_ShouldSucceed() throws Executio .doesNotThrowAnyException(); } - @Test - public void repairNamespace_WithSufficientPermission_ShouldSucceed() throws Exception { - // Arrange - createNamespaceByRoot(); - // Drop the namespaces table to simulate a repair scenario - AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); - try { - adminTestUtils.dropNamespacesTable(); - } finally { - adminTestUtils.close(); - } - // Act Assert - assertThatCode(() -> adminForNormalUser.repairNamespace(NAMESPACE, getCreationOptions())) - .doesNotThrowAnyException(); - } - @Test public void repairTable_WithSufficientPermission_ShouldSucceed() throws Exception { // Arrange @@ -310,7 +294,6 @@ public void importTable_WithSufficientPermission_ShouldSucceed() throws Exceptio createTableByRoot(); AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); try { - adminTestUtils.dropNamespacesTable(); adminTestUtils.dropMetadataTable(); } finally { adminTestUtils.close(); @@ -328,22 +311,6 @@ public void getNamespaceNames_WithSufficientPermission_ShouldSucceed() throws Ex assertThatCode(() -> adminForNormalUser.getNamespaceNames()).doesNotThrowAnyException(); } - @Test - public void upgrade_WithSufficientPermission_ShouldSucceed() throws Exception { - // Arrange - createNamespaceByRoot(); - createTableByRoot(); - AdminTestUtils adminTestUtils = getAdminTestUtils(TEST_NAME); - try { - adminTestUtils.dropNamespacesTable(); - } finally { - adminTestUtils.close(); - } - // Act Assert - assertThatCode(() -> adminForNormalUser.upgrade(getCreationOptions())) - .doesNotThrowAnyException(); - } - protected abstract Properties getProperties(String testName); protected abstract Properties getPropertiesForNormalUser(String testName); diff --git a/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java b/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java index 3c80bb9bb4..f548040d54 100644 --- a/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java +++ b/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java @@ -73,4 +73,32 @@ public boolean areTableMetadataForCoordinatorTablesPresent() throws Exception { } return tableMetadata.equals(expectedMetadata); } + + /** + * Verify if the namespace exists in the underlying storage. It does not check the ScalarDB + * metadata. + * + * @param namespace a namespace + * @return true if the namespace exists or if the storage does not have the concept of namespace, + * false otherwise + * @throws Exception if an error occurs + */ + public abstract boolean namespaceExists(String namespace) throws Exception; + + /** + * Check if the table exists in the underlying storage. + * + * @param namespace a namespace + * @param table a table + * @return true if the table exists, false otherwise + * @throws Exception if an error occurs + */ + public abstract boolean tableExists(String namespace, String table) throws Exception; + + /** + * Closes connections to the storage + * + * @throws Exception if an error occurs + */ + public abstract void close() throws Exception; }