diff --git a/README.md b/README.md index 56d668b0a6..e70ee5eb3a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.31.0') +implementation platform('com.google.cloud:libraries-bom:26.32.0') implementation 'com.google.cloud:google-cloud-spanner' ``` diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 54eae11d51..fbbc0153f8 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -539,6 +539,30 @@ com/google/cloud/spanner/Dialect java.lang.String getDefaultSchema() + + + 7012 + com/google/cloud/spanner/Spanner + com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient createDatabaseAdminClient() + + + 7012 + com/google/cloud/spanner/Spanner + com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient createInstanceAdminClient() + + + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings getDatabaseAdminStubSettings() + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings getInstanceAdminStubSettings() + + 7005 com/google/cloud/spanner/PartitionedDmlTransaction @@ -556,6 +580,5 @@ 7012 com/google/cloud/spanner/connection/Connection void setDirectedRead(com.google.spanner.v1.DirectedReadOptions) - - + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java index 52c35cb713..7ccbc88d97 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java @@ -26,7 +26,12 @@ * quota. */ public interface Spanner extends Service, AutoCloseable { - /** Returns a {@code DatabaseAdminClient} to do admin operations on Cloud Spanner databases. */ + + /** + * Returns a {@code DatabaseAdminClient} to execute admin operations on Cloud Spanner databases. + * + * @return {@code DatabaseAdminClient} + */ /* * *
{@code
@@ -38,7 +43,34 @@ public interface Spanner extends Service, AutoCloseable {
    */
   DatabaseAdminClient getDatabaseAdminClient();
 
-  /** Returns an {@code InstanceAdminClient} to do admin operations on Cloud Spanner instances. */
+  /**
+   * Returns a {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} to execute
+   * admin operations on Cloud Spanner databases. This method always creates a new instance of
+   * {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} which is an {@link
+   * AutoCloseable} resource. For optimising the number of clients, caller may choose to cache the
+   * clients instead of repeatedly invoking this method and creating new instances.
+   *
+   * @return {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient}
+   */
+  /*
+   * 
+   * 
{@code
+   * SpannerOptions options = SpannerOptions.newBuilder().build();
+   * Spanner spanner = options.getService();
+   * DatabaseAdminClient dbAdminClient = spanner.createDatabaseAdminClient();
+   * }
+ * + */ + default com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient + createDatabaseAdminClient() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Returns an {@code InstanceAdminClient} to execute admin operations on Cloud Spanner instances. + * + * @return {@code InstanceAdminClient} + */ /* * *
{@code
@@ -50,6 +82,29 @@ public interface Spanner extends Service, AutoCloseable {
    */
   InstanceAdminClient getInstanceAdminClient();
 
+  /**
+   * Returns a {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} to execute
+   * admin operations on Cloud Spanner databases. This method always creates a new instance of
+   * {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} which is an {@link
+   * AutoCloseable} resource. For optimising the number of clients, caller may choose to cache the
+   * clients instead of repeatedly invoking this method and creating new instances.
+   *
+   * @return {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient}
+   */
+  /*
+   * 
+   * 
{@code
+   * SpannerOptions options = SpannerOptions.newBuilder().build();
+   * Spanner spanner = options.getService();
+   * InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient();
+   * }
+ * + */ + default com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient + createInstanceAdminClient() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns a {@code DatabaseClient} for the given database. It uses a pool of sessions to talk to * the database. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 8fe06f76cc..2ab75d7417 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -25,6 +25,8 @@ import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.SessionClient.SessionId; import com.google.cloud.spanner.SpannerOptions.CloseableExecutorProvider; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.spi.v1.GapicSpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; @@ -40,6 +42,7 @@ import io.opencensus.trace.Tracing; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -207,11 +210,37 @@ public DatabaseAdminClient getDatabaseAdminClient() { return dbAdminClient; } + @Override + public com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient + createDatabaseAdminClient() { + try { + final DatabaseAdminStubSettings settings = + Preconditions.checkNotNull(gapicRpc.getDatabaseAdminStubSettings()); + return com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.create( + settings.createStub()); + } catch (IOException ex) { + throw SpannerExceptionFactory.newSpannerException(ex); + } + } + @Override public InstanceAdminClient getInstanceAdminClient() { return instanceClient; } + @Override + public com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient + createInstanceAdminClient() { + try { + final InstanceAdminStubSettings settings = + Preconditions.checkNotNull(gapicRpc.getInstanceAdminStubSettings()); + return com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient.create( + settings.createStub()); + } catch (IOException ex) { + throw SpannerExceptionFactory.newSpannerException(ex); + } + } + @Override public DatabaseClient getDatabaseClient(DatabaseId db) { synchronized (this) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index c9aa598766..0f4b227571 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -243,6 +243,7 @@ public class GapicSpannerRpc implements SpannerRpc { private final Set readRetryableCodes; private final SpannerStub partitionedDmlStub; private final RetrySettings partitionedDmlRetrySettings; + private final InstanceAdminStubSettings instanceAdminStubSettings; private final InstanceAdminStub instanceAdminStub; private final DatabaseAdminStubSettings databaseAdminStubSettings; private final DatabaseAdminStub databaseAdminStub; @@ -435,16 +436,15 @@ public GapicSpannerRpc(final SpannerOptions options) { .withCheckInterval(pdmlSettings.getStreamWatchdogCheckInterval())); } this.partitionedDmlStub = GrpcSpannerStub.create(pdmlSettings.build()); - - this.instanceAdminStub = - GrpcInstanceAdminStub.create( - options - .getInstanceAdminStubSettings() - .toBuilder() - .setTransportChannelProvider(channelProvider) - .setCredentialsProvider(credentialsProvider) - .setStreamWatchdogProvider(watchdogProvider) - .build()); + this.instanceAdminStubSettings = + options + .getInstanceAdminStubSettings() + .toBuilder() + .setTransportChannelProvider(channelProvider) + .setCredentialsProvider(credentialsProvider) + .setStreamWatchdogProvider(watchdogProvider) + .build(); + this.instanceAdminStub = GrpcInstanceAdminStub.create(instanceAdminStubSettings); this.databaseAdminStubSettings = options @@ -510,6 +510,7 @@ public UnaryCallable createUnaryCalla this.executeQueryRetryableCodes = null; this.partitionedDmlStub = null; this.databaseAdminStubSettings = null; + this.instanceAdminStubSettings = null; this.spannerWatchdog = null; this.partitionedDmlRetrySettings = null; } @@ -2004,6 +2005,16 @@ public boolean isClosed() { return rpcIsClosed; } + @Override + public DatabaseAdminStubSettings getDatabaseAdminStubSettings() { + return databaseAdminStubSettings; + } + + @Override + public InstanceAdminStubSettings getInstanceAdminStubSettings() { + return instanceAdminStubSettings; + } + private static final class GrpcStreamingCall implements StreamingCall { private final ApiCallContext callContext; private final StreamController controller; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 89659e4741..7868f3ec09 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -28,7 +28,9 @@ import com.google.cloud.spanner.Restore; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.common.collect.ImmutableList; import com.google.iam.v1.GetPolicyOptions; @@ -500,4 +502,24 @@ TestIamPermissionsResponse testInstanceAdminIAMPermissions( void shutdown(); boolean isClosed(); + + /** + * Getter method to obtain the auto-generated instance admin client stub settings. + * + * @return InstanceAdminStubSettings + */ + @InternalApi + default InstanceAdminStubSettings getInstanceAdminStubSettings() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Getter method to obtain the auto-generated database admin client stub settings. + * + * @return DatabaseAdminStubSettings + */ + @InternalApi + default DatabaseAdminStubSettings getDatabaseAdminStubSettings() { + throw new UnsupportedOperationException("Not implemented"); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java index 31a6cad4c8..3cf13dc58d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.when; @@ -29,9 +30,16 @@ import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly; import com.google.cloud.spanner.SpannerImpl.ClosedException; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import io.opentelemetry.api.OpenTelemetry; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; @@ -55,6 +63,10 @@ public class SpannerImplTest { @Mock private SpannerRpc rpc; @Mock private SpannerOptions spannerOptions; + @Mock private DatabaseAdminStubSettings databaseAdminStubSettings; + @Mock private DatabaseAdminStub databaseAdminStub; + @Mock private InstanceAdminStubSettings instanceAdminStubSettings; + @Mock private InstanceAdminStub instanceAdminStub; private SpannerImpl impl; @Captor ArgumentCaptor> options; @@ -286,6 +298,66 @@ public void testClosedException() { assertThat(sw.toString()).contains("closeSpannerAndIncludeStacktrace"); } + @Test + public void testCreateDatabaseAdminClient_whenNullAdminSettings_assertPreconditionFailure() { + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + assertThrows(NullPointerException.class, spanner::createDatabaseAdminClient); + } + + @Test + public void testCreateDatabaseAdminClient_whenMockAdminSettings_assertMethodInvocation() + throws IOException { + when(rpc.getDatabaseAdminStubSettings()).thenReturn(databaseAdminStubSettings); + when(databaseAdminStubSettings.createStub()).thenReturn(databaseAdminStub); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient(); + assertNotNull(databaseAdminClient); + } + + @Test(expected = SpannerException.class) + public void testCreateDatabaseAdminClient_whenMockAdminSettings_assertException() + throws IOException { + when(rpc.getDatabaseAdminStubSettings()).thenReturn(databaseAdminStubSettings); + when(databaseAdminStubSettings.createStub()).thenThrow(IOException.class); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient(); + assertNotNull(databaseAdminClient); + } + + @Test + public void testCreateInstanceAdminClient_whenNullAdminSettings_assertPreconditionFailure() { + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + assertThrows(NullPointerException.class, spanner::createInstanceAdminClient); + } + + @Test + public void testCreateInstanceAdminClient_whenMockAdminSettings_assertMethodInvocation() + throws IOException { + when(rpc.getInstanceAdminStubSettings()).thenReturn(instanceAdminStubSettings); + when(instanceAdminStubSettings.createStub()).thenReturn(instanceAdminStub); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient(); + assertNotNull(instanceAdminClient); + } + + @Test(expected = SpannerException.class) + public void testCreateInstanceAdminClient_whenMockAdminSettings_assertException() + throws IOException { + when(rpc.getInstanceAdminStubSettings()).thenReturn(instanceAdminStubSettings); + when(instanceAdminStubSettings.createStub()).thenThrow(IOException.class); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient(); + assertNotNull(instanceAdminClient); + } + private void closeSpannerAndIncludeStacktrace(Spanner spanner) { spanner.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java new file mode 100644 index 0000000000..a1c6a35819 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.it; + +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; + +import com.google.api.gax.rpc.PermissionDeniedException; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.InstanceName; +import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.ProjectName; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Integration tests for testing the auto-generated database admin {@link + * com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} and instance admin clients {@link + * com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} + */ +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITAutogeneratedAdminClientTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static DatabaseAdminClient dbAdminClient; + + private static InstanceAdminClient instanceAdminClient; + private static RemoteSpannerHelper testHelper; + + private static List databasesToDrop; + + @Parameter public DatabaseDialect dialect; + + @Parameters(name = "Dialect = {0}") + public static List data() { + return ImmutableList.of(DatabaseDialect.GOOGLE_STANDARD_SQL, DatabaseDialect.POSTGRESQL); + } + + @BeforeClass + public static void setUp() { + assumeFalse("Emulator does not support database roles", isUsingEmulator()); + testHelper = env.getTestHelper(); + dbAdminClient = testHelper.getClient().createDatabaseAdminClient(); + instanceAdminClient = testHelper.getClient().createInstanceAdminClient(); + databasesToDrop = new ArrayList<>(); + } + + @AfterClass + public static void cleanup() throws Exception { + if (databasesToDrop != null) { + for (DatabaseName databaseName : databasesToDrop) { + try { + dbAdminClient.dropDatabase(databaseName); + } catch (Exception e) { + System.err.println( + "Failed to drop database " + databaseName + ", skipping...: " + e.getMessage()); + } + } + } + } + + @Test + public void grantAndRevokeDatabaseRolePermissions() throws Exception { + // Create database with table and role permission. + final String dbRoleParent = "parent"; + final String databaseId = testHelper.getUniqueDatabaseId(); + final InstanceId instanceId = testHelper.getInstanceId(); + + final String createTableT = getCreateTableStatement(); + final String createRoleParent = String.format("CREATE ROLE %s", dbRoleParent); + final String grantSelectOnTableToParent = + dialect == DatabaseDialect.POSTGRESQL + ? String.format("GRANT SELECT ON TABLE T TO %s", dbRoleParent) + : String.format("GRANT SELECT ON TABLE T TO ROLE %s", dbRoleParent); + final Database createdDatabase = + createAndUpdateDatabase( + testHelper.getOptions().getProjectId(), + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleParent, grantSelectOnTableToParent)); + + // Connect to db with dbRoleParent. + SpannerOptions options = + testHelper.getOptions().toBuilder().setDatabaseRole(dbRoleParent).build(); + + Spanner spanner = options.getService(); + DatabaseId id = DatabaseId.of(createdDatabase.getName()); + DatabaseClient dbClient = spanner.getDatabaseClient(id); + + // Test SELECT permissions to role dbRoleParent on table T. + // Query using dbRoleParent should return result. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + assertTrue(rs.next()); + assertEquals(dbClient.getDatabaseRole(), dbRoleParent); + } catch (PermissionDeniedException e) { + // This is not expected + fail("Got PermissionDeniedException when it should not have occurred."); + } + + // Revoke select Permission for dbRoleParent. + final String revokeSelectOnTableFromParent = + dialect == DatabaseDialect.POSTGRESQL + ? String.format("REVOKE SELECT ON TABLE T FROM %s", dbRoleParent) + : String.format("REVOKE SELECT ON TABLE T FROM ROLE %s", dbRoleParent); + + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(revokeSelectOnTableFromParent)) + .get(5, TimeUnit.MINUTES); + + // Test SELECT permissions to role dbRoleParent on table T. + // Query using dbRoleParent should return PermissionDeniedException. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.PERMISSION_DENIED); + assertThat(e.getMessage()).contains(dbRoleParent); + } + // Drop role and table. + final String dropTableT = "DROP TABLE T"; + final String dropRoleParent = String.format("DROP ROLE %s", dbRoleParent); + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(dropTableT, dropRoleParent)) + .get(5, TimeUnit.MINUTES); + databasesToDrop.add(DatabaseName.parse(createdDatabase.getName())); + } + + @Test + public void roleWithNoPermissions() throws Exception { + final String dbRoleOrphan = testHelper.getUniqueDatabaseRole(); + final String databaseId = testHelper.getUniqueDatabaseId(); + final InstanceId instanceId = testHelper.getInstanceId(); + + final String createTableT = getCreateTableStatement(); + final String createRoleOrphan = String.format("CREATE ROLE %s", dbRoleOrphan); + + final Database createdDatabase = + createAndUpdateDatabase( + testHelper.getOptions().getProjectId(), + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleOrphan)); + + // Connect to db with dbRoleOrphan + SpannerOptions options = + testHelper.getOptions().toBuilder().setDatabaseRole(dbRoleOrphan).build(); + + Spanner spanner = options.getService(); + DatabaseId id = DatabaseId.of(createdDatabase.getName()); + DatabaseClient dbClient = spanner.getDatabaseClient(id); + + // Test SELECT permissions to role dbRoleOrphan on table T. + // Query using dbRoleOrphan should return PermissionDeniedException. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.PERMISSION_DENIED); + assertThat(e.getMessage()).contains(dbRoleOrphan); + } + // Drop role and table. + final String dropTableT = "DROP TABLE T"; + final String dropRoleParent = String.format("DROP ROLE %s", dbRoleOrphan); + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(dropTableT, dropRoleParent)) + .get(5, TimeUnit.MINUTES); + + databasesToDrop.add(DatabaseName.parse(createdDatabase.getName())); + } + + @Test + public void instanceConfigOperations() { + List configs = new ArrayList<>(); + Iterators.addAll( + configs, + instanceAdminClient + .listInstanceConfigs(ProjectName.of(testHelper.getOptions().getProjectId())) + .iterateAll() + .iterator()); + assertThat(configs.isEmpty()).isFalse(); + InstanceConfig config = instanceAdminClient.getInstanceConfig(configs.get(0).getName()); + assertThat(config.getName()).isEqualTo(configs.get(0).getName()); + } + + private Database createAndUpdateDatabase( + String projectId, + final InstanceId instanceId, + final String databaseId, + final List statements) + throws Exception { + if (dialect == DatabaseDialect.POSTGRESQL) { + // DDL statements other than are not allowed in database creation request + // for PostgreSQL-enabled databases. + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setParent(InstanceName.of(projectId, instanceId.getInstance()).toString()) + .setCreateStatement(getCreateDatabaseStatement(databaseId, dialect)) + .setDatabaseDialect(dialect) + .build(); + Database database = + dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(10, TimeUnit.MINUTES); + dbAdminClient.updateDatabaseDdlAsync(database.getName(), statements).get(5, TimeUnit.MINUTES); + return database; + } else { + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setParent(InstanceName.of(projectId, instanceId.getInstance()).toString()) + .setCreateStatement(getCreateDatabaseStatement(databaseId, dialect)) + .setDatabaseDialect(dialect) + .addAllExtraStatements(statements) + .build(); + return dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(10, TimeUnit.MINUTES); + } + } + + private String getCreateTableStatement() { + if (dialect == DatabaseDialect.POSTGRESQL) { + return "CREATE TABLE T (" + " \"K\" VARCHAR PRIMARY KEY" + ")"; + } else { + return "CREATE TABLE T (" + " K STRING(MAX)" + ") PRIMARY KEY (K)"; + } + } + + static String getCreateDatabaseStatement( + final String databaseName, final DatabaseDialect dialect) { + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + return "CREATE DATABASE `" + databaseName + "`"; + } else { + return "CREATE DATABASE \"" + databaseName + "\""; + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java index bbe23aff63..fb139dc89d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java @@ -637,6 +637,37 @@ public void testCustomClientLibToken_alsoContainsDefaultToken() { Objects.requireNonNull(lastSeenHeaders.get(key)).contains("gl-java/")); } + @Test + public void testGetDatabaseAdminStubSettings_whenStubInitialized_assertNonNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, true); + + assertNotNull(rpc.getDatabaseAdminStubSettings()); + + rpc.shutdown(); + } + + @Test + public void testGetInstanceAdminStubSettings_whenStubInitialized_assertNonNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, true); + + assertNotNull(rpc.getInstanceAdminStubSettings()); + + rpc.shutdown(); + } + + @Test + public void testAdminStubSettings_whenStubNotInitialized_assertNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, false); + + assertNull(rpc.getDatabaseAdminStubSettings()); + assertNull(rpc.getInstanceAdminStubSettings()); + + rpc.shutdown(); + } + private SpannerOptions createSpannerOptions() { String endpoint = address.getHostString() + ":" + server.getPort(); return SpannerOptions.newBuilder()