From 9511b173e84d2b28ab1a1625b16e3e648c3856fb Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Wed, 4 Jan 2023 11:33:11 -0500 Subject: [PATCH] docs: document differing behavior of {get,list}{,default}Acl between HTTP and gRPC (#1820) Update ITAccessTest to run UBLA and IAM centric tests for gRPC. Split iam v1 and iam v3 tests out from ITAccessTest to ITBucketIamPolicyTest making them hermetic. --- .../com/google/cloud/storage/Storage.java | 40 ++ .../google/cloud/storage/it/ITAccessTest.java | 439 ++---------------- .../storage/it/ITBucketIamPolicyTest.java | 225 +++++++++ 3 files changed, 305 insertions(+), 399 deletions(-) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketIamPolicyTest.java 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 5b3c06dab4..bfc2cdb093 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 @@ -3363,6 +3363,16 @@ PostPolicyV4 generateSignedPostPolicyV4( * Acl acl = storage.getAcl(bucketName, new User(userEmail), userProjectOption); * } * + *

Behavioral Differences between HTTP and gRPC

+ * + *
    + *
  1. Calling this method for a Bucket which has Uniform + * bucket-level access enabled exhibits different behavior Depending on which {@link + * Transport} is used. For JSON, an HTTP 400 Bad Request error will be thrown. Whereas for + * gRPC, an empty list will be returned. + *
+ * * @param bucket name of the bucket where the getAcl operation takes place * @param entity ACL entity to fetch * @param options extra parameters to apply to this operation @@ -3496,6 +3506,16 @@ PostPolicyV4 generateSignedPostPolicyV4( * } * } * + *

Behavioral Differences between HTTP and gRPC

+ * + *
    + *
  1. Calling this method for a Bucket which has Uniform + * bucket-level access enabled exhibits different behavior Depending on which {@link + * Transport} is used. For JSON, an HTTP 400 Bad Request error will be thrown. Whereas for + * gRPC, an empty list will be returned. + *
+ * * @param bucket the name of the bucket to list ACLs for * @param options any number of BucketSourceOptions to apply to this operation * @throws StorageException upon failure @@ -3521,6 +3541,16 @@ PostPolicyV4 generateSignedPostPolicyV4( * Acl acl = storage.getDefaultAcl(bucketName, User.ofAllAuthenticatedUsers()); * } * + *

Behavioral Differences between HTTP and gRPC

+ * + *
    + *
  1. Calling this method for a Bucket which has Uniform + * bucket-level access enabled exhibits different behavior Depending on which {@link + * Transport} is used. For JSON, an HTTP 400 Bad Request error will be thrown. Whereas for + * gRPC, an empty list will be returned. + *
+ * * @throws StorageException upon failure */ @TransportCompatibility({Transport.HTTP, Transport.GRPC}) @@ -3604,6 +3634,16 @@ PostPolicyV4 generateSignedPostPolicyV4( * } * } * + *

Behavioral Differences between HTTP and gRPC

+ * + *
    + *
  1. Calling this method for a Bucket which has Uniform + * bucket-level access enabled exhibits different behavior Depending on which {@link + * Transport} is used. For JSON, an HTTP 400 Bad Request error will be thrown. Whereas for + * gRPC, an empty list will be returned. + *
+ * * @throws StorageException upon failure */ @TransportCompatibility({Transport.HTTP, Transport.GRPC}) 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 99b29818d3..a46aad5313 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 @@ -16,6 +16,7 @@ package com.google.cloud.storage.it; +import static com.google.cloud.storage.TestUtils.assertAll; import static com.google.cloud.storage.TestUtils.retry429s; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -26,8 +27,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.cloud.Condition; -import com.google.cloud.Identity; import com.google.cloud.Policy; import com.google.cloud.storage.Acl; import com.google.cloud.storage.Acl.Entity; @@ -46,30 +45,19 @@ import com.google.cloud.storage.Storage.BucketGetOption; import com.google.cloud.storage.Storage.BucketTargetOption; import com.google.cloud.storage.StorageException; -import com.google.cloud.storage.StorageRoles; import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.it.runner.StorageITRunner; import com.google.cloud.storage.it.runner.annotations.Backend; -import com.google.cloud.storage.it.runner.annotations.BucketFixture; -import com.google.cloud.storage.it.runner.annotations.BucketType; import com.google.cloud.storage.it.runner.annotations.CrossRun; import com.google.cloud.storage.it.runner.annotations.Inject; import com.google.cloud.storage.it.runner.registry.Generator; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import java.time.Duration; -import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collector; import java.util.stream.Collectors; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,13 +72,9 @@ public class ITAccessTest { @Inject public Storage storage; - @Inject - @BucketFixture(BucketType.DEFAULT) - public BucketInfo bucket; + @Inject public Transport transport; - @Inject - @BucketFixture(BucketType.REQUESTER_PAYS) - public BucketInfo requesterPaysBucket; + @Inject public BucketInfo bucket; @Inject public Generator generator; @@ -310,367 +294,53 @@ public void bucket_defaultAcl_delete_noExistingAcl() throws Exception { } } + /** Validate legacy deprecated field is redirected correctly */ @Test - @Ignore("Make hermetic, previously dependant on external transitive state") - public void testBucketPolicyV1RequesterPays() { - String projectId = storage.getOptions().getProjectId(); - - Storage.BucketSourceOption[] bucketOptions = - new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)}; - Identity projectOwner = Identity.projectOwner(projectId); - Identity projectEditor = Identity.projectEditor(projectId); - Identity projectViewer = Identity.projectViewer(projectId); - Map> bindingsWithoutPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - ImmutableSet.of(projectOwner, projectEditor), - StorageRoles.legacyBucketReader(), - ImmutableSet.of(projectViewer)); - Map> bindingsWithPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - ImmutableSet.of(projectOwner, projectEditor), - StorageRoles.legacyBucketReader(), - ImmutableSet.of(projectViewer), - StorageRoles.legacyObjectReader(), - ImmutableSet.of(Identity.allUsers())); - - // Validate getting policy. - Policy currentPolicy = storage.getIamPolicy(requesterPaysBucket.getName(), bucketOptions); - assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings()); - - // Validate updating policy. - Policy updatedPolicy = - storage.setIamPolicy( - requesterPaysBucket.getName(), - currentPolicy - .toBuilder() - .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings()); - Policy revertedPolicy = - storage.setIamPolicy( - requesterPaysBucket.getName(), - updatedPolicy - .toBuilder() - .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings()); - - // Validate testing permissions. - List expectedPermissions = ImmutableList.of(true, true); - assertEquals( - expectedPermissions, - storage.testIamPermissions( - requesterPaysBucket.getName(), - ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), - bucketOptions)); + @SuppressWarnings("deprecation") + public void testBucketWithBucketPolicyOnlyEnabled() throws Exception { + doTestUniformBucketLevelAccessAclImpact( + BucketInfo.IamConfiguration.newBuilder().setIsBucketPolicyOnlyEnabled(true).build()); } @Test - @Ignore("Make hermetic, previously dependant on external transitive state") - public void testBucketPolicyV1() { - String projectId = storage.getOptions().getProjectId(); - - Storage.BucketSourceOption[] bucketOptions = new Storage.BucketSourceOption[] {}; - Identity projectOwner = Identity.projectOwner(projectId); - Identity projectEditor = Identity.projectEditor(projectId); - Identity projectViewer = Identity.projectViewer(projectId); - Map> bindingsWithoutPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - ImmutableSet.of(projectOwner, projectEditor), - StorageRoles.legacyBucketReader(), - ImmutableSet.of(projectViewer)); - Map> bindingsWithPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - ImmutableSet.of(projectOwner, projectEditor), - StorageRoles.legacyBucketReader(), - ImmutableSet.of(projectViewer), - StorageRoles.legacyObjectReader(), - ImmutableSet.of(Identity.allUsers())); - - // Validate getting policy. - Policy currentPolicy = storage.getIamPolicy(bucket.getName(), bucketOptions); - assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings()); - - // Validate updating policy. - Policy updatedPolicy = - storage.setIamPolicy( - bucket.getName(), - currentPolicy - .toBuilder() - .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings()); - Policy revertedPolicy = - storage.setIamPolicy( - bucket.getName(), - updatedPolicy - .toBuilder() - .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings()); - - // Validate testing permissions. - List expectedPermissions = ImmutableList.of(true, true); - assertEquals( - expectedPermissions, - storage.testIamPermissions( - bucket.getName(), - ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), - bucketOptions)); + public void testBucketWithUniformBucketLevelAccessEnabled() throws Exception { + doTestUniformBucketLevelAccessAclImpact( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(true) + .build()); } - @Test - @Ignore("Make hermetic, previously dependant on external transitive state") - public void testBucketPolicyV3() throws Exception { - String projectId = storage.getOptions().getProjectId(); - BucketInfo bucketInfo = - BucketInfo.newBuilder(generator.randomBucketName()) - .setIamConfiguration( - BucketInfo.IamConfiguration.newBuilder() - .setIsUniformBucketLevelAccessEnabled(true) - .build()) - .build(); - Storage.BucketSourceOption[] bucketOptions = - new Storage.BucketSourceOption[] {Storage.BucketSourceOption.requestedPolicyVersion(3)}; - Identity projectOwner = Identity.projectOwner(projectId); - Identity projectEditor = Identity.projectEditor(projectId); - Identity projectViewer = Identity.projectViewer(projectId); - List bindingsWithoutPublicRead = - ImmutableList.of( - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketOwner().toString()) - .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue())) - .build(), - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketReader().toString()) - .setMembers(ImmutableList.of(projectViewer.strValue())) - .build()); - List bindingsWithPublicRead = - ImmutableList.of( - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketReader().toString()) - .setMembers(ImmutableList.of(projectViewer.strValue())) - .build(), - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketOwner().toString()) - .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue())) - .build(), - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyObjectReader().toString()) - .setMembers(ImmutableList.of("allUsers")) - .build()); - - List bindingsWithConditionalPolicy = - ImmutableList.of( - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketReader().toString()) - .setMembers(ImmutableList.of(projectViewer.strValue())) - .build(), - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyBucketOwner().toString()) - .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue())) - .build(), - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyObjectReader().toString()) - .setMembers( - ImmutableList.of( - "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com")) - .setCondition( - Condition.newBuilder() - .setTitle("Title") - .setDescription("Description") - .setExpression( - "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")") - .build()) - .build()); - + private void doTestUniformBucketLevelAccessAclImpact(IamConfiguration iamConfiguration) + throws Exception { + String bucketName = generator.randomBucketName(); try (TemporaryBucket tempB = - TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) { + TemporaryBucket.newBuilder() + .setBucketInfo( + Bucket.newBuilder(bucketName).setIamConfiguration(iamConfiguration).build()) + .setStorage(storage) + .build()) { BucketInfo bucket = tempB.getBucket(); - // Validate getting policy. - Policy currentPolicy = storage.getIamPolicy(bucket.getName(), bucketOptions); - Collector joining = Collectors.joining(",\n\t", "[\n\t", "\n]"); - String s = currentPolicy.getBindingsList().stream().map(Object::toString).collect(joining); - String ss = bindingsWithoutPublicRead.stream().map(Object::toString).collect(joining); - assertThat(s).isEqualTo(ss); - // assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindingsList()); - - // Validate updating policy. - List currentBindings = - new ArrayList(currentPolicy.getBindingsList()); - currentBindings.add( - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyObjectReader().getValue()) - .addMembers(Identity.allUsers().strValue()) - .build()); - Policy updatedPolicy = - storage.setIamPolicy( - bucket.getName(), - currentPolicy.toBuilder().setBindings(currentBindings).build(), - bucketOptions); - assertTrue( - bindingsWithPublicRead.size() == updatedPolicy.getBindingsList().size() - && bindingsWithPublicRead.containsAll(updatedPolicy.getBindingsList())); - - // Remove a member - List updatedBindings = - new ArrayList(updatedPolicy.getBindingsList()); - for (int i = 0; i < updatedBindings.size(); i++) { - com.google.cloud.Binding binding = updatedBindings.get(i); - if (binding.getRole().equals(StorageRoles.legacyObjectReader().toString())) { - List members = new ArrayList(binding.getMembers()); - members.remove(Identity.allUsers().strValue()); - updatedBindings.set(i, binding.toBuilder().setMembers(members).build()); - break; - } + assertTrue(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled()); + assertNotNull( + bucket.getIamConfiguration().getUniformBucketLevelAccessLockedTimeOffsetDateTime()); + + if (transport == Transport.HTTP) { + StorageException listAclsError = + assertThrows(StorageException.class, () -> storage.listAcls(bucketName)); + assertAll( + () -> assertThat(listAclsError.getCode()).isEqualTo(400), + () -> assertThat(listAclsError.getReason()).isEqualTo("invalid")); + + StorageException listDefaultAclsError = + assertThrows(StorageException.class, () -> storage.listDefaultAcls(bucketName)); + assertAll( + () -> assertThat(listDefaultAclsError.getCode()).isEqualTo(400), + () -> assertThat(listDefaultAclsError.getReason()).isEqualTo("invalid")); + } else if (transport == Transport.GRPC) { + assertThat(storage.listAcls(bucketName)).isEmpty(); + assertThat(storage.listDefaultAcls(bucketName)).isEmpty(); } - - Policy revertedPolicy = - storage.setIamPolicy( - bucket.getName(), - updatedPolicy.toBuilder().setBindings(updatedBindings).build(), - bucketOptions); - - assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindingsList()); - assertTrue( - bindingsWithoutPublicRead.size() == revertedPolicy.getBindingsList().size() - && bindingsWithoutPublicRead.containsAll(revertedPolicy.getBindingsList())); - - // Add Conditional Policy - List conditionalBindings = - new ArrayList(revertedPolicy.getBindingsList()); - conditionalBindings.add( - com.google.cloud.Binding.newBuilder() - .setRole(StorageRoles.legacyObjectReader().toString()) - .addMembers( - "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com") - .setCondition( - Condition.newBuilder() - .setTitle("Title") - .setDescription("Description") - .setExpression( - "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")") - .build()) - .build()); - Policy conditionalPolicy = - storage.setIamPolicy( - bucket.getName(), - revertedPolicy.toBuilder().setBindings(conditionalBindings).setVersion(3).build(), - bucketOptions); - assertTrue( - bindingsWithConditionalPolicy.size() == conditionalPolicy.getBindingsList().size() - && bindingsWithConditionalPolicy.containsAll(conditionalPolicy.getBindingsList())); - - // Remove Conditional Policy - conditionalPolicy = - storage.setIamPolicy( - bucket.getName(), - conditionalPolicy.toBuilder().setBindings(updatedBindings).setVersion(3).build(), - bucketOptions); - - // Validate testing permissions. - List expectedPermissions = ImmutableList.of(true, true); - assertEquals( - expectedPermissions, - storage.testIamPermissions( - bucket.getName(), - ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), - bucketOptions)); - } - } - - @Test - @SuppressWarnings({"unchecked", "deprecation"}) - @CrossRun.Ignore(transports = Transport.GRPC) - public void testBucketWithBucketPolicyOnlyEnabled() throws Exception { - // TODO: break this test up into each of the respective scenarios - // 1. Create bucket with BucketPolicyOnly enabled - // 2. Get bucket with BucketPolicyOnly enabled - // 3. Expect failure when attempting to list ACLs for BucketPolicyOnly bucket - // 4. Expect failure when attempting to list default ACLs for BucketPolicyOnly bucket - - // TODO: temp bucket - String randBucketName = generator.randomBucketName(); - try { - storage.create( - Bucket.newBuilder(randBucketName) - .setIamConfiguration( - BucketInfo.IamConfiguration.newBuilder() - .setIsBucketPolicyOnlyEnabled(true) - .build()) - .build()); - - Bucket remoteBucket = - storage.get(randBucketName, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION)); - - assertTrue(remoteBucket.getIamConfiguration().isBucketPolicyOnlyEnabled()); - assertNotNull(remoteBucket.getIamConfiguration().getBucketPolicyOnlyLockedTime()); - - try { - remoteBucket.listAcls(); - fail("StorageException was expected."); - } catch (StorageException e) { - // Expected: Listing legacy ACLs should fail on a BPO enabled bucket - } - try { - remoteBucket.listDefaultAcls(); - fail("StorageException was expected"); - } catch (StorageException e) { - // Expected: Listing legacy ACLs should fail on a BPO enabled bucket - } - } finally { - BucketCleaner.doCleanup(randBucketName, storage); - } - } - - @Test - @CrossRun.Ignore(transports = Transport.GRPC) - public void testBucketWithUniformBucketLevelAccessEnabled() throws Exception { - // TODO: break this test up into each of the respective scenarios - // 1. Create bucket with UniformBucketLevelAccess enabled - // 2. Get bucket with UniformBucketLevelAccess enabled - // 3. Expect failure when attempting to list ACLs for UniformBucketLevelAccess bucket - // 4. Expect failure when attempting to list default ACLs for UniformBucketLevelAccess bucket - - // TODO: temp bucket - String randBucketName = generator.randomBucketName(); - try { - storage.create( - Bucket.newBuilder(randBucketName) - .setIamConfiguration( - BucketInfo.IamConfiguration.newBuilder() - .setIsUniformBucketLevelAccessEnabled(true) - .build()) - .build()); - - Bucket remoteBucket = - storage.get(randBucketName, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION)); - - assertTrue(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled()); - assertNotNull(remoteBucket.getIamConfiguration().getUniformBucketLevelAccessLockedTime()); - try { - remoteBucket.listAcls(); - fail("StorageException was expected."); - } catch (StorageException e) { - // Expected: Listing legacy ACLs should fail on a BPO enabled bucket - } - try { - remoteBucket.listDefaultAcls(); - fail("StorageException was expected"); - } catch (StorageException e) { - // Expected: Listing legacy ACLs should fail on a BPO enabled bucket - } - } finally { - BucketCleaner.doCleanup(randBucketName, storage); } } @@ -910,35 +580,6 @@ public void changingUBLADoesNotAffectPAP() throws Exception { } } - @Test - public void testListBucketRequesterPaysFails() throws InterruptedException { - String projectId = storage.getOptions().getProjectId(); - Iterator bucketIterator = - storage - .list( - Storage.BucketListOption.prefix(bucket.getName()), - Storage.BucketListOption.fields(), - Storage.BucketListOption.userProject(projectId)) - .iterateAll() - .iterator(); - while (!bucketIterator.hasNext()) { - Thread.sleep(500); - bucketIterator = - storage - .list( - Storage.BucketListOption.prefix(bucket.getName()), - Storage.BucketListOption.fields()) - .iterateAll() - .iterator(); - } - while (bucketIterator.hasNext()) { - Bucket remoteBucket = bucketIterator.next(); - assertTrue(remoteBucket.getName().startsWith(bucket.getName())); - assertNull(remoteBucket.getCreateTime()); - assertNull(remoteBucket.getSelfLink()); - } - } - @Test public void testRetentionPolicyNoLock() throws Exception { String bucketName = generator.randomBucketName(); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketIamPolicyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketIamPolicyTest.java new file mode 100644 index 0000000000..ded7e96b8b --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketIamPolicyTest.java @@ -0,0 +1,225 @@ +/* + * 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.storage.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.Binding; +import com.google.cloud.Condition; +import com.google.cloud.Identity; +import com.google.cloud.Policy; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BucketSourceOption; +import com.google.cloud.storage.StorageRoles; +import com.google.cloud.storage.TestUtils; +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.CrossRun; +import com.google.cloud.storage.it.runner.annotations.Inject; +import com.google.cloud.storage.it.runner.annotations.ParallelFriendly; +import com.google.cloud.storage.it.runner.registry.Generator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@CrossRun( + transports = {Transport.HTTP, Transport.GRPC}, + backends = {Backend.PROD}) +@ParallelFriendly +public final class ITBucketIamPolicyTest { + @Inject public Storage storage; + + @Inject public BucketInfo bucketInfo; + + @Inject public Generator generator; + + private Identity projectOwner; + private Identity projectEditor; + private Identity projectViewer; + + @Before + public void setUp() throws Exception { + String projectId = storage.getOptions().getProjectId(); + projectOwner = Identity.projectOwner(projectId); + projectEditor = Identity.projectEditor(projectId); + projectViewer = Identity.projectViewer(projectId); + } + + /** + * In order to define an IAM Condition, policy version 3 and Uniform Bucket Level Access must both + * be used. + * + *

Define a policy with a condition and verify it can be read back and decoded equivalently. + */ + @Test + public void iamPolicyWithCondition() throws Exception { + BucketSourceOption opt = BucketSourceOption.requestedPolicyVersion(3); + Policy policy = + Policy.newBuilder() + .setVersion(3) + .setBindings( + ImmutableList.of( + Binding.newBuilder() + .setRole(StorageRoles.legacyBucketReader().toString()) + .setMembers(ImmutableList.of(projectViewer.strValue())) + .build(), + Binding.newBuilder() + .setRole(StorageRoles.legacyBucketOwner().toString()) + .setMembers( + ImmutableList.of(projectEditor.strValue(), projectOwner.strValue())) + .build(), + Binding.newBuilder() + .setRole(StorageRoles.legacyObjectReader().toString()) + .setMembers( + ImmutableList.of( + "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com")) + .setCondition( + Condition.newBuilder() + .setTitle("Title") + .setDescription("Description") + .setExpression( + "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")") + .build()) + .build())) + .build(); + + try (TemporaryBucket tempB = + TemporaryBucket.newBuilder() + .setBucketInfo( + // create a bucket with UBLA set to true + BucketInfo.newBuilder(generator.randomBucketName()) + .setIamConfiguration( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(true) + .build()) + .build()) + .setStorage(storage) + .build()) { + BucketInfo bucket = tempB.getBucket(); + String bucketName = bucket.getName(); + + // Set the policy on the bucket + Policy setResult = + storage.setIamPolicy( + bucketName, + policy, + BucketSourceOption.metagenerationMatch(bucket.getMetageneration()), + opt); + assertPolicyEqual(policy, setResult); + + Policy actual = storage.getIamPolicy(bucketName, opt); + assertPolicyEqual(policy, actual); + } + } + + @Test + public void iamPolicyWithoutCondition() throws Exception { + BucketSourceOption opt = BucketSourceOption.requestedPolicyVersion(1); + Policy policy = + Policy.newBuilder() + .setVersion(1) + .setBindings( + ImmutableMap.of( + StorageRoles.legacyBucketOwner(), + ImmutableSet.of(projectOwner, projectEditor), + StorageRoles.legacyBucketReader(), + ImmutableSet.of(projectViewer))) + .build(); + + try (TemporaryBucket tempB = + TemporaryBucket.newBuilder() + .setBucketInfo( + // create a bucket without UBLA + BucketInfo.newBuilder(generator.randomBucketName()) + .setIamConfiguration( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(false) + .build()) + .build()) + .setStorage(storage) + .build()) { + BucketInfo bucket = tempB.getBucket(); + String bucketName = bucket.getName(); + + // Set the policy on the bucket + Policy setResult = + storage.setIamPolicy( + bucketName, + policy, + BucketSourceOption.metagenerationMatch(bucket.getMetageneration()), + opt); + assertPolicyEqual(policy, setResult); + + Policy actual = storage.getIamPolicy(bucketName, opt); + assertPolicyEqual(policy, actual); + } + } + + @Test + public void testIamPermissions() { + List expectedResult = ImmutableList.of(true, true); + ImmutableList permissions = + ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"); + List actual = storage.testIamPermissions(bucketInfo.getName(), permissions); + assertThat(actual).isEqualTo(expectedResult); + } + + private static void assertPolicyEqual(Policy expected, Policy actual) throws Exception { + TestUtils.assertAll( + () -> assertThat(actual.getVersion()).isEqualTo(expected.getVersion()), + () -> assertBindingsEqual(expected.getBindingsList(), actual.getBindingsList())); + } + + private static void assertBindingsEqual(List expected, List actual) { + + // pre-stringify the value to be compared to make it easier to diff if there is a failure + // ordering is not necessarily maintained across RPCs, after stringification sort before + // comparison + String e = stringifyBindings(expected); + String a = stringifyBindings(actual); + + assertThat(a).isEqualTo(e); + } + + private static String stringifyBindings(List bindings) { + Collector joining = Collectors.joining(",\n\t", "[\n\t", "\n]"); + // ordering is not necessarily maintained across RPCs + // Sort any lists before stringification + return bindings.stream() + .map( + b -> { + Binding.Builder builder = b.toBuilder(); + builder.setRole(b.getRole()); + builder.setCondition(b.getCondition()); + builder.setMembers( + b.getMembers().stream().sorted().collect(ImmutableList.toImmutableList())); + return builder.build(); + }) + .map(Object::toString) + .sorted() + .collect(joining); + } +}