-
Notifications
You must be signed in to change notification settings - Fork 75
Closed as not planned
Closed as not planned
Copy link
Labels
priority: p3Desirable enhancement or fix. May not be included in next release.Desirable enhancement or fix. May not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Description
@mohanli-ml can you help with this issue?
cc: @danielduhh , @sydney-munro , and @arunkumarchacko
Environment details
- OS type and version:
- Java version:
- artifact version(s): com.google.api:gax-grpc:jar:2.39.0, com.google.cloud:google-cloud-storage:2.31.0
Steps to reproduce
Run the following example in a GCE instance:
package org.example;
import com.google.cloud.NoCredentials;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
public class FailureExample {
public static void main(String[] args) throws Exception {
// GCS client converts NoCredentials type to NoCredentialsProvider
// and hands that over to underlying Storage v2 GAPIC
// https://github.com/googleapis/java-storage/blob/main/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java#L175-L177
Storage storage = StorageOptions.grpc()
.setAttemptDirectPath(true)
.setCredentials(NoCredentials.getInstance())
.build().getService();
}
}Stack trace
Exception in thread "main" java.lang.NullPointerException: creds
at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:921)
at io.grpc.auth.GoogleAuthLibraryCallCredentials.<init>(GoogleAuthLibraryCallCredentials.java:74)
at io.grpc.auth.GoogleAuthLibraryCallCredentials.<init>(GoogleAuthLibraryCallCredentials.java:69)
at io.grpc.auth.MoreCallCredentials.from(MoreCallCredentials.java:35)
at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.createSingleChannel(InstantiatingGrpcChannelProvider.java:371)
at com.google.api.gax.grpc.ChannelPool.<init>(ChannelPool.java:107)
at com.google.api.gax.grpc.ChannelPool.create(ChannelPool.java:85)
at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.createChannel(InstantiatingGrpcChannelProvider.java:243)
at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.getTransportChannel(InstantiatingGrpcChannelProvider.java:237)
at com.google.api.gax.rpc.ClientContext.create(ClientContext.java:226)
at com.google.storage.v2.stub.GrpcStorageStub.create(GrpcStorageStub.java:535)
at com.google.storage.v2.stub.StorageStubSettings.createStub(StorageStubSettings.java:622)
at com.google.storage.v2.StorageClient.<init>(StorageClient.java:166)
at com.google.storage.v2.StorageClient.create(StorageClient.java:149)
at com.google.cloud.storage.GrpcStorageOptions$GrpcStorageFactory.create(GrpcStorageOptions.java:663)
at com.google.cloud.storage.GrpcStorageOptions$GrpcStorageFactory.create(GrpcStorageOptions.java:627)
at com.google.cloud.ServiceOptions.getService(ServiceOptions.java:563)
at org.example.ArunFailureSimple.main(ArunFailureSimple.java:28)
Potential issue
Digging into the stacktrace, I found that in InstantiatingGrpcChannelProvider.java, there's logic to handle null creds when not using DirectPath. However, when using DirectPath, no handling for null creds exists and is passed along as-is which I think is causing the NPE.
E2E Code example
Following is Downscoped Token example that reads data from GCS using DirectPath:
package org.example;
import com.google.auth.Credentials;
import static com.google.auth.http.AuthHttpConstants.AUTHORIZATION;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.DownscopedCredentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.NoCredentials;
import com.google.cloud.storage.*;
import com.google.common.collect.ImmutableList;
import com.google.storage.v2.BucketName;
import com.google.storage.v2.GetObjectRequest;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import java.io.IOException;
public class FailureWithCABToken {
private static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
throws IOException {
// Retrieve the source credentials from ADC.
GoogleCredentials sourceCredentials =
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");
// Initialize the Credential Access Boundary rules.
String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;
// Downscoped credentials will have readonly access to the resource.
String availablePermission = "inRole:roles/storage.objectViewer";
// Only objects starting with the specified prefix string in the object name will be allowed
// read access.
String expression =
"resource.name.startsWith('projects/_/buckets/"
+ bucketName
+ "/objects/"
+ objectPrefix
+ "')";
// Build the AvailabilityCondition.
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
.setExpression(expression)
.build();
// Define the single access boundary rule using the above properties.
CredentialAccessBoundary.AccessBoundaryRule rule =
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
.setAvailableResource(availableResource)
.addAvailablePermission(availablePermission)
.setAvailabilityCondition(availabilityCondition)
.build();
// Define the Credential Access Boundary with all the relevant rules.
CredentialAccessBoundary credentialAccessBoundary =
CredentialAccessBoundary.newBuilder().addRule(rule).build();
// Create the downscoped credentials.
DownscopedCredentials downscopedCredentials =
DownscopedCredentials.newBuilder()
.setSourceCredential(sourceCredentials)
.setCredentialAccessBoundary(credentialAccessBoundary)
.build();
// Retrieve the token.
// This will need to be passed to the Token Consumer.
AccessToken accessToken = downscopedCredentials.refreshAccessToken();
return accessToken;
}
private static class DownscopedTokenByRequestInterceptor implements ClientInterceptor {
public final Metadata.Key<String> AUTH_KEY =
Metadata.Key.of(AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
if (!method.getFullMethodName().equals("google.storage.v2.Storage/GetObject")) {
// Only support PCU based operations
return next.newCall(method, callOptions);
}
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
Listener responseListener;
Metadata headers;
int flowControlRequests;
String methodName;
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
this.responseListener = responseListener;
this.headers = headers;
this.methodName = method.getFullMethodName();
}
@Override
public void sendMessage(ReqT message) {
if (headers != null) { // start() is required before sendMessage()
try {
GetObjectRequest req = (GetObjectRequest)message;
String bucketName = BucketName.parse(req.getBucket()).getBucket();
String token = getTokenFromBroker(bucketName, req.getObject()).getTokenValue();
headers.put(AUTH_KEY, "Bearer " + token);
} catch (Exception e) {
halfClose();
}
delegate().start(responseListener, headers);
if (flowControlRequests != 0) {
super.request(flowControlRequests);
}
headers = null;
}
super.sendMessage(message);
}
@Override
public void request(int numMessages) {
if (headers != null) {
this.flowControlRequests += numMessages;
} else {
super.request(numMessages);
}
}
};
}
}
private static Credentials getNoCredentialsWorkaround() {
GoogleCredentials theCred = GoogleCredentials.create(new AccessToken("", null));
return theCred;
}
public static void main(String[] args) throws Exception {
Storage storage = StorageOptions.grpc()
.setAttemptDirectPath(true)
.setGrpcInterceptorProvider(() -> ImmutableList.of(new DownscopedTokenByRequestInterceptor()))
.setCredentials(NoCredentials.getInstance())
.build().getService();
Blob blob = storage.get("bucket-name", "object-name");
System.out.println("Downloaded blob?: " + (blob != null));
}
}Workaround
package org.example;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
public class ArunFailureSimple {
public static void main(String[] args) throws Exception {
Storage storage = StorageOptions.grpc()
.setAttemptDirectPath(true)
.setCredentials(GoogleCredentials.create(new AccessToken("", null)))
.build().getService();
}
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
priority: p3Desirable enhancement or fix. May not be included in next release.Desirable enhancement or fix. May not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.