From 0f24a11c5289a4c07f27d8a3c29fab34520b036f Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Wed, 14 Dec 2022 17:41:50 -0500 Subject: [PATCH] feat: implement GrpcStorageImpl#createDefaultAcl & GrpcStorageImpl#updateDefaultAcl (#1806) --- .../java/com/google/cloud/storage/Bucket.java | 4 +- .../google/cloud/storage/GrpcStorageImpl.java | 52 +++++++- .../com/google/cloud/storage/Storage.java | 4 +- .../com/google/cloud/storage/UnifiedOpts.java | 7 +- .../google/cloud/storage/it/ITAccessTest.java | 121 +++++++++++++++++- 5 files changed, 180 insertions(+), 8 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java index 4160327a80..84222eab22 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java @@ -1285,7 +1285,7 @@ public boolean deleteDefaultAcl(Entity entity) { * * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) public Acl createDefaultAcl(Acl acl) { return storage.createDefaultAcl(getName(), acl); } @@ -1304,7 +1304,7 @@ public Acl createDefaultAcl(Acl acl) { * * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) public Acl updateDefaultAcl(Acl acl) { return storage.updateDefaultAcl(getName(), acl); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index 73e56bbe20..1ced5cc8c5 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -68,6 +68,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import com.google.iam.v1.GetIamPolicyRequest; @@ -936,12 +937,59 @@ public boolean deleteDefaultAcl(String bucket, Entity entity) { @Override public Acl createDefaultAcl(String bucket, Acl acl) { - return throwNotYetImplemented(fmtMethodName("createDefaultAcl", String.class, Acl.class)); + return updateDefaultAcl(bucket, acl); } @Override public Acl updateDefaultAcl(String bucket, Acl acl) { - return throwNotYetImplemented(fmtMethodName("updateDefaultAcl", String.class, Acl.class)); + try { + com.google.storage.v2.Bucket resp = getBucketDefaultAcls(bucket); + ObjectAccessControl encode = codecs.objectAcl().encode(acl); + String entity = encode.getEntity(); + + Predicate entityPredicate = objectAclEntityOrAltEq(entity); + + ImmutableList collect = + Streams.concat( + resp.getDefaultObjectAclList().stream().filter(entityPredicate.negate()), + Stream.of(encode)) + .collect(ImmutableList.toImmutableList()); + + com.google.storage.v2.Bucket update = + com.google.storage.v2.Bucket.newBuilder() + .setName(bucketNameCodec.encode(bucket)) + .addAllDefaultObjectAcl(collect) + .build(); + Opts opts = + Opts.from( + UnifiedOpts.fields(ImmutableSet.of(BucketField.DEFAULT_OBJECT_ACL)), + UnifiedOpts.metagenerationMatch(resp.getMetageneration())); + UpdateBucketRequest req = + opts.updateBucketsRequest() + .apply(UpdateBucketRequest.newBuilder()) + .setBucket(update) + .build(); + + GrpcCallContext grpcCallContext = GrpcCallContext.createDefault(); + com.google.storage.v2.Bucket updateResult = + Retrying.run( + getOptions(), + retryAlgorithmManager.getFor(req), + () -> storageClient.updateBucketCallable().call(req, grpcCallContext), + Decoder.identity()); + + //noinspection DataFlowIssue + Optional first = + updateResult.getDefaultObjectAclList().stream() + .filter(entityPredicate) + .findFirst() + .map(codecs.objectAcl()::decode); + + return first.orElseThrow( + () -> new StorageException(0, "Acl update call success, but not in response")); + } catch (NotFoundException e) { + throw StorageException.coalesce(e); + } } @Override diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index ad92aae596..1819989836 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -3530,7 +3530,7 @@ PostPolicyV4 generateSignedPostPolicyV4( * * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) Acl createDefaultAcl(String bucket, Acl acl); /** @@ -3549,7 +3549,7 @@ PostPolicyV4 generateSignedPostPolicyV4( * * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) Acl updateDefaultAcl(String bucket, Acl acl); /** diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java index 934c2c298a..dc5d10bf52 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java @@ -713,7 +713,7 @@ public Mapper listObjects() { } static final class Fields extends RpcOptVal> - implements ObjectSourceOpt, ObjectListOpt, BucketSourceOpt, BucketListOpt { + implements ObjectSourceOpt, ObjectListOpt, BucketSourceOpt, BucketTargetOpt, BucketListOpt { /** * Apiary and gRPC have differing handling of where the field selector is evaluated relative to @@ -752,6 +752,11 @@ public Mapper listBuckets() { return b -> b.setReadMask(FieldMask.newBuilder().addAllPaths(getPaths()).build()); } + @Override + public Mapper updateBucket() { + return b -> b.setUpdateMask(FieldMask.newBuilder().addAllPaths(getPaths()).build()); + } + @Override public Mapper getObject() { return b -> b.setReadMask(FieldMask.newBuilder().addAllPaths(getPaths()).build()); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java index 7351227dd9..f15a799823 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java @@ -33,6 +33,8 @@ import com.google.cloud.RetryHelper.RetryHelperException; import com.google.cloud.http.BaseHttpServiceException; import com.google.cloud.storage.Acl; +import com.google.cloud.storage.Acl.Entity; +import com.google.cloud.storage.Acl.Project.ProjectRole; import com.google.cloud.storage.Acl.Role; import com.google.cloud.storage.Acl.User; import com.google.cloud.storage.Blob; @@ -70,6 +72,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; import org.junit.Ignore; @@ -185,12 +188,108 @@ public void bucket_defaultAcl_list_bucket404() { assertThat(storageException.getCode()).isEqualTo(404); } + @Test + public void bucket_defaultAcl_create() throws Exception { + BucketInfo bucketInfo = BucketInfo.newBuilder(generator.randomBucketName()).build(); + try (TemporaryBucket tempB = + TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) { + BucketInfo bucket = tempB.getBucket(); + + Acl readAll = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER); + Acl actual = retry429s(() -> storage.createDefaultAcl(bucket.getName(), readAll), storage); + + assertThat(actual.getEntity()).isEqualTo(readAll.getEntity()); + assertThat(actual.getRole()).isEqualTo(readAll.getRole()); + assertThat(actual.getEtag()).isNotEmpty(); + + Bucket bucketUpdated = + storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values())); + assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration()); + + // etags change when updates happen, drop before our comparison + List expectedAcls = dropEtags(bucket.getDefaultAcl()); + List actualAcls = dropEtags(bucketUpdated.getDefaultAcl()); + assertThat(actualAcls).containsAtLeastElementsIn(expectedAcls); + assertThat(actualAcls).contains(readAll); + } + } + + @Test + public void bucket_defaultAcl_create_bucket404() { + Acl readAll = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER); + StorageException storageException = + assertThrows( + StorageException.class, + () -> + retry429s( + () -> storage.createDefaultAcl(bucket.getName() + "x", readAll), storage)); + + assertThat(storageException.getCode()).isEqualTo(404); + } + + @Test + public void bucket_defaultAcl_update() throws Exception { + BucketInfo bucketInfo = BucketInfo.newBuilder(generator.randomBucketName()).build(); + try (TemporaryBucket tempB = + TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) { + BucketInfo bucket = tempB.getBucket(); + + List defaultAcls = bucket.getDefaultAcl(); + assertThat(defaultAcls).isNotEmpty(); + + Predicate isProjectEditor = hasProjectRole(ProjectRole.EDITORS); + + //noinspection OptionalGetWithoutIsPresent + Acl projectEditorAsOwner = + defaultAcls.stream().filter(hasRole(Role.OWNER).and(isProjectEditor)).findFirst().get(); + + // lower the privileges of project editors to writer from owner + Entity entity = projectEditorAsOwner.getEntity(); + Acl projectEditorAsReader = Acl.of(entity, Role.READER); + + Acl actual = + retry429s( + () -> storage.updateDefaultAcl(bucket.getName(), projectEditorAsReader), storage); + + assertThat(actual.getEntity()).isEqualTo(projectEditorAsReader.getEntity()); + assertThat(actual.getRole()).isEqualTo(projectEditorAsReader.getRole()); + assertThat(actual.getEtag()).isNotEmpty(); + + Bucket bucketUpdated = + storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values())); + assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration()); + + // etags change when updates happen, drop before our comparison + List expectedAcls = + dropEtags( + bucket.getDefaultAcl().stream() + .filter(isProjectEditor.negate()) + .collect(Collectors.toList())); + List actualAcls = dropEtags(bucketUpdated.getDefaultAcl()); + assertThat(actualAcls).containsAtLeastElementsIn(expectedAcls); + assertThat(actualAcls).doesNotContain(projectEditorAsOwner); + assertThat(actualAcls).contains(projectEditorAsReader); + } + } + + @Test + public void bucket_defaultAcl_update_bucket404() { + Acl readAll = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER); + StorageException storageException = + assertThrows( + StorageException.class, + () -> + retry429s( + () -> storage.updateDefaultAcl(bucket.getName() + "x", readAll), storage)); + + assertThat(storageException.getCode()).isEqualTo(404); + } + @Test @CrossRun.Ignore(transports = Transport.GRPC) public void testBucketDefaultAcl() { // TODO: break this test up into each of the respective scenarios // 2. Delete a default ACL for a specific entity - // 3. Create a default ACL for specific entity // 4. Update default ACL to change role of a specific entity // according to https://cloud.google.com/storage/docs/access-control/lists#default @@ -1026,4 +1125,24 @@ public boolean shouldRetry(Throwable previousThrowable, Object previousResponse) } } } + + private static ImmutableList dropEtags(List defaultAcls) { + return defaultAcls.stream() + .map(acl -> Acl.of(acl.getEntity(), acl.getRole())) + .collect(ImmutableList.toImmutableList()); + } + + private static Predicate hasRole(Acl.Role expected) { + return acl -> acl.getRole().equals(expected); + } + + private static Predicate hasProjectRole(Acl.Project.ProjectRole expected) { + return acl -> { + Entity entity = acl.getEntity(); + if (entity.getType().equals(Entity.Type.PROJECT)) { + return ((Acl.Project) entity).getProjectRole().equals(expected); + } + return false; + }; + } }