diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/PolicyHelper.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/PolicyHelper.java index aa2901ecb..e198d853b 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/PolicyHelper.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/PolicyHelper.java @@ -16,14 +16,14 @@ package com.google.cloud.storage; +import com.google.api.services.storage.model.Expr; import com.google.api.services.storage.model.Policy.Bindings; -import com.google.cloud.Identity; +import com.google.cloud.Binding; +import com.google.cloud.Condition; import com.google.cloud.Policy; -import com.google.cloud.Role; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; /** * Helper for converting between the Policy model provided by the API and the Policy model provided @@ -35,29 +35,46 @@ static Policy convertFromApiPolicy(com.google.api.services.storage.model.Policy Policy.Builder policyBuilder = Policy.newBuilder(); List bindings = apiPolicy.getBindings(); if (null != bindings && !bindings.isEmpty()) { + ImmutableList.Builder coreBindings = ImmutableList.builder(); for (Bindings binding : bindings) { - for (String member : binding.getMembers()) { - policyBuilder.addIdentity(Role.of(binding.getRole()), Identity.valueOf(member)); + Binding.Builder bindingBuilder = Binding.newBuilder(); + bindingBuilder.setRole(binding.getRole()); + bindingBuilder.setMembers(binding.getMembers()); + if (binding.getCondition() != null) { + Condition.Builder conditionBuilder = Condition.newBuilder(); + conditionBuilder.setTitle(binding.getCondition().getTitle()); + conditionBuilder.setDescription(binding.getCondition().getDescription()); + conditionBuilder.setExpression(binding.getCondition().getExpression()); + bindingBuilder.setCondition(conditionBuilder.build()); } + coreBindings.add(bindingBuilder.build()); } + policyBuilder.setBindings(coreBindings.build()); } else { throw new IllegalStateException("Missing required bindings."); } - return policyBuilder.setEtag(apiPolicy.getEtag()).build(); + return policyBuilder.setEtag(apiPolicy.getEtag()).setVersion(apiPolicy.getVersion()).build(); } static com.google.api.services.storage.model.Policy convertToApiPolicy(Policy policy) { - List bindings = new ArrayList<>(policy.getBindings().size()); - for (Map.Entry> entry : policy.getBindings().entrySet()) { - List members = new ArrayList<>(entry.getValue().size()); - for (Identity identity : entry.getValue()) { - members.add(identity.strValue()); + List bindings = new ArrayList<>(policy.getBindingsList().size()); + for (Binding binding : policy.getBindingsList()) { + Bindings apiBinding = new Bindings(); + apiBinding.setRole(binding.getRole()); + apiBinding.setMembers(new ArrayList<>(binding.getMembers())); + if (binding.getCondition() != null) { + Expr expr = new Expr(); + expr.setTitle(binding.getCondition().getTitle()); + expr.setDescription(binding.getCondition().getDescription()); + expr.setExpression(binding.getCondition().getExpression()); + apiBinding.setCondition(expr); } - bindings.add(new Bindings().setMembers(members).setRole(entry.getKey().getValue())); + bindings.add(apiBinding); } return new com.google.api.services.storage.model.Policy() .setBindings(bindings) - .setEtag(policy.getEtag()); + .setEtag(policy.getEtag()) + .setVersion(policy.getVersion()); } private PolicyHelper() { 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 6c1949f38..c95213d85 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 @@ -269,6 +269,10 @@ public static BucketSourceOption metagenerationNotMatch(long metageneration) { public static BucketSourceOption userProject(String userProject) { return new BucketSourceOption(StorageRpc.Option.USER_PROJECT, userProject); } + + public static BucketSourceOption requestedPolicyVersion(long version) { + return new BucketSourceOption(StorageRpc.Option.REQUESTED_POLICY_VERSION, version); + } } /** Class for specifying listHmacKeys options */ diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index afd45ae01..af0166733 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -1376,11 +1376,16 @@ public Policy getIamPolicy(String bucket, Map options) { Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_GET_BUCKET_IAM_POLICY); Scope scope = tracer.withSpan(span); try { - return storage - .buckets() - .getIamPolicy(bucket) - .setUserProject(Option.USER_PROJECT.getString(options)) - .execute(); + Storage.Buckets.GetIamPolicy getIamPolicy = + storage + .buckets() + .getIamPolicy(bucket) + .setUserProject(Option.USER_PROJECT.getString(options)); + if (null != Option.REQUESTED_POLICY_VERSION.getLong(options)) { + getIamPolicy.setOptionsRequestedPolicyVersion( + Option.REQUESTED_POLICY_VERSION.getLong(options).intValue()); + } + return getIamPolicy.execute(); } catch (IOException ex) { span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage())); throw translate(ex); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java index 439d4b175..36c7a5ff1 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java @@ -64,7 +64,8 @@ enum Option { USER_PROJECT("userProject"), KMS_KEY_NAME("kmsKeyName"), SERVICE_ACCOUNT_EMAIL("serviceAccount"), - SHOW_DELETED_KEYS("showDeletedKeys"); + SHOW_DELETED_KEYS("showDeletedKeys"), + REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"); private final String value; diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/PolicyHelperTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/PolicyHelperTest.java index 2e50797e2..dd5c0f78a 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/PolicyHelperTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/PolicyHelperTest.java @@ -41,6 +41,7 @@ public void testEquivalence() { Identity.user("test1@gmail.com"), Identity.user("test2@gmail.com")) .setEtag(ETAG) + .setVersion(1) .build(); com.google.api.services.storage.model.Policy apiPolicy = new com.google.api.services.storage.model.Policy() @@ -53,7 +54,8 @@ public void testEquivalence() { .setMembers( ImmutableList.of("user:test1@gmail.com", "user:test2@gmail.com")) .setRole("roles/storage.objectAdmin"))) - .setEtag(ETAG); + .setEtag(ETAG) + .setVersion(1); Policy actualLibPolicy = PolicyHelper.convertFromApiPolicy(apiPolicy); com.google.api.services.storage.model.Policy actualApiPolicy = diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 5c809c7b6..e1dd06e4f 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -288,6 +288,7 @@ public class StorageImplTest { Identity.user("test1@gmail.com"), Identity.user("test2@gmail.com")) .setEtag(POLICY_ETAG1) + .setVersion(1) .build(); private static final ServiceAccount SERVICE_ACCOUNT = ServiceAccount.of("test@google.com"); @@ -302,7 +303,8 @@ public class StorageImplTest { new Bindings() .setMembers(ImmutableList.of("user:test1@gmail.com", "user:test2@gmail.com")) .setRole("roles/storage.objectAdmin"))) - .setEtag(POLICY_ETAG1); + .setEtag(POLICY_ETAG1) + .setVersion(1); private static final String PRIVATE_KEY_STRING = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoG" @@ -2891,7 +2893,8 @@ public void testSetIamPolicy() { new Bindings() .setMembers(ImmutableList.of("group:test-group@gmail.com")) .setRole("roles/storage.admin"))) - .setEtag(POLICY_ETAG1); + .setEtag(POLICY_ETAG1) + .setVersion(1); // postCommitApiPolicy is identical but for the etag, which has been updated. com.google.api.services.storage.model.Policy postCommitApiPolicy = new com.google.api.services.storage.model.Policy() @@ -2907,7 +2910,8 @@ public void testSetIamPolicy() { new Bindings() .setMembers(ImmutableList.of("group:test-group@gmail.com")) .setRole("roles/storage.admin"))) - .setEtag(POLICY_ETAG2); + .setEtag(POLICY_ETAG2) + .setVersion(1); Policy postCommitLibPolicy = Policy.newBuilder() .addIdentity(StorageRoles.objectViewer(), Identity.allUsers()) @@ -2917,6 +2921,7 @@ public void testSetIamPolicy() { Identity.user("test2@gmail.com")) .addIdentity(StorageRoles.admin(), Identity.group("test-group@gmail.com")) .setEtag(POLICY_ETAG2) + .setVersion(1) .build(); EasyMock.expect(storageRpcMock.getIamPolicy(BUCKET_NAME1, EMPTY_RPC_OPTIONS)) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java index 7aad8a740..f9b45c870 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java @@ -35,6 +35,7 @@ import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.Condition; import com.google.cloud.Identity; import com.google.cloud.Policy; import com.google.cloud.ReadChannel; @@ -103,6 +104,7 @@ import java.net.URLConnection; import java.nio.ByteBuffer; import java.security.Key; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -2341,26 +2343,74 @@ public void testReadCompressedBlob() throws IOException { } @Test - public void testBucketPolicy() { - testBucketPolicyRequesterPays(true); - testBucketPolicyRequesterPays(false); - } - - private void testBucketPolicyRequesterPays(boolean requesterPays) { - if (requesterPays) { - Bucket remoteBucket = storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID)); - assertNull(remoteBucket.requesterPays()); - remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build(); - Bucket updatedBucket = storage.update(remoteBucket); - assertTrue(updatedBucket.requesterPays()); - } + public void testBucketPolicyV1RequesterPays() { + Bucket remoteBucket = storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID)); + assertNull(remoteBucket.requesterPays()); + remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build(); + Bucket updatedBucket = storage.update(remoteBucket); + assertTrue(updatedBucket.requesterPays()); String projectId = remoteStorageHelper.getOptions().getProjectId(); Storage.BucketSourceOption[] bucketOptions = - requesterPays - ? new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)} - : new Storage.BucketSourceOption[] {}; + 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(), + new HashSet<>(Arrays.asList(projectOwner, projectEditor)), + StorageRoles.legacyBucketReader(), + (Set) new HashSet<>(Collections.singleton(projectViewer))); + Map> bindingsWithPublicRead = + ImmutableMap.of( + StorageRoles.legacyBucketOwner(), + new HashSet<>(Arrays.asList(projectOwner, projectEditor)), + StorageRoles.legacyBucketReader(), + new HashSet<>(Collections.singleton(projectViewer)), + StorageRoles.legacyObjectReader(), + (Set) new HashSet<>(Collections.singleton(Identity.allUsers()))); + + // Validate getting policy. + Policy currentPolicy = storage.getIamPolicy(BUCKET, bucketOptions); + assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings()); + + // Validate updating policy. + Policy updatedPolicy = + storage.setIamPolicy( + BUCKET, + currentPolicy + .toBuilder() + .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) + .build(), + bucketOptions); + assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings()); + Policy revertedPolicy = + storage.setIamPolicy( + BUCKET, + 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, + ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), + bucketOptions)); + } + + @Test + public void testBucketPolicyV1() { + String projectId = remoteStorageHelper.getOptions().getProjectId(); + + Storage.BucketSourceOption[] bucketOptions = new Storage.BucketSourceOption[] {}; Identity projectOwner = Identity.projectOwner(projectId); Identity projectEditor = Identity.projectEditor(projectId); Identity projectViewer = Identity.projectViewer(projectId); @@ -2413,6 +2463,162 @@ private void testBucketPolicyRequesterPays(boolean requesterPays) { bucketOptions)); } + @Test + public void testBucketPolicyV3() { + // Enable Uniform Bucket-Level Access + storage.update( + BucketInfo.newBuilder(BUCKET) + .setIamConfiguration( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(true) + .build()) + .build()); + String projectId = remoteStorageHelper.getOptions().getProjectId(); + + 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()); + + // Validate getting policy. + Policy currentPolicy = storage.getIamPolicy(BUCKET, bucketOptions); + 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, 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; + } + } + + Policy revertedPolicy = + storage.setIamPolicy( + BUCKET, 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, + 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, + conditionalPolicy.toBuilder().setBindings(updatedBindings).setVersion(3).build(), + bucketOptions); + + // Validate testing permissions. + List expectedPermissions = ImmutableList.of(true, true); + assertEquals( + expectedPermissions, + storage.testIamPermissions( + BUCKET, + ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), + bucketOptions)); + + // Disable Uniform Bucket-Level Access + storage.update( + BucketInfo.newBuilder(BUCKET) + .setIamConfiguration( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(false) + .build()) + .build()); + } + @Test public void testUpdateBucketLabel() { Bucket remoteBucket = storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID));