get signed URLs for GCS objects#261
Conversation
| } | ||
|
|
||
| @Test | ||
| public void nullSignedUrlForNullBlob() throws IOException { |
There was a problem hiding this comment.
I think we need more tests to fully cover the method.
IOExceptionwhenprivateKeyPathis empty.- Verify that
this.storage.signUrlis called in the happy path.
There was a problem hiding this comment.
-
Will be not be nice to test, because env vars are not great to test with unit tests. Remember a few months ago we had issues with the CI stuff and tests that altered env vars. The GOOGLE_APPLICATION_CREDENTIALS env var is very widespread and set in stone, so I think this is the right way to access that private key JSON file. I think maybe we can just skip testing it. Even if this test would have failed, it's not a catastrophic or mysterious failure anyway.
-
Will do.
There was a problem hiding this comment.
- Why can't we rely on
spring.cloud.gcp.storage.credentials.location, which is easy to mock in the test?
There was a problem hiding this comment.
Isn't that one only in the starter project? Also, other than a unit test of minimal (if any) value, what's the purpose? GOOGLE_APPLICATION_CREDENTIALS is much more widely known, and is mentioned directly in the documentation in this repo (not just the starter projects as the spring.cloud.gcp.xxxxx.credentials.location props).
| to obtain a https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java[`Blob`]. | ||
| This type represents a GCS file, which has associated https://cloud.google.com/storage/docs/gsutil/addlhelp/WorkingWithObjectMetadata[metadata], such as content-type, that can be set. | ||
|
|
||
| The `getSignedUrl` method can also be used to obtain signed URLs for GCS objects. |
There was a problem hiding this comment.
getSignedUrl -> createSignedUrl
Also, link to the documentation about signed URLs: https://cloud.google.com/storage/docs/access-control/signed-urls
| can be cast as a `GoogleStorageResourceObject` and the `getGoogleStorageObject` method can be called | ||
| to obtain a https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java[`Blob`]. | ||
| This type represents a GCS file, which has associated https://cloud.google.com/storage/docs/gsutil/addlhelp/WorkingWithObjectMetadata[metadata], such as content-type, that can be set. | ||
| The `getSignedUrl` method can also be used to obtain signed URLs for GCS objects. |
| } | ||
|
|
||
| /** | ||
| * Gets a signed URL to an object if it exists. |
| * @return the URL if the object exists, and null if it does not. | ||
| * @throws IOException | ||
| */ | ||
| public URL getSignedUrl(TimeUnit timeUnit, int timePeriods) throws IOException { |
| if (blob == null) { | ||
| return null; | ||
| } | ||
| String privateKeyPath = System.getenv( |
There was a problem hiding this comment.
Can we use the auto-configured CredentialsProvider to get the private key instead? @joaoandremartins
There was a problem hiding this comment.
Doesn't look like there's a way to get a private key from com.google.auth.Credentials. I'm not a security guy, but perhaps it's intentional that the only copy of the private key you're supposed to have is from the JSON file you download just once when creating the service account in the UI.
There was a problem hiding this comment.
What about the GcpStorageProperties.credentials.location? We already have a mechanism for specifying the private key file. Why use the environment variable instead?
There was a problem hiding this comment.
See my other comment. The prop you're thinking of is actually only in the starter projects, and infact the larger repo-level docs here specify using the env var. The env var is also standard across any GCP code, so I don't feel it's out of place (it's also mentioned to be set in this repo's docs anyway).
There was a problem hiding this comment.
Actually I could make this path a member that we set in the constructor (like the auto-create setting last time), and that it would just default to GOOGLE_APPLICATION_CREDENTIALS. Still, we would have this env var involved, but we could at least use the path from the starter instead if it was provided.
|
@ChengyuanZhao Also, I noticed that this PR does not add support for creating signed URLs for buckets. One way to fix it would be to just go back to having a single |
| String privateKeyPath = getGcpPrivateKeyPath(); | ||
| return this.storage.signUrl( | ||
| BlobInfo.newBuilder(blob.getBucket(), blob.getName()).build(), | ||
| timePeriods, timeUnit, getSignUrlOptionForPrivateKey(privateKeyPath)); |
There was a problem hiding this comment.
Why are you passing credentials here at all?
this.storage already contains the correct credentials, no need to pass them here again.
Plus, if for whatever reason they needed to be passed here again, you would depend on GoogleCredentials, not get the credentials on your own.
There was a problem hiding this comment.
What's needed is a ServiceAccountCredentials, which is a child of GoogleCredentials, and I'll try looking for if that is available at all, but it might not be, since none of the existing code requires specifically a service account credentials to work.
There was a problem hiding this comment.
I see what you mean.
I think the best way forward here is to add a GoogleCredentials argument to this method and then checking if it's an instance of ServiceAccountCredentials and, if so, cast it.
If it isn't, we should fail with clear instructions for the user (e.g., createSignedUrl only works if you're using service account creds).
There was a problem hiding this comment.
Makes sense, will do.
|
My understanding of #251 was that when a blob is created, we should return (e.g., by logging) the GCS URL to users. Although now that I'm re-reading #251 I'm not totally sure what it's about. Maybe I'd just add a log statement to |
|
On a second thought, I may have misinterpreted the meaning of signed URL. |
|
I must say I feel completely the opposite. We had a previous PR that exposes setting metadata, which only makes sense for an object ( not a bucket), and that was elegantly handled by having the two separate types. Also ,the function for getting signed url is from Storage, and only works for objects, not buckets, so again I feel our current setup with 2 classes is very fitting. I definitely don't think we should be implementing our own url building functionn for buckets especially since Storage only provides one for objects. not buckets. |
|
Ultimately it's because while a gs://bucket and gs://bucket/object path look very similar to a user, they represent very different things and have different functionality even from the user perspective, so I really don't think we should be hiding both behind a single type and instead warning the user at runtime that one operation is supported or not. For example, setting meta data or even getting a write stream. |
|
I'm also OK with having the two types vs single type and internal In this case, this comes at the price of having a duplicated |
|
If it's true that you can't create a signed URL for a bucket, then this makes total sense and two classes are good. |
| if (blob == null) { | ||
| return null; | ||
| } | ||
| return this.storage.signUrl( |
There was a problem hiding this comment.
Have you tested this change without using application default credentials?
From reading the Storage#signUrl() documentation, I think we do need to pass it a SignUrlOptions object containing the desired service account credentials.
In other words, I don't think the Storage object is smart enough to use the GoogleCredentials that it was passed in its constructor, but I'd be curious to test if this is true.
There was a problem hiding this comment.
I did try it just now and it did make a working URL for me. This part of the doc makes it sound like it is aware of the credentials with which it was made:
If the credentials passed to
- {@link StorageOptions} do not implement {@link ServiceAccountSigner} (this is the case, for
- instance, for Compute Engine credentials and Google Cloud SDK credentials) then {@code signUrl}
- will throw an {@link IllegalStateException} unless an implementation of
- {@link ServiceAccountSigner} is passed using the
- {@link SignUrlOption#signWith(ServiceAccountSigner)} option.
| GoogleStorageResourceObject resource = new GoogleStorageResourceObject(storage, location); | ||
| Mockito.when(storage.get(Mockito.any(BlobId.class))).thenReturn(blob); | ||
| resource.createSignedUrl(TimeUnit.DAYS, 1L); | ||
| Mockito.verify(storage, Mockito.times(1)) |
There was a problem hiding this comment.
nit: we've been using static imports in tests, you might want to do the same here for consistency.
joaoandremartins
left a comment
There was a problem hiding this comment.
LGTM modulo the test static imports nit
fixes #251