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 b67c5f3a2b..b623f2b97c 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 @@ -67,6 +67,7 @@ import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; import com.google.cloud.storage.UnifiedOpts.Opts; import com.google.cloud.storage.UnifiedOpts.ProjectId; +import com.google.cloud.storage.UnifiedOpts.UserProject; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -159,11 +160,15 @@ final class GrpcStorageImpl extends BaseService implements Stora final GrpcRetryAlgorithmManager retryAlgorithmManager; final SyntaxDecoders syntaxDecoders; + // workaround for https://github.com/googleapis/java-storage/issues/1736 + private final Opts defaultOpts; @Deprecated private final ProjectId defaultProjectId; - GrpcStorageImpl(GrpcStorageOptions options, StorageClient storageClient) { + GrpcStorageImpl( + GrpcStorageOptions options, StorageClient storageClient, Opts defaultOpts) { super(options); this.storageClient = storageClient; + this.defaultOpts = defaultOpts; this.codecs = Conversions.grpc(); this.retryAlgorithmManager = options.getRetryAlgorithmManager(); this.syntaxDecoders = new SyntaxDecoders(); @@ -182,7 +187,7 @@ public void close() throws Exception { @Override public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(bucketInfo); + Opts opts = Opts.unwrap(options).resolveFrom(bucketInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo); @@ -215,7 +220,7 @@ public Blob create( BlobInfo blobInfo, byte[] content, int offset, int length, BlobTargetOption... options) { requireNonNull(blobInfo, "blobInfo must be non null"); requireNonNull(content, "content must be non null"); - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo); + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); @@ -267,7 +272,7 @@ public Blob createFrom(BlobInfo blobInfo, Path path, int bufferSize, BlobWriteOp throw new StorageException(0, path + " is a directory"); } - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo); + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); @@ -335,7 +340,7 @@ public Blob createFrom( throws IOException { requireNonNull(blobInfo, "blobInfo must be non null"); - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo); + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); @@ -369,7 +374,7 @@ public Blob createFrom( @Override public Bucket get(String bucket, BucketGetOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetBucketRequest.Builder builder = @@ -384,7 +389,7 @@ public Bucket get(String bucket, BucketGetOption... options) { @Override public Bucket lockRetentionPolicy(BucketInfo bucket, BucketTargetOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(bucket); + Opts opts = Opts.unwrap(options).resolveFrom(bucket).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); LockBucketRetentionPolicyRequest.Builder builder = @@ -406,7 +411,7 @@ public Blob get(String bucket, String blob, BlobGetOption... options) { @Override public Blob get(BlobId blob, BlobGetOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(blob); + Opts opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetObjectRequest.Builder builder = @@ -435,7 +440,7 @@ public Blob get(BlobId blob) { @Override public Page list(BucketListOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); ListBucketsRequest request = @@ -462,7 +467,7 @@ public Page list(BucketListOption... options) { @Override public Page list(String bucket, BlobListOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); ListObjectsRequest.Builder builder = @@ -481,7 +486,7 @@ public Page list(String bucket, BlobListOption... options) { @Override public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(bucketInfo); + Opts opts = Opts.unwrap(options).resolveFrom(bucketInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo); @@ -503,7 +508,7 @@ public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) { @Override public Blob update(BlobInfo blobInfo, BlobTargetOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo); + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); Object object = codecs.blobInfo().encode(blobInfo); @@ -530,7 +535,7 @@ public Blob update(BlobInfo blobInfo) { @Override public boolean delete(String bucket, BucketSourceOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); DeleteBucketRequest.Builder builder = @@ -555,7 +560,7 @@ public boolean delete(String bucket, String blob, BlobSourceOption... options) { @Override public boolean delete(BlobId blob, BlobSourceOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(blob); + Opts opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); DeleteObjectRequest.Builder builder = @@ -587,7 +592,9 @@ public boolean delete(BlobId blob) { @Override public Blob compose(ComposeRequest composeRequest) { Opts opts = - Opts.unwrap(composeRequest.getTargetOptions()).resolveFrom(composeRequest.getTarget()); + Opts.unwrap(composeRequest.getTargetOptions()) + .resolveFrom(composeRequest.getTarget()) + .prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); ComposeObjectRequest.Builder builder = ComposeObjectRequest.newBuilder(); @@ -609,8 +616,12 @@ public CopyWriter copy(CopyRequest copyRequest) { BlobId src = copyRequest.getSource(); BlobInfo dst = copyRequest.getTarget(); Opts srcOpts = - Opts.unwrap(copyRequest.getSourceOptions()).projectAsSource().resolveFrom(src); - Opts dstOpts = Opts.unwrap(copyRequest.getTargetOptions()).resolveFrom(dst); + Opts.unwrap(copyRequest.getSourceOptions()) + .projectAsSource() + .resolveFrom(src) + .prepend(defaultOpts); + Opts dstOpts = + Opts.unwrap(copyRequest.getTargetOptions()).resolveFrom(dst).prepend(defaultOpts); Mapper mapper = srcOpts.rewriteObjectsRequest().andThen(dstOpts.rewriteObjectsRequest()); @@ -684,7 +695,7 @@ public GrpcBlobReadChannel reader(String bucket, String blob, BlobSourceOption.. @Override public GrpcBlobReadChannel reader(BlobId blob, BlobSourceOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(blob); + Opts opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts); ReadObjectRequest request = getReadObjectRequest(blob, opts); Set codes = resultRetryAlgorithmToCodes(retryAlgorithmManager.getFor(request)); GrpcCallContext grpcCallContext = GrpcCallContext.createDefault().withRetryableCodes(codes); @@ -722,7 +733,7 @@ public void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption. @Override public GrpcBlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) { - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo); + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); @@ -844,7 +855,7 @@ public List delete(Iterable blobIds) { @Override public Acl getAcl(String bucket, Entity entity, BucketSourceOption... options) { try { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); com.google.storage.v2.Bucket resp = getBucketWithAcls(bucket, opts); Predicate entityPredicate = @@ -874,7 +885,7 @@ public Acl getAcl(String bucket, Entity entity) { @Override public boolean deleteAcl(String bucket, Entity entity, BucketSourceOption... options) { try { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); com.google.storage.v2.Bucket resp = getBucketWithAcls(bucket, opts); String encode = codecs.entity().encode(entity); @@ -928,7 +939,7 @@ public Acl createAcl(String bucket, Acl acl) { @Override public Acl updateAcl(String bucket, Acl acl, BucketSourceOption... options) { try { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); com.google.storage.v2.Bucket resp = getBucketWithAcls(bucket, opts); BucketAccessControl encode = codecs.bucketAcl().encode(acl); String entity = encode.getEntity(); @@ -966,7 +977,7 @@ public Acl updateAcl(String bucket, Acl acl) { @Override public List listAcls(String bucket, BucketSourceOption... options) { try { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); com.google.storage.v2.Bucket resp = getBucketWithAcls(bucket, opts); return resp.getAclList().stream() .map(codecs.bucketAcl()::decode) @@ -1211,7 +1222,7 @@ public List listAcls(BlobId blob) { @Override public HmacKey createHmacKey(ServiceAccount serviceAccount, CreateHmacKeyOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); CreateHmacKeyRequest request = @@ -1236,7 +1247,7 @@ public HmacKey createHmacKey(ServiceAccount serviceAccount, CreateHmacKeyOption. @Override public Page listHmacKeys(ListHmacKeysOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); @@ -1264,7 +1275,7 @@ public Page listHmacKeys(ListHmacKeysOption... options) { @Override public HmacKeyMetadata getHmacKey(String accessId, GetHmacKeyOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetHmacKeyRequest request = @@ -1283,7 +1294,7 @@ public HmacKeyMetadata getHmacKey(String accessId, GetHmacKeyOption... options) @Override public void deleteHmacKey(HmacKeyMetadata hmacKeyMetadata, DeleteHmacKeyOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); DeleteHmacKeyRequest req = @@ -1304,7 +1315,7 @@ public void deleteHmacKey(HmacKeyMetadata hmacKeyMetadata, DeleteHmacKeyOption.. @Override public HmacKeyMetadata updateHmacKeyState( HmacKeyMetadata hmacKeyMetadata, HmacKeyState state, UpdateHmacKeyOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); com.google.storage.v2.HmacKeyMetadata encode = @@ -1323,7 +1334,7 @@ public HmacKeyMetadata updateHmacKeyState( @Override public Policy getIamPolicy(String bucket, BucketSourceOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetIamPolicyRequest.Builder builder = @@ -1338,7 +1349,7 @@ public Policy getIamPolicy(String bucket, BucketSourceOption... options) { @Override public Policy setIamPolicy(String bucket, Policy policy, BucketSourceOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); SetIamPolicyRequest req = @@ -1356,7 +1367,7 @@ public Policy setIamPolicy(String bucket, Policy policy, BucketSourceOption... o @Override public List testIamPermissions( String bucket, List permissions, BucketSourceOption... options) { - Opts opts = Opts.unwrap(options); + Opts opts = Opts.unwrap(options).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); TestIamPermissionsRequest req = @@ -1706,7 +1717,7 @@ private WriteObjectRequest getWriteObjectRequest(BlobInfo info, Opts unbufferedReadSession( BlobId blob, BlobSourceOption[] options) { - Opts opts = Opts.unwrap(options).resolveFrom(blob); + Opts opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts); ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts); Set codes = resultRetryAlgorithmToCodes(retryAlgorithmManager.getFor(readObjectRequest)); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index d78eed0e6e..f001483eaf 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -31,15 +31,19 @@ import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.NoHeaderProvider; import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.auth.Credentials; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceFactory; import com.google.cloud.ServiceOptions; import com.google.cloud.ServiceRpc; import com.google.cloud.TransportOptions; +import com.google.cloud.Tuple; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spi.ServiceRpcFactory; import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.cloud.storage.UnifiedOpts.UserProject; import com.google.cloud.storage.spi.StorageRpcFactory; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableSet; @@ -50,6 +54,10 @@ import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.net.URI; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -99,6 +107,41 @@ Duration getTerminationAwaitDuration() { @InternalApi StorageSettings getStorageSettings() throws IOException { + return resolveSettingsAndOpts().x(); + } + + /** + * We have to perform several introspections and detections to cross-wire/support several features + * that are either gapic primitives, ServieOption primitives or GCS semantic requirements. + * + *

Requester Pays, {@code quota_project_id} and {@code userProject}

+ * + * When using the JSON Api operations destined for requester pays buckets can identify the project + * for billing and quota attribution by specifying either {@code userProject} query parameter or + * {@code x-goog-user-project} HTTP Header. + * + *

If the credentials being used contain the property {@code quota_project_id} this value will + * automatically be set to the {@code x-goog-user-project} header for both JSON and GAPIC. In the + * case of JSON this isn't an issue, as any {@code userProject} query parameter takes precedence. + * However, in gRPC/GAPIC there isn't a {@code userProject} query parameter, instead we are adding + * {@code x-goog-user-project} to the request context as metadata. If the credentials set the + * request metadata and we set the request metadata it results in two different entries in the + * request. This creates ambiguity for GCS which then rejects the request. + * + *

To account for this and to provide a similarly level of precedence we are introspecting the + * credentials and service options to save any {@code quota_project_id} into an {@link + * UserProject} which is then used by {@link GrpcStorageImpl} to resolve individual request + * metadata. + * + *

The precedence we want to provide is as follows

+ * + *
    + *
  1. Any "userProject" Option provided to an individual method + *
  2. Any Non-empty value for {@link #getQuotaProjectId()} + *
  3. Any {@code x-goog-user-project} provided by {@link #credentials} + *
+ */ + private Tuple> resolveSettingsAndOpts() throws IOException { String endpoint = getHost(); URI uri = URI.create(endpoint); String scheme = uri.getScheme(); @@ -114,11 +157,33 @@ StorageSettings getStorageSettings() throws IOException { break; } + Opts defaultOpts = Opts.empty(); CredentialsProvider credentialsProvider; if (credentials instanceof NoCredentials) { credentialsProvider = NoCredentialsProvider.create(); } else { - credentialsProvider = FixedCredentialsProvider.create(credentials); + boolean foundQuotaProject = false; + if (credentials.hasRequestMetadata()) { + Map> requestMetadata = credentials.getRequestMetadata(uri); + for (Entry> e : requestMetadata.entrySet()) { + String key = e.getKey(); + if ("x-goog-user-project".equals(key.trim().toLowerCase(Locale.ENGLISH))) { + List value = e.getValue(); + if (!value.isEmpty()) { + foundQuotaProject = true; + defaultOpts = Opts.from(UnifiedOpts.userProject(value.get(0))); + break; + } + } + } + } + if (foundQuotaProject) { + // fix for https://github.com/googleapis/java-storage/issues/1736 + credentialsProvider = + FixedCredentialsProvider.create(new QuotaProjectIdHidingCredentials(credentials)); + } else { + credentialsProvider = FixedCredentialsProvider.create(credentials); + } } HeaderProvider internalHeaderProvider = @@ -135,6 +200,12 @@ StorageSettings getStorageSettings() throws IOException { .setCredentialsProvider(credentialsProvider) .setClock(getClock()); + // this MUST come after credentials, service options set value has higher priority than creds + String quotaProjectId = this.getQuotaProjectId(); + if (quotaProjectId != null && !quotaProjectId.isEmpty()) { + defaultOpts = Opts.from(UnifiedOpts.userProject(quotaProjectId)); + } + builder.setHeaderProvider(this.getMergedHeaderProvider(new NoHeaderProvider())); InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = @@ -192,7 +263,7 @@ StorageSettings getStorageSettings() throws IOException { // this is totally valid. instead we want to monitor if the stream is doing work and if not // timeout. .setIdleTimeout(totalTimeout); - return builder.build(); + return Tuple.of(builder.build(), defaultOpts); } /** @since 2.14.0 This new api is in preview and is subject to breaking changes. */ @@ -502,8 +573,11 @@ public Storage create(StorageOptions options) { if (options instanceof GrpcStorageOptions) { GrpcStorageOptions grpcStorageOptions = (GrpcStorageOptions) options; try { - StorageSettings storageSettings = grpcStorageOptions.getStorageSettings(); - return new GrpcStorageImpl(grpcStorageOptions, StorageClient.create(storageSettings)); + Tuple> t = grpcStorageOptions.resolveSettingsAndOpts(); + StorageSettings storageSettings = t.x(); + Opts defaultOpts = t.y(); + return new GrpcStorageImpl( + grpcStorageOptions, StorageClient.create(storageSettings), defaultOpts); } catch (IOException e) { throw new IllegalStateException( "Unable to instantiate gRPC com.google.cloud.storage.Storage client.", e); 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 6e78001b24..535c645c65 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 @@ -2288,6 +2288,22 @@ Decoder clearBucketFields() { .orElse(Decoder.identity()); } + Opts prepend(Opts l) { + Opts right = this; + Set> rightClasses = + right.opts.stream().map(Opt::getClass).collect(Collectors.toSet()); + + ImmutableList list = + Stream.of( + l.opts.stream() + // remove opts which are defined on the right hand size + .filter(o -> !rightClasses.contains(o.getClass())), + right.opts.stream()) + .flatMap(x -> x) + .collect(ImmutableList.toImmutableList()); + return new Opts<>(list); + } + private Mapper> rpcOptionMapper() { return fuseMappers(RpcOptVal.class, RpcOptVal::mapper); } @@ -2310,6 +2326,10 @@ static Opts from(T... ts) { return new Opts<>(ImmutableList.copyOf(ts)); } + static Opts empty() { + return new Opts<>(ImmutableList.of()); + } + /** * Given an array of OptionShim, extract the opt from each of them to construct a new instance * of {@link Opts} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java index 9b346aca26..d22b18294d 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java @@ -22,6 +22,7 @@ import com.google.cloud.storage.PostPolicyV4.PostConditionsV4; import com.google.cloud.storage.PostPolicyV4.PostFieldsV4; import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.UnifiedOpts.Opts; import com.google.common.collect.ImmutableList; import java.util.function.Supplier; import java.util.stream.Stream; @@ -37,7 +38,7 @@ public void verifyUnsupportedMethodsGenerateMeaningfulException() { .setCredentials(NoCredentials.getInstance()) .build(); @SuppressWarnings("resource") - Storage s = new GrpcStorageImpl(options, null); + Storage s = new GrpcStorageImpl(options, null, Opts.empty()); ImmutableList messages = Stream.>of( s::batch, diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITQuotaProjectIdTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITQuotaProjectIdTest.java new file mode 100644 index 0000000000..9a1a9fc159 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITQuotaProjectIdTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2023 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 static org.junit.Assume.assumeTrue; + +import com.google.api.gax.paging.Page; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.StorageOptions; +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.ObjectsFixture; +import java.util.stream.StreamSupport; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@CrossRun( + backends = Backend.PROD, + transports = {Transport.HTTP, Transport.GRPC}) +public final class ITQuotaProjectIdTest { + // a project number to a dev project which is not used for CI + // this value is the negative case, and points to a project which doesn't have access to the + // bucket used here. + private static final String BAD_PROJECT_ID = "954355001984"; + + @Inject public Storage storage; + + @Inject + @BucketFixture(BucketType.REQUESTER_PAYS) + public BucketInfo bucket; + + // make sure there is an object in the bucket to be listed + @Inject + @BucketFixture(BucketType.REQUESTER_PAYS) + public ObjectsFixture objectsFixture; + + private StorageOptions baseOptions; + private String projectId; + private GoogleCredentials credentials; + + @Before + public void setUp() throws Exception { + baseOptions = storage.getOptions(); + assumeTrue( + "These tests require GoogleCredentials", + baseOptions.getCredentials() instanceof GoogleCredentials); + credentials = (GoogleCredentials) baseOptions.getCredentials(); + projectId = baseOptions.getProjectId(); + } + + /* + * UserProject precedence + * 1. Method userProject Option + * 2. ServiceOptions.getQuotaProjectId() + * 3. Credentials.quota_project_id + */ + + @Test + public void fromCredentials() throws Exception { + StorageOptions build = + baseOptions + .toBuilder() + .setCredentials(credentialsWithQuotaProjectId(credentials, projectId)) + .build(); + + try (Storage s = build.getService()) { + Page page = s.list(bucket.getName()); + assertPage(page); + } + } + + @Test + public void methodOptionOverCredentials() throws Exception { + StorageOptions build = + baseOptions + .toBuilder() + .setCredentials(credentialsWithQuotaProjectId(credentials, BAD_PROJECT_ID)) + .build(); + + try (Storage s = build.getService()) { + Page page = s.list(bucket.getName(), BlobListOption.userProject(projectId)); + assertPage(page); + } + } + + @Test + public void fromServiceOptionParameter() throws Exception { + StorageOptions build = baseOptions.toBuilder().setQuotaProjectId(projectId).build(); + + try (Storage s = build.getService()) { + Page page = s.list(bucket.getName()); + assertPage(page); + } + } + + @Test + public void serviceOptionParameterOverCredentials() throws Exception { + StorageOptions build = + baseOptions + .toBuilder() + .setCredentials(credentialsWithQuotaProjectId(credentials, BAD_PROJECT_ID)) + .setQuotaProjectId(projectId) + .build(); + + try (Storage s = build.getService()) { + Page page = s.list(bucket.getName()); + assertPage(page); + } + } + + @Test + public void methodOptionOverServiceOptionParameter() throws Exception { + StorageOptions build = baseOptions.toBuilder().setQuotaProjectId(BAD_PROJECT_ID).build(); + + try (Storage s = build.getService()) { + Page page = s.list(bucket.getName(), BlobListOption.userProject(projectId)); + assertPage(page); + } + } + + private void assertPage(Page page) { + boolean info1InResults = + StreamSupport.stream(page.iterateAll().spliterator(), false) + .map(Blob::getName) + .anyMatch(objectsFixture.getInfo1().getName()::equals); + assertThat(info1InResults).isTrue(); + } + + private GoogleCredentials credentialsWithQuotaProjectId( + GoogleCredentials creds, String quotaProjectId) { + return creds.toBuilder().setQuotaProjectId(quotaProjectId).build(); + } +}