From 8034c67af6cfe24e96cc26b1cea51c3405ed98d6 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 22 Aug 2022 15:22:46 +0530 Subject: [PATCH] feat: add support for db roles list (#1916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for db roles * fix sample indentations * fix tests * Fix tests * Delete samples * skip unsupported emulator tests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot --- .../clirr-ignored-differences.xml | 50 ++++++ .../com/google/cloud/spanner/Database.java | 20 ++- .../cloud/spanner/DatabaseAdminClient.java | 23 ++- .../spanner/DatabaseAdminClientImpl.java | 48 +++++- .../google/cloud/spanner/DatabaseRole.java | 78 +++++++++ .../google/cloud/spanner/SessionClient.java | 12 +- .../google/cloud/spanner/SpannerOptions.java | 19 +++ .../connection/AbstractStatementParser.java | 3 +- .../spanner/connection/ConnectionOptions.java | 25 ++- .../cloud/spanner/connection/SpannerPool.java | 5 + .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 56 +++++- .../cloud/spanner/spi/v1/SpannerRpc.java | 12 +- .../spanner/testing/RemoteSpannerHelper.java | 10 ++ .../cloud/spanner/BatchClientImplTest.java | 4 +- .../spanner/DatabaseAdminClientImplTest.java | 71 +++++++- .../spanner/DatabaseAdminClientTest.java | 4 +- .../google/cloud/spanner/DatabaseTest.java | 4 +- .../spanner/MockDatabaseAdminServiceImpl.java | 23 +++ .../cloud/spanner/SessionClientTests.java | 40 ++++- .../google/cloud/spanner/SessionImplTest.java | 5 +- .../spanner/SessionPoolMaintainerTest.java | 1 + .../cloud/spanner/SessionPoolStressTest.java | 1 + .../google/cloud/spanner/SessionPoolTest.java | 2 + .../google/cloud/spanner/SpannerImplTest.java | 5 +- .../spanner/TransactionManagerImplTest.java | 14 +- .../spanner/TransactionRunnerImplTest.java | 7 +- .../cloud/spanner/it/ITDatabaseAdminTest.java | 64 +++++++ .../it/ITDatabaseRolePermissionTest.java | 161 ++++++++++++++++++ synth.metadata | 4 + 29 files changed, 726 insertions(+), 45 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index b8aca39a27..fb952176bd 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -81,4 +81,54 @@ 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/Database + com.google.cloud.Policy getIAMPolicy() + + + 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 eb3afdca70..94c30c9c70 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 @@ -143,9 +143,23 @@ public Page listDatabaseOperations() { instance(), Options.filter(String.format(FILTER_DB_OPERATIONS_TEMPLATE, database()))); } - /** Returns the IAM {@link Policy} for this database. */ - public Policy getIAMPolicy() { - return dbClient.getDatabaseIAMPolicy(instance(), database()); + /** + * Returns the IAM {@link Policy} for this database. + * + *

Version specifies the format used to create the policy, valid values are 0, 1, and 3. + * Requests specifying an invalid value will be rejected. Requests for policies with any + * conditional role bindings must specify version 3. Policies with no conditional role bindings + * may specify any valid value or leave the field unset. + * + *

The policy in the response might use the policy version that you specified, or it might use + * a lower policy version. For example, if you specify version 3, but the policy has no + * conditional role bindings, the response uses version 1. + * + *

To learn which resources support conditions in their IAM policies, see the [IAM + * documentation](https://cloud.google.com/iam/help/conditions/resource-policies). + */ + public Policy getIAMPolicy(int version) { + return dbClient.getDatabaseIAMPolicy(instance(), database(), version); } /** 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 26c48507c4..1363118e3a 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 @@ -339,6 +339,9 @@ OperationFuture restoreDatabase(Restore resto /** Lists long-running database operations on the specified instance. */ Page listDatabaseOperations(String instanceId, ListOption... options); + /** Lists database roles on the specified database. */ + Page listDatabaseRoles(String instanceId, String databaseId, ListOption... options); + /** Lists long-running backup operations on the specified instance. */ Page listBackupOperations(String instanceId, ListOption... options); @@ -491,8 +494,24 @@ OperationFuture updateDatabaseDdl( /** Gets the specified long-running operation. */ Operation getOperation(String name); - /** Returns the IAM policy for the given database. */ - Policy getDatabaseIAMPolicy(String instanceId, String databaseId); + /** + * Returns the IAM policy for the given database. + * + *

Version specifies the format used to create the policy, valid values are 0, 1, and 3. + * Requests specifying an invalid value will be rejected. Requests for policies with any + * conditional role bindings must specify version 3. Policies with no conditional role bindings + * may specify any valid value or leave the field unset. + * + *

The policy in the response might use the policy version that you specified, or it might use + * a lower policy version. For example, if you specify version 3, but the policy has no + * conditional role bindings, the response uses version 1. + * + *

To learn which resources support conditions in their IAM policies, see the + * + * @see IAM + * documentation. + */ + 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 86b0bdbb8d..73ece214c3 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; @@ -290,6 +291,43 @@ public Operation fromProto(Operation proto) { return pageFetcher.getNextPage(); } + @Override + public final Page listDatabaseRoles( + String instanceId, String databaseId, ListOption... options) { + final String databaseName = getDatabaseName(instanceId, databaseId); + final Options listOptions = Options.fromListOptions(options); + final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0; + + PageFetcher pageFetcher = + new PageFetcher() { + @Override + public Paginated getNextPage( + String nextPageToken) { + try { + return rpc.listDatabaseRoles(databaseName, pageSize, nextPageToken); + } catch (SpannerException e) { + throw SpannerExceptionFactory.newSpannerException( + e.getErrorCode(), + String.format( + "Failed to list the databases roles of %s with pageToken %s: %s", + databaseName, + MoreObjects.firstNonNull(nextPageToken, ""), + e.getMessage()), + e); + } + } + + @Override + public DatabaseRole fromProto(com.google.spanner.admin.database.v1.DatabaseRole proto) { + return DatabaseRole.fromProto(proto); + } + }; + if (listOptions.hasPageToken()) { + pageFetcher.setNextPageToken(listOptions.pageToken()); + } + return pageFetcher.getNextPage(); + } + @Override public Page listBackups(String instanceId, ListOption... options) { final String instanceName = getInstanceName(instanceId); @@ -463,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 @@ -487,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/DatabaseRole.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java new file mode 100644 index 0000000000..37daf1cced --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 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; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.Objects; + +/** A Cloud Spanner database role. */ +public class DatabaseRole { + + public static class Builder { + + private final String name; + + public Builder(String name) { + this.name = Preconditions.checkNotNull(name); + } + + public DatabaseRole build() { + return new DatabaseRole(this.name); + } + } + + private final String name; + + @VisibleForTesting + DatabaseRole(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(o.getClass())) { + return false; + } + DatabaseRole databaseRole = (DatabaseRole) o; + return Objects.equals(name, databaseRole.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return String.format("DatabaseRole[%s]", name); + } + + static DatabaseRole fromProto(com.google.spanner.admin.database.v1.DatabaseRole proto) { + checkArgument(!proto.getName().isEmpty(), "Missing expected 'name' field"); + return new DatabaseRole.Builder(proto.getName()).build(); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java index cc9681b44d..474d99671e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java @@ -211,7 +211,11 @@ SessionImpl createSession() { com.google.spanner.v1.Session session = spanner .getRpc() - .createSession(db.getName(), spanner.getOptions().getSessionLabels(), options); + .createSession( + db.getName(), + spanner.getOptions().getDatabaseRole(), + spanner.getOptions().getSessionLabels(), + options); return new SessionImpl(spanner, session.getName(), options); } catch (RuntimeException e) { TraceUtil.setWithFailure(span, e); @@ -297,7 +301,11 @@ private List internalBatchCreateSessions( spanner .getRpc() .batchCreateSessions( - db.getName(), sessionCount, spanner.getOptions().getSessionLabels(), options); + db.getName(), + sessionCount, + spanner.getOptions().getDatabaseRole(), + spanner.getOptions().getSessionLabels(), + options); span.addAnnotation( String.format( "Request for %d sessions returned %d sessions", sessionCount, sessions.size())); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index b62b2d4455..b117750654 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -99,6 +99,7 @@ public class SpannerOptions extends ServiceOptions { private final int prefetchChunks; private final int numChannels; private final String transportChannelExecutorThreadNameFormat; + private final String databaseRole; private final ImmutableMap sessionLabels; private final SpannerStubSettings spannerStubSettings; private final InstanceAdminStubSettings instanceAdminStubSettings; @@ -564,6 +565,7 @@ private SpannerOptions(Builder builder) { ? builder.sessionPoolOptions : SessionPoolOptions.newBuilder().build(); prefetchChunks = builder.prefetchChunks; + databaseRole = builder.databaseRole; sessionLabels = builder.sessionLabels; try { spannerStubSettings = builder.spannerStubSettingsBuilder.build(); @@ -674,6 +676,7 @@ public static class Builder private int prefetchChunks = DEFAULT_PREFETCH_CHUNKS; private SessionPoolOptions sessionPoolOptions; + private String databaseRole; private ImmutableMap sessionLabels; private SpannerStubSettings.Builder spannerStubSettingsBuilder = SpannerStubSettings.newBuilder(); @@ -730,6 +733,7 @@ private Builder() { options.transportChannelExecutorThreadNameFormat; this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; + this.databaseRole = options.databaseRole; this.sessionLabels = options.sessionLabels; this.spannerStubSettingsBuilder = options.spannerStubSettings.toBuilder(); this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); @@ -830,6 +834,17 @@ public Builder setSessionPoolOption(SessionPoolOptions sessionPoolOptions) { return this; } + /** + * Sets the database role that should be used for connections that are created by this instance. + * The database role that is used determines the access permissions that a connection has. This + * can for example be used to create connections that are only permitted to access certain + * tables. + */ + public Builder setDatabaseRole(String databaseRole) { + this.databaseRole = databaseRole; + return this; + } + /** * Sets the labels to add to all Sessions created in this client. * @@ -1215,6 +1230,10 @@ public SessionPoolOptions getSessionPoolOptions() { return sessionPoolOptions; } + public String getDatabaseRole() { + return databaseRole; + } + public Map getSessionLabels() { return sessionLabels; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java index b2b94c0213..fb272e913e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java @@ -320,7 +320,8 @@ ClientSideStatement getClientSideStatement() { } } - static final Set ddlStatements = ImmutableSet.of("CREATE", "DROP", "ALTER", "ANALYZE"); + static final Set ddlStatements = + ImmutableSet.of("CREATE", "DROP", "ALTER", "ANALYZE", "GRANT", "REVOKE"); static final Set selectStatements = ImmutableSet.of("SELECT", "WITH", "SHOW"); static final Set dmlStatements = ImmutableSet.of("INSERT", "UPDATE", "DELETE"); private final Set statements; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 7923156da1..e44a99c1ec 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -165,6 +165,7 @@ public String[] getValidValues() { private static final String DEFAULT_MAX_SESSIONS = null; private static final String DEFAULT_NUM_CHANNELS = null; private static final String DEFAULT_CHANNEL_PROVIDER = null; + private static final String DEFAULT_DATABASE_ROLE = null; private static final String DEFAULT_USER_AGENT = null; private static final String DEFAULT_OPTIMIZER_VERSION = ""; private static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; @@ -215,7 +216,8 @@ public String[] getValidValues() { public static final String RPC_PRIORITY_NAME = "rpcPriority"; /** Dialect to use for a connection. */ private static final String DIALECT_PROPERTY_NAME = "dialect"; - + /** Name of the 'databaseRole' connection property. */ + public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole"; /** All valid connection properties. */ public static final Set VALID_PROPERTIES = Collections.unmodifiableSet( @@ -282,7 +284,10 @@ public String[] getValidValues() { RPC_PRIORITY_NAME, "Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH."), ConnectionProperty.createStringProperty( - DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection.")))); + DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."), + ConnectionProperty.createStringProperty( + DATABASE_ROLE_PROPERTY_NAME, + "Sets the database role to use for this connection. The default is privileges assigned to IAM role")))); private static final Set INTERNAL_PROPERTIES = Collections.unmodifiableSet( @@ -533,6 +538,7 @@ public static Builder newBuilder() { private final String channelProvider; private final Integer minSessions; private final Integer maxSessions; + private final String databaseRole; private final String userAgent; private final QueryOptions queryOptions; private final boolean returnCommitStats; @@ -620,6 +626,7 @@ private ConnectionOptions(Builder builder) { this.numChannels = parseIntegerProperty(NUM_CHANNELS_PROPERTY_NAME, parseNumChannels(builder.uri)); this.channelProvider = parseChannelProvider(builder.uri); + this.databaseRole = parseDatabaseRole(this.uri); String projectId = matcher.group(Builder.PROJECT_GROUP); if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { @@ -790,6 +797,12 @@ static String parseChannelProvider(String uri) { return value != null ? value : DEFAULT_CHANNEL_PROVIDER; } + @VisibleForTesting + static String parseDatabaseRole(String uri) { + String value = parseUriProperty(uri, DATABASE_ROLE_PROPERTY_NAME); + return value != null ? value : DEFAULT_DATABASE_ROLE; + } + @VisibleForTesting static String parseUserAgent(String uri) { String value = parseUriProperty(uri, USER_AGENT_PROPERTY_NAME); @@ -959,6 +972,14 @@ public TransportChannelProvider getChannelProvider() { } } + /** + * The database role that is used for this connection. Assigning a role to a connection can be + * used to for example restrict the access of a connection to a specific set of tables. + */ + public String getDatabaseRole() { + return databaseRole; + } + /** The host and port number that this {@link ConnectionOptions} will connect to */ public String getHost() { return host; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index f94e27d963..6a57779020 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -153,6 +153,7 @@ static class SpannerPoolKey { private final Integer numChannels; private final boolean usePlainText; private final String userAgent; + private final String databaseRole; @VisibleForTesting static SpannerPoolKey of(ConnectionOptions options) { @@ -170,6 +171,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException { this.host = options.getHost(); this.projectId = options.getProjectId(); this.credentialsKey = CredentialsKey.create(options); + this.databaseRole = options.getDatabaseRole(); this.sessionPoolOptions = options.getSessionPoolOptions() == null ? SessionPoolOptions.newBuilder().build() @@ -190,6 +192,7 @@ public boolean equals(Object o) { && Objects.equals(this.credentialsKey, other.credentialsKey) && Objects.equals(this.sessionPoolOptions, other.sessionPoolOptions) && Objects.equals(this.numChannels, other.numChannels) + && Objects.equals(this.databaseRole, other.databaseRole) && Objects.equals(this.usePlainText, other.usePlainText) && Objects.equals(this.userAgent, other.userAgent); } @@ -203,6 +206,7 @@ public int hashCode() { this.sessionPoolOptions, this.numChannels, this.usePlainText, + this.databaseRole, this.userAgent); } } @@ -329,6 +333,7 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, CONNECTION_API_CLIENT_LIB_TOKEN)) .setHost(key.host) .setProjectId(key.projectId) + .setDatabaseRole(options.getDatabaseRole()) .setCredentials(options.getCredentials()); builder.setSessionPoolOption(key.sessionPoolOptions); if (key.numChannels != null) { 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 3a20c2a3c7..937da7e8d2 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; @@ -111,6 +112,7 @@ import com.google.spanner.admin.database.v1.CreateDatabaseRequest; import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.DatabaseAdminGrpc; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.DeleteBackupRequest; import com.google.spanner.admin.database.v1.DropDatabaseRequest; import com.google.spanner.admin.database.v1.GetBackupRequest; @@ -122,6 +124,8 @@ import com.google.spanner.admin.database.v1.ListBackupsResponse; import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest; import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse; +import com.google.spanner.admin.database.v1.ListDatabaseRolesRequest; +import com.google.spanner.admin.database.v1.ListDatabaseRolesResponse; import com.google.spanner.admin.database.v1.ListDatabasesRequest; import com.google.spanner.admin.database.v1.ListDatabasesResponse; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; @@ -1002,6 +1006,27 @@ public Paginated listDatabaseOperations( return new Paginated<>(response.getOperationsList(), response.getNextPageToken()); } + @Override + public Paginated listDatabaseRoles( + String databaseName, int pageSize, @Nullable String pageToken) { + acquireAdministrativeRequestsRateLimiter(); + ListDatabaseRolesRequest.Builder requestBuilder = + ListDatabaseRolesRequest.newBuilder().setParent(databaseName).setPageSize(pageSize); + + if (pageToken != null) { + requestBuilder.setPageToken(pageToken); + } + final ListDatabaseRolesRequest request = requestBuilder.build(); + + final GrpcCallContext context = + newCallContext(null, databaseName, request, DatabaseAdminGrpc.getListDatabaseRolesMethod()); + ListDatabaseRolesResponse response = + runWithRetryOnAdministrativeRequestsExceeded( + () -> get(databaseAdminStub.listDatabaseRolesCallable().futureCall(request, context))); + + return new Paginated<>(response.getDatabaseRolesList(), response.getNextPageToken()); + } + @Override public Paginated listBackups( String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken) @@ -1450,6 +1475,7 @@ public void cancelOperation(String name) throws SpannerException { public List batchCreateSessions( String databaseName, int sessionCount, + @Nullable String databaseRole, @Nullable Map labels, @Nullable Map options) throws SpannerException { @@ -1457,10 +1483,14 @@ public List batchCreateSessions( BatchCreateSessionsRequest.newBuilder() .setDatabase(databaseName) .setSessionCount(sessionCount); + Session.Builder sessionBuilder = Session.newBuilder(); if (labels != null && !labels.isEmpty()) { - Session.Builder session = Session.newBuilder().putAllLabels(labels); - requestBuilder.setSessionTemplate(session); + sessionBuilder.putAllLabels(labels); + } + if (databaseRole != null && !databaseRole.isEmpty()) { + sessionBuilder.setCreatorRole(databaseRole); } + requestBuilder.setSessionTemplate(sessionBuilder); BatchCreateSessionsRequest request = requestBuilder.build(); GrpcCallContext context = newCallContext(options, databaseName, request, SpannerGrpc.getBatchCreateSessionsMethod()); @@ -1470,14 +1500,21 @@ public List batchCreateSessions( @Override public Session createSession( - String databaseName, @Nullable Map labels, @Nullable Map options) + String databaseName, + @Nullable String databaseRole, + @Nullable Map labels, + @Nullable Map options) throws SpannerException { CreateSessionRequest.Builder requestBuilder = CreateSessionRequest.newBuilder().setDatabase(databaseName); + Session.Builder sessionBuilder = Session.newBuilder(); if (labels != null && !labels.isEmpty()) { - Session.Builder session = Session.newBuilder().putAllLabels(labels); - requestBuilder.setSession(session); + sessionBuilder.putAllLabels(labels); } + if (databaseRole != null && !databaseRole.isEmpty()) { + sessionBuilder.setCreatorRole(databaseRole); + } + requestBuilder.setSession(sessionBuilder); CreateSessionRequest request = requestBuilder.build(); GrpcCallContext context = newCallContext(options, databaseName, request, SpannerGrpc.getCreateSessionMethod()); @@ -1658,10 +1695,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 00382e228f..189af2fb55 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; @@ -253,6 +254,9 @@ Paginated listBackupOperations( Paginated listDatabaseOperations( String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken); + Paginated listDatabaseRoles( + String databaseName, int pageSize, @Nullable String pageToken); + /** Retrieves a long running operation. */ Operation getOperation(String name) throws SpannerException; @@ -262,12 +266,16 @@ Paginated listDatabaseOperations( List batchCreateSessions( String databaseName, int sessionCount, + @Nullable String databaseRole, @Nullable Map labels, @Nullable Map options) throws SpannerException; Session createSession( - String databaseName, @Nullable Map labels, @Nullable Map options) + String databaseName, + @Nullable String databaseRole, + @Nullable Map labels, + @Nullable Map options) throws SpannerException; void deleteSession(String sessionName, @Nullable Map options) throws SpannerException; @@ -321,7 +329,7 @@ PartitionResponse partitionRead(PartitionReadRequest request, @Nullable Map dbs = new ArrayList<>(); @@ -107,6 +109,14 @@ public String getUniqueDatabaseId() { return String.format("testdb_%d_%04d", dbPrefix, dbSeq.incrementAndGet()); } + /** + * Returns a database role name which is guaranteed to be unique within the context of this + * environment. + */ + public String getUniqueDatabaseRole() { + return String.format("testdbrole_%d_%04d", dbRolePrefix, dbRoleSeq.incrementAndGet()); + } + /** * Returns a backup id which is guaranteed to be unique within the context of this environment. */ diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java index 91fbac83e6..211f3969cc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.anyMap; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -66,6 +67,7 @@ public void setUp() { initMocks(this); DatabaseId db = DatabaseId.of(DB_NAME); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getPrefetchChunks()).thenReturn(1); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); @@ -83,7 +85,7 @@ public void setUp() { @Test public void testBatchReadOnlyTxnWithBound() throws Exception { Session sessionProto = Session.newBuilder().setName(SESSION_NAME).build(); - when(gapicRpc.createSession(eq(DB_NAME), anyMap(), optionsCaptor.capture())) + when(gapicRpc.createSession(eq(DB_NAME), anyString(), anyMap(), optionsCaptor.capture())) .thenReturn(sessionProto); com.google.protobuf.Timestamp timestamp = Timestamps.parse(TIMESTAMP); Transaction txnMetadata = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java index 56dfd70730..0255f6c166 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java @@ -49,9 +49,11 @@ import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.EncryptionInfo; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -74,6 +76,8 @@ public class DatabaseAdminClientImplTest { "projects/my-project/instances/my-instance/databases/my-db2"; private static final String BK_ID = "my-bk"; private static final String SOURCE_BK = "my-source-bk"; + private static final String DB_ROLE = "dummy-role"; + private static final String DB_ROLE2 = "dummy-role-2"; private static final String BK_NAME = "projects/my-project/instances/my-instance/backups/my-bk"; private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2"; private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now(); @@ -102,6 +106,14 @@ private Database getDatabaseProto() { .build(); } + private DatabaseRole getDatabaseRoleProto() { + return DatabaseRole.newBuilder().setName(DB_ROLE).build(); + } + + private DatabaseRole getAnotherDatabaseRoleProto() { + return DatabaseRole.newBuilder().setName(DB_ROLE2).build(); + } + private Database getEncryptedDatabaseProto() { return getDatabaseProto() .toBuilder() @@ -293,9 +305,60 @@ public void listDatabaseErrorWithToken() { assertThat(e.getMessage()).contains(String.format("with pageToken %s", pageToken)); } + @Test + public void listDatabaseRoles() { + String pageToken = "token"; + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenReturn(new Paginated<>(ImmutableList.of(getDatabaseRoleProto()), pageToken)); + when(rpc.listDatabaseRoles(DB_NAME, 1, pageToken)) + .thenReturn(new Paginated<>(ImmutableList.of(getAnotherDatabaseRoleProto()), "")); + + ArrayList databaseRoles = + Lists.newArrayList( + client.listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1)).iterateAll()); + assertThat(databaseRoles.get(0).getName()).isEqualTo(DB_ROLE); + assertThat(databaseRoles.get(1).getName()).isEqualTo(DB_ROLE2); + assertThat(databaseRoles.size()).isEqualTo(2); + } + + @Test + public void listDatabaseRolesError() { + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenThrow( + SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error")); + SpannerException e = + assertThrows( + SpannerException.class, + () -> client.listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1))); + assertThat(e.getMessage()).contains(INSTANCE_NAME); + // Assert that the call was done without a page token. + assertThat(e.getMessage()).contains("with pageToken "); + } + + @Test + public void listDatabaseRolesErrorWithToken() { + String pageToken = "token"; + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenReturn(new Paginated<>(ImmutableList.of(getDatabaseRoleProto()), pageToken)); + when(rpc.listDatabaseRoles(DB_NAME, 1, pageToken)) + .thenThrow( + SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error")); + SpannerException e = + assertThrows( + SpannerException.class, + () -> + Lists.newArrayList( + client + .listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1)) + .iterateAll())); + assertThat(e.getMessage()).contains(INSTANCE_NAME); + // Assert that the call was done without a page token. + assertThat(e.getMessage()).contains(String.format("with pageToken %s", pageToken)); + } + @Test public void getDatabaseIAMPolicy() { - when(rpc.getDatabaseAdminIAMPolicy(DB_NAME)) + when(rpc.getDatabaseAdminIAMPolicy(DB_NAME, null)) .thenReturn( Policy.newBuilder() .addBindings( @@ -304,11 +367,11 @@ public void getDatabaseIAMPolicy() { .setRole("roles/viewer") .build()) .build()); - com.google.cloud.Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + com.google.cloud.Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 0); assertThat(policy.getBindings()) .containsExactly(Role.viewer(), Sets.newHashSet(Identity.user("joe@example.com"))); - when(rpc.getDatabaseAdminIAMPolicy(DB_NAME)) + when(rpc.getDatabaseAdminIAMPolicy(DB_NAME, null)) .thenReturn( Policy.newBuilder() .addBindings( @@ -317,7 +380,7 @@ public void getDatabaseIAMPolicy() { .setRole("roles/viewer") .build()) .build()); - policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 0); assertThat(policy.getBindings()) .containsExactly( Role.viewer(), diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java index 9dcadc85ad..eee9f520a7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java @@ -754,13 +754,13 @@ public void backupListBackupOperations() @Test public void getAndSetIAMPolicy() { - Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 1); assertThat(policy).isEqualTo(Policy.newBuilder().build()); Policy newPolicy = Policy.newBuilder().addIdentity(Role.editor(), Identity.user("joe@example.com")).build(); Policy returnedPolicy = client.setDatabaseIAMPolicy(INSTANCE_ID, DB_ID, newPolicy); assertThat(returnedPolicy).isEqualTo(newPolicy); - assertThat(client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID)).isEqualTo(newPolicy); + assertThat(client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 1)).isEqualTo(newPolicy); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java index bd09d131a6..1eb2794c8e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java @@ -183,8 +183,8 @@ public void getIAMPolicy() { Database database = new Database( DatabaseId.of("test-project", "test-instance", "test-database"), State.READY, dbClient); - database.getIAMPolicy(); - verify(dbClient).getDatabaseIAMPolicy("test-instance", "test-database"); + database.getIAMPolicy(1); + verify(dbClient).getDatabaseIAMPolicy("test-instance", "test-database", 1); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java index 6b248f7902..4165f168d8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java @@ -40,6 +40,7 @@ import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.Database.State; import com.google.spanner.admin.database.v1.DatabaseAdminGrpc.DatabaseAdminImplBase; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.DeleteBackupRequest; import com.google.spanner.admin.database.v1.DropDatabaseRequest; import com.google.spanner.admin.database.v1.GetBackupRequest; @@ -52,6 +53,8 @@ import com.google.spanner.admin.database.v1.ListBackupsResponse; import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest; import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse; +import com.google.spanner.admin.database.v1.ListDatabaseRolesRequest; +import com.google.spanner.admin.database.v1.ListDatabaseRolesResponse; import com.google.spanner.admin.database.v1.ListDatabasesRequest; import com.google.spanner.admin.database.v1.ListDatabasesResponse; import com.google.spanner.admin.database.v1.OperationProgress; @@ -436,6 +439,7 @@ private com.google.rpc.Status fromException(Exception e) { private final ConcurrentMap databases = new ConcurrentHashMap<>(); private final ConcurrentMap backups = new ConcurrentHashMap<>(); private final ConcurrentMap> filterMatches = new ConcurrentHashMap<>(); + private final List databaseRoles = new ArrayList<>(); private final MockOperationsServiceImpl operations; private long createBackupOperationExecutionTime; @@ -581,6 +585,25 @@ public void listDatabaseOperations( } } + @Override + public void listDatabaseRoles( + ListDatabaseRolesRequest request, + StreamObserver responseObserver) { + requests.add(request); + List dbRoles = + new ArrayList<>(databaseRoles.size()); + for (DatabaseRole entry : databaseRoles) { + dbRoles.add( + com.google.spanner.admin.database.v1.DatabaseRole.newBuilder() + .setName(entry.getName()) + .build()); + } + + responseObserver.onNext( + ListDatabaseRolesResponse.newBuilder().addAllDatabaseRoles(dbRoles).build()); + responseObserver.onCompleted(); + } + private boolean matchesFilter(Object obj, String filter) throws Exception { if (!Strings.isNullOrEmpty(filter)) { Set matches = filterMatches.get(filter); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java index 6d1f88b26f..8af1323238 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java @@ -105,6 +105,7 @@ public ScheduledExecutorService get() { when(spannerOptions.getTransportOptions()).thenReturn(transportOptions); when(spannerOptions.getNumChannels()).thenReturn(numChannels); when(spannerOptions.getPrefetchChunks()).thenReturn(1); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); when(spanner.getOptions()).thenReturn(spannerOptions); @@ -117,13 +118,16 @@ public void createAndCloseSession() { String sessionName = dbName + "/sessions/s1"; Map labels = new HashMap<>(); labels.put("env", "dev"); + String databaseRole = "role"; when(spannerOptions.getSessionLabels()).thenReturn(labels); + when(spannerOptions.getDatabaseRole()).thenReturn(databaseRole); com.google.spanner.v1.Session sessionProto = com.google.spanner.v1.Session.newBuilder() .setName(sessionName) .putAllLabels(labels) .build(); - when(rpc.createSession(Mockito.eq(dbName), Mockito.eq(labels), options.capture())) + when(rpc.createSession( + Mockito.eq(dbName), Mockito.eq(databaseRole), Mockito.eq(labels), options.capture())) .thenReturn(sessionProto); try (SessionClient client = new SessionClient(spanner, db, new TestExecutorFactory())) { @@ -144,12 +148,18 @@ public void batchCreateAndCloseSessions() { final Map labels = new HashMap<>(); labels.put("env", "dev"); when(spannerOptions.getSessionLabels()).thenReturn(labels); + String databaseRole = new String("role"); + when(spannerOptions.getDatabaseRole()).thenReturn(databaseRole); final List usedChannels = Collections.synchronizedList(new ArrayList<>()); when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.eq(labels), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.eq(databaseRole), + Mockito.eq(labels), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); usedChannels.add(channelHint); int sessionCount = invocation.getArgument(1, Integer.class); @@ -204,12 +214,17 @@ public void batchCreateSessionsDistributesMultipleRequestsOverChannels() { final String sessionName = dbName + "/sessions/s%d"; final Map labels = Collections.emptyMap(); when(spannerOptions.getSessionLabels()).thenReturn(labels); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); final Set usedChannelHints = Collections.synchronizedSet(new HashSet<>()); when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.eq(labels), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.eq(labels), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); usedChannelHints.add(channelHint); int sessionCount = invocation.getArgument(1, Integer.class); @@ -219,6 +234,7 @@ public void batchCreateSessionsDistributesMultipleRequestsOverChannels() { com.google.spanner.v1.Session.newBuilder() .setName(String.format(sessionName, i)) .putAllLabels(labels) + .setCreatorRole("role") .build()); } return res; @@ -287,10 +303,14 @@ public void batchCreateSessionsWithExceptions() { DatabaseId db = DatabaseId.of(dbName); final String sessionName = dbName + "/sessions/s%d"; when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.anyMap(), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); if (errorOnChannels.contains(channelHint)) { throw SpannerExceptionFactory.newSpannerException( @@ -351,7 +371,11 @@ public void batchCreateSessionsServerReturnsLessSessionsPerBatch() { DatabaseId db = DatabaseId.of(dbName); final String sessionName = dbName + "/sessions/s%d"; when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.anyMap(), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .then( invocation -> { int sessionCount = invocation.getArgument(1, Integer.class); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java index cf4e6dda81..1174cbf4eb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java @@ -78,6 +78,7 @@ public void setUp() { MockitoAnnotations.initMocks(this); when(spannerOptions.getNumChannels()).thenReturn(4); when(spannerOptions.getPrefetchChunks()).thenReturn(1); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); when(spannerOptions.getSessionLabels()).thenReturn(Collections.emptyMap()); @@ -92,7 +93,9 @@ public void setUp() { DatabaseId db = DatabaseId.of(dbName); Session sessionProto = Session.newBuilder().setName(sessionName).build(); - Mockito.when(rpc.createSession(Mockito.eq(dbName), Mockito.anyMap(), optionsCaptor.capture())) + Mockito.when( + rpc.createSession( + Mockito.eq(dbName), Mockito.anyString(), Mockito.anyMap(), optionsCaptor.capture())) .thenReturn(sessionProto); Transaction txn = Transaction.newBuilder().setId(ByteString.copyFromUtf8("TEST")).build(); Mockito.when( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index 160e32d084..ab6a51c926 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -59,6 +59,7 @@ public void setUp() { when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); setupMockSessionCreation(); options = SessionPoolOptions.newBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index ada30d0c8e..9a1df1c964 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -89,6 +89,7 @@ private void setupSpanner(DatabaseId db) { mockSpanner = mock(SpannerImpl.class); spannerOptions = mock(SpannerOptions.class); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); SessionClient sessionClient = mock(SessionClient.class); when(mockSpanner.getSessionClient(db)).thenReturn(sessionClient); when(mockSpanner.getOptions()).thenReturn(spannerOptions); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index 4efe2cfef3..3f018e382d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -140,6 +140,7 @@ public void setUp() { when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); options = SessionPoolOptions.newBuilder() .setMinSessions(minSessions) @@ -847,6 +848,7 @@ public void testSessionNotFoundReadWriteTransaction() { SpannerOptions spannerOptions = mock(SpannerOptions.class); when(spannerOptions.getSessionPoolOptions()).thenReturn(options); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spanner.getOptions()).thenReturn(spannerOptions); SessionPool pool = SessionPool.createPool(options, new TestExecutorFactory(), spanner.getSessionClient(db)); 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 0cc0e6e9fb..2d098d9700 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 @@ -61,6 +61,7 @@ public class SpannerImplTest { public void setUp() { MockitoAnnotations.initMocks(this); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getPrefetchChunks()).thenReturn(1); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); @@ -78,6 +79,7 @@ public void getDbclientAgainGivesSame() { Map labels = new HashMap<>(); labels.put("env", "dev"); Mockito.when(spannerOptions.getSessionLabels()).thenReturn(labels); + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); String dbName = "projects/p1/instances/i1/databases/d1"; DatabaseId db = DatabaseId.of(dbName); @@ -160,7 +162,7 @@ public void getDbclientAfterCloseThrows() { .thenReturn(GrpcTransportOptions.newBuilder().build()); Mockito.when(spannerOptions.getSessionPoolOptions()) .thenReturn(SessionPoolOptions.newBuilder().build()); - + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); imp.close(); IllegalStateException e = @@ -210,6 +212,7 @@ public void testClientId() { .thenReturn(GrpcTransportOptions.newBuilder().build()); Mockito.when(spannerOptions.getSessionPoolOptions()) .thenReturn(SessionPoolOptions.newBuilder().setMinSessions(0).build()); + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); DatabaseClientImpl databaseClient = (DatabaseClientImpl) impl.getDatabaseClient(db); assertThat(databaseClient.clientId).isEqualTo("client-1"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 9ca2c77f62..55df44a96d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -198,11 +198,16 @@ public void usesPreparedTransaction() { SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDatabaseRole()).thenReturn("role"); SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( @@ -257,8 +262,13 @@ public void inlineBegin() { SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(options.getDatabaseRole()).thenReturn("role"); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index 95595607bc..04ac46d887 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -142,11 +142,16 @@ public void usesPreparedTransaction() { SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDatabaseRole()).thenReturn("role"); SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java index bc9a075e0c..e19272a076 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java @@ -16,15 +16,18 @@ 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.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.paging.Page; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseRole; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.IntegrationTestEnv; @@ -205,4 +208,65 @@ public void listPagination() throws Exception { } assertThat(dbIdsGot).containsAtLeastElementsIn(dbIds); } + + @Test + public void createAndListDatabaseRoles() throws Exception { + assumeFalse("Emulator does not support create & list database roles", isUsingEmulator()); + List dbRoles = + ImmutableList.of( + testHelper.getUniqueDatabaseRole(), + testHelper.getUniqueDatabaseRole(), + testHelper.getUniqueDatabaseRole()); + + String instanceId = testHelper.getInstanceId().getInstance(); + Database database = + dbAdminClient + .createDatabase(instanceId, testHelper.getUniqueDatabaseId(), ImmutableList.of()) + .get(); + + // Create the roles in Db. + List dbRolesCreateStatements = new ArrayList<>(); + for (String dbRole : dbRoles) { + dbRolesCreateStatements.add(String.format("CREATE ROLE %s", dbRole)); + } + dbAdminClient + .updateDatabaseDdl( + instanceId, database.getId().getDatabase(), dbRolesCreateStatements, null) + .get(); + + // List roles from Db. + Page page = + dbAdminClient.listDatabaseRoles(instanceId, database.getId().getDatabase()); + List dbRolesGot = new ArrayList<>(); + while (page != null && page.getValues().iterator().hasNext()) { + for (DatabaseRole value : page.getValues()) { + String[] split = value.getName().split("/"); + dbRolesGot.add(split[split.length - 1]); + } + page = page.getNextPage(); + } + assertThat(dbRolesGot).containsAtLeastElementsIn(dbRoles); + + // Delete the created roles. + List dbRolesDropStatements = new ArrayList<>(); + for (String dbRole : dbRoles) { + dbRolesDropStatements.add(String.format("DROP ROLE %s", dbRole)); + } + dbAdminClient + .updateDatabaseDdl(instanceId, database.getId().getDatabase(), dbRolesDropStatements, null) + .get(); + + // List roles from Db. Deleted roles should not be present in list. + Page pageRemainingRoles = + dbAdminClient.listDatabaseRoles(instanceId, database.getId().getDatabase()); + List dbRolesRemaining = new ArrayList<>(); + while (pageRemainingRoles != null && pageRemainingRoles.getValues().iterator().hasNext()) { + for (DatabaseRole value : pageRemainingRoles.getValues()) { + String[] split = value.getName().split("/"); + dbRolesRemaining.add(split[split.length - 1]); + } + pageRemainingRoles = pageRemainingRoles.getNextPage(); + } + assertThat(dbRolesRemaining).containsNoneIn(dbRoles); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java new file mode 100644 index 0000000000..842df028dd --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2022 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.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.ErrorCode; +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.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for Role Permissions using {@link com.google.cloud.spanner.DatabaseRole}. */ +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITDatabaseRolePermissionTest { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private DatabaseAdminClient dbAdminClient; + private RemoteSpannerHelper testHelper; + + @Before + public void setUp() { + assumeFalse("Emulator does not support database roles", isUsingEmulator()); + testHelper = env.getTestHelper(); + dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); + } + + @Test + public void grantAndRevokeDatabaseRolePermissions() throws Exception { + // Create database with table and role permission. + final String dbRoleParent = "parent"; + final String databaseId = testHelper.getUniqueDatabaseId(); + final String instanceId = testHelper.getInstanceId().getInstance(); + + final String createTableT = "CREATE TABLE T (\n" + " K STRING(MAX),\n" + ") PRIMARY KEY(K)"; + final String createRoleParent = String.format("CREATE ROLE %s", dbRoleParent); + final String grantSelectOnTableToParent = + String.format("GRANT SELECT ON TABLE T TO ROLE %s", dbRoleParent); + + final Database createdDatabase = + dbAdminClient + .createDatabase( + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleParent, grantSelectOnTableToParent)) + .get(5, TimeUnit.MINUTES); + + // Connect to db with dbRoleParent. + SpannerOptions options = SpannerOptions.newBuilder().setDatabaseRole(dbRoleParent).build(); + + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + + // 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()); + } catch (PermissionDeniedException e) { + // This is not expected + fail("Got PermissionDeniedException when it should not have occurred."); + } + + // Revoke select Permission for dbRoleParent. + final String revokeSelectOnTableFromParent = + String.format("REVOKE SELECT ON TABLE T FROM ROLE %s", dbRoleParent); + + dbAdminClient + .updateDatabaseDdl( + instanceId, databaseId, Arrays.asList(revokeSelectOnTableFromParent), null) + .get(); + + // 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 + .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) + .get(); + } + + @Test + public void roleWithNoPermissions() throws Exception { + final String dbRoleOrphan = testHelper.getUniqueDatabaseRole(); + final String databaseId = testHelper.getUniqueDatabaseId(); + final String instanceId = testHelper.getInstanceId().getInstance(); + + final String createTableT = "CREATE TABLE T (\n" + " K STRING(MAX),\n" + ") PRIMARY KEY(K)"; + final String createRoleOrphan = String.format("CREATE ROLE %s", dbRoleOrphan); + + final Database createdDatabase = + dbAdminClient + .createDatabase( + instanceId, databaseId, ImmutableList.of(createTableT, createRoleOrphan)) + .get(5, TimeUnit.MINUTES); + + // Connect to db with dbRoleOrphan + SpannerOptions options = SpannerOptions.newBuilder().setDatabaseRole(dbRoleOrphan).build(); + + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + + // 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 + .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) + .get(); + } +} diff --git a/synth.metadata b/synth.metadata index 6e6fa5d805..099e30c368 100644 --- a/synth.metadata +++ b/synth.metadata @@ -201,6 +201,10 @@ "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesRequestOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesResponse.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesResponseOrBuilder.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesRequest.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesRequestOrBuilder.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesResponse.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesResponseOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OperationProgress.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OperationProgressOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OptimizeRestoredDatabaseMetadata.java",