Skip to content

Commit 6696a9c

Browse files
authored
feat(bigquery): support IAM conditions in datasets in Java client. (#3602)
* feat(bigquery): support IAM conditions in datasets in Java client. * Fix formatting * Account for possible null condition field in Acl. * Add toString() method to Acl.Expr object. Use service account in integration test instead of hardcoded personal account. Change Database API calls to only have one branch, toggling only the access policy version in a conditional. * Change Acl.User to be default google credentials in IT test * fix formatting * Add Acl.Expr builder. Fix review nits. * fix formatting
1 parent 08c483c commit 6696a9c

File tree

7 files changed

+400
-22
lines changed

7 files changed

+400
-22
lines changed

google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Acl.java

+158-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.api.core.ApiFunction;
2222
import com.google.api.services.bigquery.model.Dataset.Access;
2323
import com.google.api.services.bigquery.model.DatasetAccessEntry;
24+
import com.google.api.services.bigquery.model.Expr;
2425
import com.google.cloud.StringEnumType;
2526
import com.google.cloud.StringEnumValue;
2627
import java.io.Serializable;
@@ -41,6 +42,7 @@ public final class Acl implements Serializable {
4142

4243
private final Entity entity;
4344
private final Role role;
45+
private final Expr condition;
4446

4547
/**
4648
* Dataset roles supported by BigQuery.
@@ -568,9 +570,147 @@ Access toPb() {
568570
}
569571
}
570572

573+
/** Expr represents the conditional information related to dataset access policies. */
574+
public static final class Expr implements Serializable {
575+
// Textual representation of an expression in Common Expression Language syntax.
576+
private final String expression;
577+
/**
578+
* Optional. Title for the expression, i.e. a short string describing its purpose. This can be
579+
* used e.g. in UIs which allow to enter the expression.
580+
*/
581+
private final String title;
582+
/**
583+
* Optional. Description of the expression. This is a longer text which describes the
584+
* expression, e.g. when hovered over it in a UI.
585+
*/
586+
private final String description;
587+
/**
588+
* Optional. String indicating the location of the expression for error reporting, e.g. a file
589+
* name and a position in the file.
590+
*/
591+
private final String location;
592+
593+
private static final long serialVersionUID = 7358264726377291156L;
594+
595+
static final class Builder {
596+
private String expression;
597+
private String title;
598+
private String description;
599+
private String location;
600+
601+
Builder() {}
602+
603+
Builder(Expr expr) {
604+
this.expression = expr.expression;
605+
this.title = expr.title;
606+
this.description = expr.description;
607+
this.location = expr.location;
608+
}
609+
610+
Builder(com.google.api.services.bigquery.model.Expr bqExpr) {
611+
this.expression = bqExpr.getExpression();
612+
if (bqExpr.getTitle() != null) {
613+
this.title = bqExpr.getTitle();
614+
}
615+
if (bqExpr.getDescription() != null) {
616+
this.description = bqExpr.getDescription();
617+
}
618+
if (bqExpr.getLocation() != null) {
619+
this.location = bqExpr.getLocation();
620+
}
621+
}
622+
623+
public Builder setExpression(String expression) {
624+
this.expression = expression;
625+
return this;
626+
}
627+
628+
public Builder setTitle(String title) {
629+
this.title = title;
630+
return this;
631+
}
632+
633+
public Builder setDescription(String description) {
634+
this.description = description;
635+
return this;
636+
}
637+
638+
public Builder setLocation(String location) {
639+
this.location = location;
640+
return this;
641+
}
642+
643+
public Expr build() {
644+
return new Expr(this);
645+
}
646+
}
647+
648+
public Expr(Builder builder) {
649+
this.expression = builder.expression;
650+
this.title = builder.title;
651+
this.description = builder.description;
652+
this.location = builder.location;
653+
}
654+
655+
public Expr(String expression, String title, String description, String location) {
656+
this.expression = expression;
657+
this.title = title;
658+
this.description = description;
659+
this.location = location;
660+
}
661+
662+
com.google.api.services.bigquery.model.Expr toPb() {
663+
com.google.api.services.bigquery.model.Expr bqExpr =
664+
new com.google.api.services.bigquery.model.Expr();
665+
bqExpr.setExpression(this.expression);
666+
bqExpr.setTitle(this.title);
667+
bqExpr.setDescription(this.description);
668+
bqExpr.setLocation(this.location);
669+
return bqExpr;
670+
}
671+
672+
static Expr fromPb(com.google.api.services.bigquery.model.Expr bqExpr) {
673+
return new Builder(bqExpr).build();
674+
}
675+
676+
public Builder toBuilder() {
677+
return new Builder(this);
678+
}
679+
680+
@Override
681+
public int hashCode() {
682+
return Objects.hash(expression, title, description, location);
683+
}
684+
685+
@Override
686+
public boolean equals(Object obj) {
687+
if (this == obj) {
688+
return true;
689+
}
690+
if (obj == null || getClass() != obj.getClass()) {
691+
return false;
692+
}
693+
final Expr other = (Expr) obj;
694+
return Objects.equals(this.expression, other.expression)
695+
&& Objects.equals(this.title, other.title)
696+
&& Objects.equals(this.description, other.description)
697+
&& Objects.equals(this.location, other.location);
698+
}
699+
700+
@Override
701+
public String toString() {
702+
return toPb().toString();
703+
}
704+
}
705+
571706
private Acl(Entity entity, Role role) {
707+
this(entity, role, null);
708+
}
709+
710+
private Acl(Entity entity, Role role, Expr condition) {
572711
this.entity = checkNotNull(entity);
573712
this.role = role;
713+
this.condition = condition;
574714
}
575715

576716
/** @return Returns the entity for this ACL. */
@@ -582,6 +722,10 @@ public Entity getEntity() {
582722
public Role getRole() {
583723
return role;
584724
}
725+
/** @return Returns the condition specified by this ACL. */
726+
public Expr getCondition() {
727+
return condition;
728+
}
585729

586730
/**
587731
* @return Returns an Acl object.
@@ -592,6 +736,10 @@ public static Acl of(Entity entity, Role role) {
592736
return new Acl(entity, role);
593737
}
594738

739+
public static Acl of(Entity entity, Role role, Expr condition) {
740+
return new Acl(entity, role, condition);
741+
}
742+
595743
/**
596744
* @param datasetAclEntity
597745
* @return Returns an Acl object for a datasetAclEntity.
@@ -618,7 +766,7 @@ public static Acl of(Routine routine) {
618766

619767
@Override
620768
public int hashCode() {
621-
return Objects.hash(entity, role);
769+
return Objects.hash(entity, role, condition);
622770
}
623771

624772
@Override
@@ -635,19 +783,26 @@ public boolean equals(Object obj) {
635783
return false;
636784
}
637785
final Acl other = (Acl) obj;
638-
return Objects.equals(this.entity, other.entity) && Objects.equals(this.role, other.role);
786+
return Objects.equals(this.entity, other.entity)
787+
&& Objects.equals(this.role, other.role)
788+
&& Objects.equals(this.condition, other.condition);
639789
}
640790

641791
Access toPb() {
642792
Access accessPb = entity.toPb();
643793
if (role != null) {
644794
accessPb.setRole(role.name());
645795
}
796+
if (condition != null) {
797+
accessPb.setCondition(condition.toPb());
798+
}
646799
return accessPb;
647800
}
648801

649802
static Acl fromPb(Access access) {
650803
return Acl.of(
651-
Entity.fromPb(access), access.getRole() != null ? Role.valueOf(access.getRole()) : null);
804+
Entity.fromPb(access),
805+
access.getRole() != null ? Role.valueOf(access.getRole()) : null,
806+
access.getCondition() != null ? Expr.fromPb(access.getCondition()) : null);
652807
}
653808
}

google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java

+18
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,24 @@ public static DatasetOption fields(DatasetField... fields) {
289289
return new DatasetOption(
290290
BigQueryRpc.Option.FIELDS, Helper.selector(DatasetField.REQUIRED_FIELDS, fields));
291291
}
292+
293+
/**
294+
* Returns an option to specify the dataset's access policy version for conditional access. If
295+
* this option is not provided the field remains unset and conditional access cannot be used.
296+
* Valid values are 0, 1, and 3. Requests specifying an invalid value will be rejected. Requests
297+
* for conditional access policy binding in datasets must specify version 3. Datasets with no
298+
* conditional role bindings in access policy may specify any valid value or leave the field
299+
* unset. This field will be mapped to <a
300+
* href="https://cloud.google.com/iam/docs/policies#versions">IAM Policy version</a> and will be
301+
* used to fetch the policy from IAM. If unset or if 0 or 1 the value is used for a dataset with
302+
* conditional bindings, access entry with condition will have role string appended by
303+
* 'withcond' string followed by a hash value. Please refer to <a
304+
* href="https://cloud.google.com/iam/docs/troubleshooting-withcond">Troubleshooting
305+
* withcond</a> for more details.
306+
*/
307+
public static DatasetOption accessPolicyVersion(Integer accessPolicyVersion) {
308+
return new DatasetOption(BigQueryRpc.Option.ACCESS_POLICY_VERSION, accessPolicyVersion);
309+
}
292310
}
293311

294312
/** Class for specifying dataset delete options. */

google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/BigQueryRpc.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ enum Option {
5959
REQUESTED_POLICY_VERSION("requestedPolicyVersion"),
6060
TABLE_METADATA_VIEW("view"),
6161
RETRY_OPTIONS("retryOptions"),
62-
BIGQUERY_RETRY_CONFIG("bigQueryRetryConfig");
62+
BIGQUERY_RETRY_CONFIG("bigQueryRetryConfig"),
63+
ACCESS_POLICY_VERSION("accessPolicyVersion");
6364

6465
private final String value;
6566

google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java

+37-18
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,19 @@ private void validateRPC() throws BigQueryException, IOException {
130130
public Dataset getDataset(String projectId, String datasetId, Map<Option, ?> options) {
131131
try {
132132
validateRPC();
133-
return bigquery
134-
.datasets()
135-
.get(projectId, datasetId)
136-
.setFields(Option.FIELDS.getString(options))
137-
.setPrettyPrint(false)
138-
.execute();
133+
134+
Bigquery.Datasets.Get bqGetRequest =
135+
bigquery
136+
.datasets()
137+
.get(projectId, datasetId)
138+
.setFields(Option.FIELDS.getString(options))
139+
.setPrettyPrint(false);
140+
for (Map.Entry<Option, ?> entry : options.entrySet()) {
141+
if (entry.getKey() == Option.ACCESS_POLICY_VERSION && entry.getValue() != null) {
142+
bqGetRequest.setAccessPolicyVersion((Integer) entry.getValue());
143+
}
144+
}
145+
return bqGetRequest.execute();
139146
} catch (IOException ex) {
140147
BigQueryException serviceException = translate(ex);
141148
if (serviceException.getCode() == HTTP_NOT_FOUND) {
@@ -174,12 +181,18 @@ public Tuple<String, Iterable<Dataset>> listDatasets(String projectId, Map<Optio
174181
public Dataset create(Dataset dataset, Map<Option, ?> options) {
175182
try {
176183
validateRPC();
177-
return bigquery
178-
.datasets()
179-
.insert(dataset.getDatasetReference().getProjectId(), dataset)
180-
.setPrettyPrint(false)
181-
.setFields(Option.FIELDS.getString(options))
182-
.execute();
184+
Bigquery.Datasets.Insert bqCreateRequest =
185+
bigquery
186+
.datasets()
187+
.insert(dataset.getDatasetReference().getProjectId(), dataset)
188+
.setPrettyPrint(false)
189+
.setFields(Option.FIELDS.getString(options));
190+
for (Map.Entry<Option, ?> entry : options.entrySet()) {
191+
if (entry.getKey() == Option.ACCESS_POLICY_VERSION && entry.getValue() != null) {
192+
bqCreateRequest.setAccessPolicyVersion((Integer) entry.getValue());
193+
}
194+
}
195+
return bqCreateRequest.execute();
183196
} catch (IOException ex) {
184197
throw translate(ex);
185198
}
@@ -277,12 +290,18 @@ public Dataset patch(Dataset dataset, Map<Option, ?> options) {
277290
try {
278291
validateRPC();
279292
DatasetReference reference = dataset.getDatasetReference();
280-
return bigquery
281-
.datasets()
282-
.patch(reference.getProjectId(), reference.getDatasetId(), dataset)
283-
.setPrettyPrint(false)
284-
.setFields(Option.FIELDS.getString(options))
285-
.execute();
293+
Bigquery.Datasets.Patch bqPatchRequest =
294+
bigquery
295+
.datasets()
296+
.patch(reference.getProjectId(), reference.getDatasetId(), dataset)
297+
.setPrettyPrint(false)
298+
.setFields(Option.FIELDS.getString(options));
299+
for (Map.Entry<Option, ?> entry : options.entrySet()) {
300+
if (entry.getKey() == Option.ACCESS_POLICY_VERSION && entry.getValue() != null) {
301+
bqPatchRequest.setAccessPolicyVersion((Integer) entry.getValue());
302+
}
303+
}
304+
return bqPatchRequest.execute();
286305
} catch (IOException ex) {
287306
throw translate(ex);
288307
}

google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/AclTest.java

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.cloud.bigquery.Acl.Domain;
2424
import com.google.cloud.bigquery.Acl.Entity;
2525
import com.google.cloud.bigquery.Acl.Entity.Type;
26+
import com.google.cloud.bigquery.Acl.Expr;
2627
import com.google.cloud.bigquery.Acl.Group;
2728
import com.google.cloud.bigquery.Acl.IamMember;
2829
import com.google.cloud.bigquery.Acl.Role;
@@ -136,4 +137,13 @@ public void testOf() {
136137
assertEquals(routine, acl.getEntity());
137138
assertEquals(null, acl.getRole());
138139
}
140+
141+
@Test
142+
public void testOfWithCondition() {
143+
Expr expr = new Expr("expression", "title", "description", "location");
144+
Acl acl = Acl.of(Group.ofAllAuthenticatedUsers(), Role.READER, expr);
145+
Dataset.Access pb = acl.toPb();
146+
assertEquals(acl, Acl.fromPb(pb));
147+
assertEquals(acl.getCondition(), expr);
148+
}
139149
}

google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java

+15
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.cloud.RetryOption;
3131
import com.google.cloud.ServiceOptions;
3232
import com.google.cloud.Tuple;
33+
import com.google.cloud.bigquery.BigQuery.DatasetOption;
3334
import com.google.cloud.bigquery.BigQuery.JobOption;
3435
import com.google.cloud.bigquery.BigQuery.QueryResultsOption;
3536
import com.google.cloud.bigquery.InsertAllRequest.RowToInsert;
@@ -572,6 +573,20 @@ public void testCreateDatasetWithSelectedFields() {
572573
verify(bigqueryRpcMock).create(eq(DATASET_INFO_WITH_PROJECT.toPb()), capturedOptions.capture());
573574
}
574575

576+
@Test
577+
public void testCreateDatasetWithAccessPolicy() {
578+
DatasetInfo datasetInfo = DATASET_INFO.setProjectId(OTHER_PROJECT);
579+
DatasetOption datasetOption = DatasetOption.accessPolicyVersion(3);
580+
when(bigqueryRpcMock.create(datasetInfo.toPb(), optionMap(datasetOption)))
581+
.thenReturn(datasetInfo.toPb());
582+
BigQueryOptions bigQueryOptions =
583+
createBigQueryOptionsForProject(OTHER_PROJECT, rpcFactoryMock);
584+
bigquery = bigQueryOptions.getService();
585+
Dataset dataset = bigquery.create(datasetInfo, datasetOption);
586+
assertEquals(new Dataset(bigquery, new DatasetInfo.BuilderImpl(datasetInfo)), dataset);
587+
verify(bigqueryRpcMock).create(datasetInfo.toPb(), optionMap(datasetOption));
588+
}
589+
575590
@Test
576591
public void testGetDataset() {
577592
when(bigqueryRpcMock.getDataset(PROJECT, DATASET, EMPTY_RPC_OPTIONS))

0 commit comments

Comments
 (0)