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