googleapis / google-cloud-java

Google Cloud Client Library for Java
https://cloud.google.com/java/docs/reference
Apache License 2.0
1.9k stars 1.07k forks source link

Generate signed URLs when using workload identity in Java #10464

Open pedrosdf opened 8 months ago

pedrosdf commented 8 months ago

Thanks for stopping by to let us know something could be better!

PLEASE READ: If you have a support contract with Google, please create an issue in the support console instead of filing on GitHub. This will ensure a timely response.

Is your feature request related to a problem? Please describe. [Storage] We are opening this Feature Request as we want to be able to easily generate signed URLs when using workload identity. We understand that this is a known missing feature and it is being worked on through other GitHub feature requests for .net [1] and ruby [2], but we want this functionality to also be available for java.

Describe the solution you'd like We found a recent Google Cloud Collective response in a Stack Overflow issue [3] explaining the following: “External account credentials (Workload ID) are not supported as URL signers and you need to use the IAM service to sign the blob yourself. External account credentials are not currently supported for URL signing because it's not always possible to know client side which service account the credential maps back to, and that's a requirement (we would be calling the IAM service internally for this).” We know this is already being worked on and there are some workarounds for .net [1] and ruby [2], therefore, we want to have a resolution/workaround on the Java side as well.

Describe alternatives you've considered Tried using this example [4] by adapting it to Java, but started receiving the following exception:

Exception in thread "main" java.lang.IllegalStateException: Signing key was not provided and could not be derived
    at com.google.common.base.Preconditions.checkState(Preconditions.java:512)
    at com.google.cloud.storage.StorageImpl.signUrl(StorageImpl.java:709)
    at com.ayla.cloud.gcs.controller.GCSController.getDownloadURL(GCSController.java:70)
    at com.ayla.cloud.gcs.controller.GCSController.greeting(GCSController.java:42)
    at com.ayla.cloud.gcs.controller.GCSController.main(GCSController.java:49)

This is the code being used:

package com.ayla.cloud.gcs.controller;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public class GCSController {
    public String greeting() {
        String bucketName = "";
        String path = "example2/example-1001";
        long ttl = 60;
        String gcsSignatureVersion = "V4";
        ComputeEngineCredentials caCreds = ComputeEngineCredentials.create();
        String targetAudience = "https://example.com";
        IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder() .setIdTokenProvider(caCreds)
            .setTargetAudience(targetAudience)
            .setOptions(Arrays.asList(ComputeEngineCredentials.Option.FORMAT_FULL)) .build();
        URL url = getDownloadURL(bucketName, path, ttl, gcsSignatureVersion, tokenCredential);
        return url.toString();
    }

    public static void main(String[] args) throws IOException {
        System.out.println(new GCSController().greeting());
    }

    public URL getDownloadURL(String bucketName, String path, long ttl, final String gcsSignatureVersion, IdTokenCredentials tokenCredential) {
        BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(bucketName, path)).build();
        Storage.SignUrlOption signUrlOption = null;

        switch (gcsSignatureVersion) {
            case "V2":
                signUrlOption = Storage.SignUrlOption.withV2Signature();
                break;
            case "V4":
            default:
                signUrlOption = Storage.SignUrlOption.withV4Signature();
        }
        if (null == signUrlOption) {
            return null;
        }
        Storage storage = StorageOptions.newBuilder().setCredentials(tokenCredential).build().getService();
        return storage.signUrl(blobInfo, ttl, TimeUnit.MINUTES, signUrlOption);
    }
}

Additional context [1] https://github.com/googleapis/google-api-dotnet-client/issues/2410 [2] https://github.com/googleapis/google-cloud-ruby/issues/13307 [3] https://stackoverflow.com/a/76266912 [4] https://gist.github.com/jezhumble/91051485db4462add82045ef9ac2a0ec

zhumin8 commented 8 months ago

Feature request to java-storage. @BenWhitehead Do you mind taking a look?

BenWhitehead commented 8 months ago

The code in google-cloud-storage to sign urls depends upon the value provided in com.google.cloud.storage.Storage.SignUrlOption#signWith, if that is not provided then it will attempt to fallback and use the instance of credentials provided at client creation time. The library specifically invokes com.google.auth.ServiceAccountSigner#sign(byte[]) here.

In this case it sounds like the type of credentials provided doesn't know how to sign.

I think this is actually an issue that would need to be addressed in google-auth-library, rather than the storage library code itself.

@timursadykov Do you know if there is anything already on the roadmap to implement the sign method for credentials that work with workload identity?

TimurSadykov commented 7 months ago

ack, I'll chat with the team that owns WIF