From b49d58dead6873ca907e8299ef5c43f634db8966 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 19 Jul 2022 17:33:24 +0530 Subject: [PATCH] add fgac samples --- .../clirr-ignored-differences.xml | 45 +++++ .../com/google/cloud/spanner/Database.java | 2 +- .../cloud/spanner/DatabaseAdminClient.java | 2 +- .../spanner/DatabaseAdminClientImpl.java | 11 +- .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 10 +- .../cloud/spanner/spi/v1/SpannerRpc.java | 3 +- .../spanner/DatabaseAdminClientImplTest.java | 8 +- .../spanner/DatabaseAdminClientTest.java | 4 +- .../google/cloud/spanner/DatabaseTest.java | 2 +- .../com/example/spanner/AddNewRoleSample.java | 76 +++++++++ .../spanner/EnableFineGrainedAccess.java | 101 +++++++++++ .../com/example/spanner/ListRolesSample.java | 60 +++++++ .../spanner/ReadDataWithRoleSample.java | 65 +++++++ .../com/example/spanner/FGACSampleIT.java | 161 ++++++++++++++++++ 14 files changed, 534 insertions(+), 16 deletions(-) create mode 100644 samples/snippets/src/main/java/com/example/spanner/AddNewRoleSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/EnableFineGrainedAccess.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/ListRolesSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/ReadDataWithRoleSample.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/FGACSampleIT.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index b8aca39a27e..28a4553760d 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -81,4 +81,49 @@ com/google/cloud/spanner/connection/Connection com.google.spanner.v1.ResultSetStats analyzeUpdate(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode) + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.paging.Page listDatabaseRoles(java.lang.String, java.lang.String, com.google.cloud.spanner.Options$ListOption[]) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listDatabaseRoles(java.lang.String, int, java.lang.String) + + + 7004 + com/google/cloud/spanner/DatabaseAdminClient + com.google.cloud.Policy getDatabaseIAMPolicy(java.lang.String, java.lang.String) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.spanner.v1.Session createSession(java.lang.String, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + java.util.List batchCreateSessions(java.lang.String, int, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.spanner.v1.Session createSession(java.lang.String, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + java.util.List batchCreateSessions(java.lang.String, int, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String) + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java index eb3afdca705..2ad44b9e903 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java @@ -145,7 +145,7 @@ public Page listDatabaseOperations() { /** Returns the IAM {@link Policy} for this database. */ public Policy getIAMPolicy() { - return dbClient.getDatabaseIAMPolicy(instance(), database()); + return dbClient.getDatabaseIAMPolicy(instance(), database(), 1); } /** diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java index 60de78e08c0..f1def0186a6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java @@ -495,7 +495,7 @@ OperationFuture updateDatabaseDdl( Operation getOperation(String name); /** Returns the IAM policy for the given database. */ - Policy getDatabaseIAMPolicy(String instanceId, String databaseId); + Policy getDatabaseIAMPolicy(String instanceId, String databaseId, int version); /** * Updates the IAM policy for the given database and returns the resulting policy. It is highly diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java index 06349220de5..73ece214c3a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java @@ -29,6 +29,7 @@ import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.iam.v1.GetPolicyOptions; import com.google.longrunning.Operation; import com.google.protobuf.Empty; import com.google.protobuf.FieldMask; @@ -500,9 +501,13 @@ public Operation getOperation(String name) { } @Override - public Policy getDatabaseIAMPolicy(String instanceId, String databaseId) { + public Policy getDatabaseIAMPolicy(String instanceId, String databaseId, int version) { final String databaseName = DatabaseId.of(projectId, instanceId, databaseId).getName(); - return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName)); + GetPolicyOptions options = null; + if (version > 0) { + options = GetPolicyOptions.newBuilder().setRequestedPolicyVersion(version).build(); + } + return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName, options)); } @Override @@ -524,7 +529,7 @@ public Iterable testDatabaseIAMPermissions( @Override public Policy getBackupIAMPolicy(String instanceId, String backupId) { final String databaseName = BackupId.of(projectId, instanceId, backupId).getName(); - return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName)); + return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName, null)); } @Override 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 0edd6c6f486..a99b3e90a69 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 @@ -89,6 +89,7 @@ import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.iam.v1.GetIamPolicyRequest; +import com.google.iam.v1.GetPolicyOptions; import com.google.iam.v1.Policy; import com.google.iam.v1.SetIamPolicyRequest; import com.google.iam.v1.TestIamPermissionsRequest; @@ -1695,10 +1696,13 @@ public PartitionResponse partitionRead( } @Override - public Policy getDatabaseAdminIAMPolicy(String resource) { + public Policy getDatabaseAdminIAMPolicy(String resource, @Nullable GetPolicyOptions options) { acquireAdministrativeRequestsRateLimiter(); - final GetIamPolicyRequest request = - GetIamPolicyRequest.newBuilder().setResource(resource).build(); + GetIamPolicyRequest.Builder builder = GetIamPolicyRequest.newBuilder().setResource(resource); + if (options != null) { + builder.setOptions(options); + } + final GetIamPolicyRequest request = builder.build(); final GrpcCallContext context = newCallContext(null, resource, request, DatabaseAdminGrpc.getGetIamPolicyMethod()); return runWithRetryOnAdministrativeRequestsExceeded( 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 bb835483a17..189af2fb55c 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,6 +28,7 @@ import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; import com.google.common.collect.ImmutableList; +import com.google.iam.v1.GetPolicyOptions; import com.google.iam.v1.Policy; import com.google.iam.v1.TestIamPermissionsResponse; import com.google.longrunning.Operation; @@ -328,7 +329,7 @@ PartitionResponse partitionRead(PartitionReadRequest request, @Nullable Map operation = + adminClient.updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of( + "CREATE ROLE parent", + "GRANT SELECT ON TABLE Albums TO ROLE parent", + "CREATE ROLE child", + "GRANT ROLE parent TO ROLE child"), + null); + + // Wait for the operation to finish. + // This will throw an ExecutionException if the operation fails. + operation.get(); + System.out.println("Successfully added parent and child roles"); + + // Delete role and membership. + operation = + adminClient.updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of("REVOKE ROLE parent FROM ROLE child", "DROP ROLE child"), + null); + // Wait for the operation to finish. + // This will throw an ExecutionException if the operation fails. + operation.get(); + System.out.println("Successfully deleted chile role"); + } + } +} +// [END spanner_add_new_database_role] diff --git a/samples/snippets/src/main/java/com/example/spanner/EnableFineGrainedAccess.java b/samples/snippets/src/main/java/com/example/spanner/EnableFineGrainedAccess.java new file mode 100644 index 00000000000..d244894244c --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/EnableFineGrainedAccess.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Google Inc. + * + * 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.example.spanner; + +// [START spanner_enable_fine_grained_access] + +import com.google.cloud.Binding; +import com.google.cloud.Condition; +import com.google.cloud.Policy; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import java.util.ArrayList; +import java.util.List; + +public class EnableFineGrainedAccess { + static void enableFineGrainedAccess() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String iamMember = "my-member"; + String databaseRole = "my-role"; + String conditionTitle = "my-title"; + enableFineGrainedAccess( + projectId, instanceId, databaseId, iamMember, databaseRole, conditionTitle); + } + + static void enableFineGrainedAccess( + String projectId, + String instanceId, + String databaseId, + String iamMember, + String databaseRole, + String title) { + try (Spanner spanner = + SpannerOptions.newBuilder() + .setProjectId(projectId) + .build() + .getService()) { + final DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + Policy policy = adminClient.getDatabaseIAMPolicy(instanceId, databaseId, 3); + int policyVersion = policy.getVersion(); + if (policy.getVersion() < 3) { + policyVersion = 3; + } + List members = new ArrayList<>(); + members.add(iamMember); + List bindings = new ArrayList<>(); + bindings.addAll(policy.getBindingsList()); + + bindings.add( + Binding.newBuilder() + .setRole("roles/spanner.fineGrainedAccessUser") + .setMembers(members) + .build()); + + bindings.add( + Binding.newBuilder() + .setRole("roles/spanner.databaseRoleUser") + .setCondition( + Condition.newBuilder() + .setDescription(title) + .setExpression( + String.format( + "resource.name.endsWith(\"/databaseRoles/%s\")", databaseRole)) + .setTitle(title) + .build()) + .setMembers(members) + .build()); + + Policy policyWithConditions = + Policy.newBuilder() + .setVersion(policyVersion) + .setEtag(policy.getEtag()) + .setBindings(bindings) + .build(); + Policy response = + adminClient.setDatabaseIAMPolicy(instanceId, databaseId, policyWithConditions); + System.out.println( + String.format( + "Successfully enabled fined grained access, created policy with version %d", + response.getVersion())); + } + } +} +// [END spanner_enable_fine_grained_access] diff --git a/samples/snippets/src/main/java/com/example/spanner/ListRolesSample.java b/samples/snippets/src/main/java/com/example/spanner/ListRolesSample.java new file mode 100644 index 00000000000..4018f1c87ee --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/ListRolesSample.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Google Inc. + * + * 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.example.spanner; + +// [START spanner_list_database_roles] + +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.DatabaseRole; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import java.util.concurrent.ExecutionException; + +public class ListRolesSample { + static void listRoles() throws InterruptedException, ExecutionException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + listRoles(projectId, instanceId, databaseId); + } + + static void listRoles(String projectId, String instanceId, String databaseId) { + try (Spanner spanner = + SpannerOptions.newBuilder() + .setProjectId(projectId) + .build() + .getService()) { + final DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + String databasePath = DatabaseId.of(projectId, instanceId, databaseId).getName(); + for (DatabaseRole role : adminClient.listDatabaseRoles(instanceId, databaseId).iterateAll()) { + if (!role.getName().startsWith(databasePath + "/databaseRoles/")) { + throw new RuntimeException( + "Role +" + + role.getName() + + "does not have prefix [" + + databasePath + + "/databaseRoles/" + + "]"); + } + System.out.printf("%s%n", role.getName()); + } + } + } +} +// [END spanner_list_database_roles] diff --git a/samples/snippets/src/main/java/com/example/spanner/ReadDataWithRoleSample.java b/samples/snippets/src/main/java/com/example/spanner/ReadDataWithRoleSample.java new file mode 100644 index 00000000000..787bb63faa9 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/ReadDataWithRoleSample.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 Google Inc. + * + * 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.example.spanner; + +// [START spanner_read_data_with_database_role] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; + +public class ReadDataWithRoleSample { + static void readDataWithRole() throws InterruptedException, ExecutionException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String role = "my-role"; + readDataWithRole(projectId, instanceId, databaseId, role); + } + + static void readDataWithRole(String projectId, String instanceId, String databaseId, String role) + throws InterruptedException, ExecutionException { + try (Spanner spanner = + SpannerOptions.newBuilder() + .setProjectId(projectId) + .setDatabaseRole(role) + .build() + .getService()) { + final DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + try (ResultSet resultSet = + dbClient + .singleUse() + .read( + "Albums", + KeySet.all(), // Read all rows in a table. + Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + } + } +} +// [END spanner_read_data_with_database_role] diff --git a/samples/snippets/src/test/java/com/example/spanner/FGACSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/FGACSampleIT.java new file mode 100644 index 00000000000..5e48d5aa1c9 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/FGACSampleIT.java @@ -0,0 +1,161 @@ +/* + * Copyright 202\2 Google Inc. + * + * 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.example.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FGACSampleIT extends SampleTestBase { + + private static DatabaseId databaseId; + + private static final List TEST_SINGERS = + Arrays.asList( + new PgAsyncExamplesIT.Singer(1, "Marc", "Richards"), + new PgAsyncExamplesIT.Singer(2, "Catalina", "Smith"), + new PgAsyncExamplesIT.Singer(3, "Alice", "Trentor"), + new PgAsyncExamplesIT.Singer(4, "Lea", "Martin"), + new PgAsyncExamplesIT.Singer(5, "David", "Lomond")); + private static final List ALBUMS = + Arrays.asList( + new PgAsyncExamplesIT.Album(1, 1, "Total Junk", 300_000L), + new PgAsyncExamplesIT.Album(1, 2, "Go, Go, Go", 400_000L), + new PgAsyncExamplesIT.Album(2, 1, "Green", 150_000L), + new PgAsyncExamplesIT.Album(2, 2, "Forever Hold Your Peace", 350_000L), + new PgAsyncExamplesIT.Album(2, 3, "Terrified", null)); + + private void assertAlbumsOutput(String out) { + assertThat(out).contains("1 1 Total Junk"); + assertThat(out).contains("1 2 Go, Go, Go"); + assertThat(out).contains("2 1 Green"); + assertThat(out).contains("2 2 Forever Hold Your Peace"); + assertThat(out).contains("2 3 Terrified"); + } + + @BeforeClass + public static void createTestDatabase() throws Exception { + final String database = idGenerator.generateDatabaseId(); + databaseId = DatabaseId.of(projectId, instanceId, database); + databaseAdminClient + .createDatabase( + databaseAdminClient.newDatabaseBuilder(databaseId).build(), Collections.emptyList()) + .get(); + databaseAdminClient + .updateDatabaseDdl( + instanceId, + database, + ImmutableList.of( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), + null) + .get(); + } + + @Before + public void insertTestData() { + DatabaseClient client = spanner.getDatabaseClient(databaseId); + ImmutableList.Builder mutations = + ImmutableList.builderWithExpectedSize(TEST_SINGERS.size()); + for (PgAsyncExamplesIT.Singer singer : TEST_SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (PgAsyncExamplesIT.Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .build()); + } + client.write(mutations.build()); + } + + @After + public void removeTestData() { + DatabaseClient client = spanner.getDatabaseClient(databaseId); + client.write(Arrays.asList(Mutation.delete("Singers", KeySet.all()))); + } + + @Test + public void testFGAC() throws Exception { + + // add new role + final String out = + SampleRunner.runSample( + () -> AddNewRoleSample.addNewRole(projectId, instanceId, databaseId.getDatabase())); + assertTrue( + "Expected to create parent and child role." + " Output received was " + out, + out.contains("Successfully added parent and child roles")); + assertTrue( + "Expected to delete child role." + " Output received was " + out, + out.contains("Successfully deleted chile role")); + + // read data with "parent" role + final String readOut = + SampleRunner.runSample( + () -> + ReadDataWithRoleSample.readDataWithRole( + projectId, instanceId, databaseId.getDatabase(), "parent")); + assertAlbumsOutput(readOut); + + // list database roles + final String listRolesOut = + SampleRunner.runSample( + () -> ListRolesSample.listRoles(projectId, instanceId, databaseId.getDatabase())); + assertThat(listRolesOut).contains("parent"); + assertThat(listRolesOut).contains("public"); + assertThat(listRolesOut).contains("spanner_info_reader"); + assertThat(listRolesOut).contains("spanner_sys_reader"); + } +}