diff --git a/google-cloud-storage/acceptance/storage/signed_url_v2_test.rb b/google-cloud-storage/acceptance/storage/signed_url_v2_test.rb index e8eb28d4c68d..06ec7b5ff296 100644 --- a/google-cloud-storage/acceptance/storage/signed_url_v2_test.rb +++ b/google-cloud-storage/acceptance/storage/signed_url_v2_test.rb @@ -84,6 +84,41 @@ end describe Google::Cloud::Storage::Bucket, :signed_url do + it "should create a signed read url automatically using IAM API when on GCE/Workload Identity" do + local_file = File.new files[:logo][:path] + file = bucket.create_file local_file, "CloudLogoSignedUrlGetBucketAuto.png" + + issuer = bucket.service.credentials.issuer + skip "Test requires a service account with an issuer" unless issuer + + bucket.service.credentials.stub :signing_key, nil do + bucket.service.credentials.stub :issuer, nil do + Google::Cloud.env.stub :metadata?, true do + Google::Cloud.env.stub :lookup_metadata, issuer do + five_min_from_now = 5 * 60 + url = bucket.signed_url file.name, + method: "GET", + expires: five_min_from_now + + uri = URI url + http = Net::HTTP.new uri.host, uri.port + http.use_ssl = true + http.ca_file ||= ENV["SSL_CERT_FILE"] if ENV["SSL_CERT_FILE"] + + resp = http.get uri.request_uri + _(resp.code).must_equal "200" + + Tempfile.open ["google-cloud", ".png"] do |tmpfile| + tmpfile.binmode + tmpfile.write resp.body + _(tmpfile.size).must_equal local_file.size + end + end + end + end + end + end + it "should create a signed read url with space in file name" do local_file = File.new files[:logo][:path] file = bucket.create_file local_file, "CloudLogoSignedUrl GetBucket.png" diff --git a/google-cloud-storage/acceptance/storage/signed_url_v4_test.rb b/google-cloud-storage/acceptance/storage/signed_url_v4_test.rb index eea50fcf071d..8720dacd6d3c 100644 --- a/google-cloud-storage/acceptance/storage/signed_url_v4_test.rb +++ b/google-cloud-storage/acceptance/storage/signed_url_v4_test.rb @@ -84,6 +84,42 @@ end describe Google::Cloud::Storage::Bucket, :signed_url do + it "should create a signed read url v4 automatically using IAM API when on GCE/Workload Identity" do + local_file = File.new files[:logo][:path] + file = bucket.create_file local_file, "CloudLogoSignedUrlGetBucketAuto.png" + + issuer = bucket.service.credentials.issuer + skip "Test requires a service account with an issuer" unless issuer + + bucket.service.credentials.stub :signing_key, nil do + bucket.service.credentials.stub :issuer, nil do + Google::Cloud.env.stub :metadata?, true do + Google::Cloud.env.stub :lookup_metadata, issuer do + five_min_from_now = 5 * 60 + url = bucket.signed_url file.name, + method: "GET", + expires: five_min_from_now, + version: :v4 + + uri = URI url + http = Net::HTTP.new uri.host, uri.port + http.use_ssl = true + http.ca_file ||= ENV["SSL_CERT_FILE"] if ENV["SSL_CERT_FILE"] + + resp = http.get uri.request_uri + _(resp.code).must_equal "200" + + Tempfile.open ["google-cloud", ".png"] do |tmpfile| + tmpfile.binmode + tmpfile.write resp.body + _(tmpfile.size).must_equal local_file.size + end + end + end + end + end + end + it "should create a signed read url version v4 with space in file name" do local_file = File.new files[:logo][:path] file = bucket.create_file local_file, "CloudLogoSignedUrl GetBucket.png" diff --git a/google-cloud-storage/lib/google/cloud/storage/bucket.rb b/google-cloud-storage/lib/google/cloud/storage/bucket.rb index 50cd2c739fcd..debe813efdcb 100644 --- a/google-cloud-storage/lib/google/cloud/storage/bucket.rb +++ b/google-cloud-storage/lib/google/cloud/storage/bucket.rb @@ -2261,6 +2261,10 @@ def compose sources, # steps in [Service Account Authentication]( # https://cloud.google.com/iam/docs/service-accounts). # + # When running in Google Cloud environments (like Compute Engine, Cloud Run, or + # Kubernetes Engine), the library automatically detects the environment and uses + # the IAM Credentials API to sign the URL. A local private key is not required. + # # @see https://cloud.google.com/storage/docs/access-control/signed-urls # Signed URLs guide # @see https://cloud.google.com/storage/docs/access-control/signed-urls#signing-resumable @@ -2284,7 +2288,12 @@ def compose sources, # @param [Hash] headers Google extension headers (custom HTTP headers # that begin with `x-goog-`) that must be included in requests that # use the signed URL. - # @param [String] issuer Service Account's Client Email. + # @param [String] issuer Service Account's Client Email. If not provided, the + # library will attempt to extract it from the standard credentials. If running + # in a Google Cloud environment (like GCE or GKE), the library will automatically + # fetch the default service account email from the metadata server. For external + # Workload Identity Federation (e.g., GitHub Actions or AWS), you must provide this + # value explicitly to use keyless signing. # @param [String] client_email Service Account's Client Email. # @param [OpenSSL::PKey::RSA, String, Proc] signing_key Service Account's # Private Key or a Proc that accepts a single String parameter and returns a @@ -2296,12 +2305,9 @@ def compose sources, # Private Key or a Proc that accepts a single String parameter and returns a # RSA SHA256 signature using a valid Google Service Account Private Key. # - # When using this method in environments such as GAE Flexible Environment, - # GKE, or Cloud Functions where the private key is unavailable, it may be - # necessary to provide a Proc (or lambda) via the signer parameter. This - # Proc should return a signature created using a RPC call to the - # [Service Account Credentials signBlob](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob) - # method as shown in the example below. + # A custom proc used to sign the URL. This is no longer required for Workload + # Identity or keyless environments, as the library will automatically fallback to + # the IAM Credentials API if a private key is missing but an `issuer` is available. # @param [Hash] query Query string parameters to include in the signed # URL. The given parameters are not verified by the signature. # diff --git a/google-cloud-storage/lib/google/cloud/storage/file.rb b/google-cloud-storage/lib/google/cloud/storage/file.rb index 8956692f0feb..8a137fd2792a 100644 --- a/google-cloud-storage/lib/google/cloud/storage/file.rb +++ b/google-cloud-storage/lib/google/cloud/storage/file.rb @@ -1741,6 +1741,10 @@ def public_url protocol: :https # steps in [Service Account Authentication]( # https://cloud.google.com/iam/docs/service-accounts). # + # When running in Google Cloud environments (like Compute Engine, Cloud Run, or + # Kubernetes Engine), the library automatically detects the environment and uses + # the IAM Credentials API to sign the URL. A local private key is not required. + # # @see https://cloud.google.com/storage/docs/access-control/signed-urls # Signed URLs guide # @see https://cloud.google.com/storage/docs/access-control/signed-urls#signing-resumable @@ -1762,7 +1766,12 @@ def public_url protocol: :https # @param [Hash] headers Google extension headers (custom HTTP headers # that begin with `x-goog-`) that must be included in requests that # use the signed URL. - # @param [String] issuer Service Account's Client Email. + # @param [String] issuer Service Account's Client Email. If not provided, the + # library will attempt to extract it from the standard credentials. If running + # in a Google Cloud environment (like GCE or GKE), the library will automatically + # fetch the default service account email from the metadata server. For external + # Workload Identity Federation (e.g., GitHub Actions or AWS), you must provide this + # value explicitly to use keyless signing. # @param [String] client_email Service Account's Client Email. # @param [OpenSSL::PKey::RSA, String, Proc] signing_key Service Account's # Private Key or a Proc that accepts a single String parameter and returns a @@ -1774,12 +1783,9 @@ def public_url protocol: :https # Private Key or a Proc that accepts a single String parameter and returns a # RSA SHA256 signature using a valid Google Service Account Private Key. # - # When using this method in environments such as GAE Flexible Environment, - # GKE, or Cloud Functions where the private key is unavailable, it may be - # necessary to provide a Proc (or lambda) via the signer parameter. This - # Proc should return a signature created using a RPC call to the - # [Service Account Credentials signBlob](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob) - # method as shown in the example below. + # A custom proc used to sign the URL. This is no longer required for Workload + # Identity or keyless environments, as the library will automatically fallback to + # the IAM Credentials API if a private key is missing but an `issuer` is available. # @param [Hash] query Query string parameters to include in the signed # URL. The given parameters are not verified by the signature. # diff --git a/google-cloud-storage/lib/google/cloud/storage/file/signer_v2.rb b/google-cloud-storage/lib/google/cloud/storage/file/signer_v2.rb index b07889464f50..c0c117c4820e 100644 --- a/google-cloud-storage/lib/google/cloud/storage/file/signer_v2.rb +++ b/google-cloud-storage/lib/google/cloud/storage/file/signer_v2.rb @@ -78,14 +78,15 @@ def signature_str options end def determine_signing_key options = {} - signing_key = options[:signing_key] || options[:private_key] || - options[:signer] || @service.credentials.signing_key - raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key - signing_key + options[:signing_key] || options[:private_key] || + options[:signer] || @service.credentials.signing_key end def determine_issuer options = {} issuer = options[:issuer] || options[:client_email] || @service.credentials.issuer + if issuer.nil? && Google::Cloud.env.metadata? + issuer = Google::Cloud.env.lookup_metadata "instance", "service-accounts/default/email" + end raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer issuer end @@ -111,7 +112,7 @@ def post_object options policy_str = p.to_json policy = Base64.strict_encode64(policy_str).delete "\n" - signature = generate_signature s, policy + signature = generate_signature i, s, policy fields[:GoogleAccessId] = i fields[:signature] = signature @@ -126,19 +127,23 @@ def signed_url options i = determine_issuer options s = determine_signing_key options - sig = generate_signature s, signature_str(options) + sig = generate_signature i, s, signature_str(options) generate_signed_url i, sig, options[:expires], options[:query] end - def generate_signature signing_key, secret + def generate_signature issuer, signing_key, secret unencoded_signature = "" - if signing_key.is_a? Proc - unencoded_signature = signing_key.call secret - else - unless signing_key.respond_to? :sign - signing_key = OpenSSL::PKey::RSA.new signing_key + if signing_key + if signing_key.is_a? Proc + unencoded_signature = signing_key.call secret + else + unless signing_key.respond_to? :sign + signing_key = OpenSSL::PKey::RSA.new signing_key + end + unencoded_signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret end - unencoded_signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret + else + unencoded_signature = iam_signer_instance.sign issuer, secret end Base64.strict_encode64(unencoded_signature).delete "\n" end @@ -168,6 +173,13 @@ def format_extension_headers headers def url_escape str CGI.escape String str end + + def iam_signer_instance + @iam_signer_instance ||= begin + require "google/cloud/storage/iam_signer" + Google::Cloud::Storage::IAMSigner.new(@service.credentials) + end + end end end end diff --git a/google-cloud-storage/lib/google/cloud/storage/file/signer_v4.rb b/google-cloud-storage/lib/google/cloud/storage/file/signer_v4.rb index fe84fa82d0cf..78669a973e70 100644 --- a/google-cloud-storage/lib/google/cloud/storage/file/signer_v4.rb +++ b/google-cloud-storage/lib/google/cloud/storage/file/signer_v4.rb @@ -66,7 +66,7 @@ def post_object issuer: nil, policy_str = escape_characters p.to_json policy = Base64.strict_encode64(policy_str).force_encoding "utf-8" - signature = generate_signature s, policy + signature = generate_signature i, s, policy post_fields["x-goog-signature"] = signature post_fields["policy"] = policy @@ -195,14 +195,15 @@ def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname def determine_issuer issuer, client_email # Parse the Service Account and get client id and private key issuer = issuer || client_email || @service.credentials.issuer + if issuer.nil? && Google::Cloud.env.metadata? + issuer = Google::Cloud.env.lookup_metadata "instance", "service-accounts/default/email" + end raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer issuer end def determine_signing_key signing_key, private_key, signer - signing_key = signing_key || private_key || signer || @service.credentials.signing_key - raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key - signing_key + signing_key || private_key || signer || @service.credentials.signing_key end def error_msg attr_name @@ -229,7 +230,14 @@ def service_account_signer signer def issuer_and_signer issuer, client_email, signing_key, private_key, signer issuer = determine_issuer issuer, client_email signing_key = determine_signing_key signing_key, private_key, signer - signer = service_account_signer signing_key + if signing_key + signer = service_account_signer signing_key + else + signer = lambda do |string_to_sign| + sig = iam_signer_instance.sign issuer, string_to_sign + sig.unpack1 "H*" + end + end [issuer, signer] end @@ -351,18 +359,29 @@ def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname end end - def generate_signature signing_key, data + def generate_signature issuer, signing_key, data packed_signature = nil - if signing_key.is_a? Proc - packed_signature = signing_key.call data - else - unless signing_key.respond_to? :sign - signing_key = OpenSSL::PKey::RSA.new signing_key + if signing_key + if signing_key.is_a? Proc + packed_signature = signing_key.call data + else + unless signing_key.respond_to? :sign + signing_key = OpenSSL::PKey::RSA.new signing_key + end + packed_signature = signing_key.sign OpenSSL::Digest::SHA256.new, data end - packed_signature = signing_key.sign OpenSSL::Digest::SHA256.new, data + else + packed_signature = iam_signer_instance.sign issuer, data end packed_signature.unpack1("H*").force_encoding "utf-8" end + + def iam_signer_instance + @iam_signer_instance ||= begin + require "google/cloud/storage/iam_signer" + Google::Cloud::Storage::IAMSigner.new(@service.credentials) + end + end end end end diff --git a/google-cloud-storage/lib/google/cloud/storage/iam_signer.rb b/google-cloud-storage/lib/google/cloud/storage/iam_signer.rb new file mode 100644 index 000000000000..825305d8618a --- /dev/null +++ b/google-cloud-storage/lib/google/cloud/storage/iam_signer.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright 2026 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 +# +# https://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. + +require "googleauth" + +module Google + module Cloud + module Storage + ## + # @private + # Helper class for signing blobs via the IAM Credentials API. + class IAMSigner + def initialize credentials + require "google/apis/iamcredentials_v1" + + @client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new + @client.authorization = credentials.client + end + + def sign issuer, string_to_sign + request = Google::Apis::IamcredentialsV1::SignBlobRequest.new( + payload: string_to_sign + ) + resource = "projects/-/serviceAccounts/#{issuer}" + + begin + response = @client.sign_service_account_blob resource, request + response.signed_blob + rescue Google::Apis::Error => e + raise Google::Cloud::Storage::SignedUrlUnavailable, "Failed to sign URL via IAM Credentials API. Ensure the Workload Identity service account has the 'Service Account Token Creator' role. Underlying error: #{e.message}" + end + end + end + end + end +end diff --git a/google-cloud-storage/samples/acceptance/files_test.rb b/google-cloud-storage/samples/acceptance/files_test.rb index aa8bf1f10f49..21e5023d66d3 100644 --- a/google-cloud-storage/samples/acceptance/files_test.rb +++ b/google-cloud-storage/samples/acceptance/files_test.rb @@ -29,6 +29,7 @@ require_relative "../storage_generate_encryption_key" require_relative "../storage_generate_signed_post_policy_v4" require_relative "../storage_generate_signed_url_v4" +require_relative "../storage_generate_signed_url_v4_workload_identity" require_relative "../storage_generate_upload_signed_url_v4" require_relative "../storage_get_metadata" require_relative "../storage_get_object_contexts" @@ -606,6 +607,35 @@ def mock_cipher.random_key assert bucket.file remote_file_name end + + it "generate_signed_url_v4_workload_identity" do + bucket.create_file local_file, remote_file_name + + # Skip this test if credentials lack an issuer since it is a prerequisite for automatic fallback + issuer = bucket.service.credentials.issuer + skip "Test requires a service account with an issuer" unless issuer + + # Temporarily remove private key to trigger keyless environment logic + bucket.service.credentials.stub :signing_key, nil do + bucket.service.credentials.stub :issuer, nil do + Google::Cloud.env.stub :metadata?, true do + Google::Cloud.env.stub :lookup_metadata, issuer do + out, _err = capture_io do + generate_signed_url_v4_workload_identity bucket_name: bucket.name, + file_name: remote_file_name + end + + signed_url = out.scan(/http.*$/).first + refute_nil signed_url + + file_contents = Net::HTTP.get URI(signed_url) + assert_equal file_contents, File.read(local_file) + end + end + end + end + end + describe "post object" do require "net/http" require "uri" diff --git a/google-cloud-storage/samples/storage_generate_signed_url_v4_workload_identity.rb b/google-cloud-storage/samples/storage_generate_signed_url_v4_workload_identity.rb new file mode 100644 index 000000000000..156c44f195c3 --- /dev/null +++ b/google-cloud-storage/samples/storage_generate_signed_url_v4_workload_identity.rb @@ -0,0 +1,44 @@ +# Copyright 2026 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. + +# [START storage_generate_signed_url_v4_workload_identity] +def generate_signed_url_v4_workload_identity bucket_name:, file_name: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The ID of your GCS object + # file_name = "your-file-name" + + require "google/cloud/storage" + + # Initialize the client. In a Google Cloud environment (like GKE Workload + # Identity or Compute Engine), credentials are automatically discovered. + storage = Google::Cloud::Storage.new + storage_expiry_time = 5 * 60 # 5 minutes + + # Generate the signed URL. The google-cloud-storage library will automatically + # detect the keyless environment and fallback to using the IAM Credentials API + # to sign the URL. + url = storage.signed_url bucket_name, file_name, method: "GET", + expires: storage_expiry_time, + version: :v4 + + puts "Generated GET signed url:" + puts url + puts "You can use this URL with any user agent, for example:" + puts "curl '#{url}'" +end +# [END storage_generate_signed_url_v4_workload_identity] + +generate_signed_url_v4_workload_identity bucket_name: ARGV.shift, file_name: ARGV.shift if $PROGRAM_NAME == __FILE__ diff --git a/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v2_test.rb b/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v2_test.rb index 49a5fe59b9a0..91b3e4546eac 100644 --- a/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v2_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v2_test.rb @@ -143,22 +143,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - bucket.signed_url file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - bucket.signed_url file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + bucket.signed_url file_path + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "raises with issuer and lambda with incorrect argument count" do diff --git a/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v4_test.rb b/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v4_test.rb index e30573d22700..72b452e99c23 100644 --- a/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v4_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/bucket_signed_url_v4_test.rb @@ -169,22 +169,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - bucket.signed_url file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - bucket.signed_url file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + bucket.signed_url file_path, version: :v4 + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "raises with issuer and lambda with incorrect argument count" do diff --git a/google-cloud-storage/test/google/cloud/storage/file_signed_url_v2_test.rb b/google-cloud-storage/test/google/cloud/storage/file_signed_url_v2_test.rb index 7bfde415e579..3bffb38f88d4 100644 --- a/google-cloud-storage/test/google/cloud/storage/file_signed_url_v2_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/file_signed_url_v2_test.rb @@ -13,6 +13,7 @@ # limitations under the License. require "helper" +require "google/cloud/storage/iam_signer" describe Google::Cloud::Storage::File, :signed_url, :mock_storage do let(:bucket_name) { "bucket" } @@ -144,22 +145,61 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new + credentials.signing_key = nil - expect { - file.signed_url - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + Google::Cloud.env.stub :metadata?, false do + expect { + file.signed_url + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + end end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" - credentials.signing_key = nil + it "falls back to IAMSigner when missing signing_key" do + Time.stub :now, Time.new(2012,1,1,0,0,0, "+00:00") do + credentials.issuer = "native_issuer@email.com" + credentials.signing_key = nil - expect { - file.signed_url - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + iam_signer_mock = Minitest::Mock.new + iam_signer_mock.expect :sign, "iam-signature", ["native_issuer@email.com", "GET\n\n\n1325376300\n/bucket/file.ext"] + + Google::Cloud.env.stub :metadata?, false do + Google::Cloud::Storage::IAMSigner.stub :new, iam_signer_mock do + signed_url = file.signed_url + + signed_url_params = CGI::parse(URI(signed_url).query) + _(signed_url_params["GoogleAccessId"]).must_equal ["native_issuer@email.com"] + _(signed_url_params["Signature"]).must_equal [Base64.strict_encode64("iam-signature").delete("\n")] + end + end + + iam_signer_mock.verify + end + end + + it "uses IAMSigner and auto-detects issuer if metadata? is true" do + Time.stub :now, Time.new(2012,1,1,0,0,0, "+00:00") do + credentials.issuer = nil + credentials.signing_key = nil + + iam_signer_mock = Minitest::Mock.new + iam_signer_mock.expect :sign, "iam-signature", ["metadata_issuer@email.com", "GET\n\n\n1325376300\n/bucket/file.ext"] + + Google::Cloud.env.stub :metadata?, true do + Google::Cloud.env.stub :lookup_metadata, "metadata_issuer@email.com" do + Google::Cloud::Storage::IAMSigner.stub :new, iam_signer_mock do + signed_url = file.signed_url + + signed_url_params = CGI::parse(URI(signed_url).query) + _(signed_url_params["GoogleAccessId"]).must_equal ["metadata_issuer@email.com"] + _(signed_url_params["Signature"]).must_equal [Base64.strict_encode64("iam-signature").delete("\n")] + end + end + end + + iam_signer_mock.verify + end end it "raises with issuer and lambda with incorrect argument count" do diff --git a/google-cloud-storage/test/google/cloud/storage/file_signed_url_v4_test.rb b/google-cloud-storage/test/google/cloud/storage/file_signed_url_v4_test.rb index 6f6818d1a6b4..25434db955a6 100644 --- a/google-cloud-storage/test/google/cloud/storage/file_signed_url_v4_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/file_signed_url_v4_test.rb @@ -13,6 +13,7 @@ # limitations under the License. require "helper" +require "google/cloud/storage/iam_signer" describe Google::Cloud::Storage::File, :signed_url, :v4, :mock_storage do let(:bucket_name) { "bucket" } @@ -145,22 +146,61 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new + credentials.signing_key = nil - expect { - file.signed_url version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + Google::Cloud.env.stub :metadata?, false do + expect { + file.signed_url version: :v4 + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + end end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" - credentials.signing_key = nil + it "falls back to IAMSigner when missing signing_key" do + Time.stub :now, Time.new(2012,1,1,0,0,0, "+00:00") do + credentials.issuer = "native_issuer@email.com" + credentials.signing_key = nil - expect { - file.signed_url version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + iam_signer_mock = Minitest::Mock.new + iam_signer_mock.expect :sign, "iam-signature", ["native_issuer@email.com", String] + + Google::Cloud.env.stub :metadata?, false do + Google::Cloud::Storage::IAMSigner.stub :new, iam_signer_mock do + signed_url = file.signed_url version: :v4 + + signed_url_params = CGI::parse(URI(signed_url).query) + _(signed_url_params["X-Goog-Credential"]).must_equal ["native_issuer@email.com/20120101/auto/storage/goog4_request"] + _(signed_url_params["X-Goog-Signature"]).must_equal ["iam-signature".unpack1("H*")] + end + end + + iam_signer_mock.verify + end + end + + it "uses IAMSigner and auto-detects issuer if metadata? is true" do + Time.stub :now, Time.new(2012,1,1,0,0,0, "+00:00") do + credentials.issuer = nil + credentials.signing_key = nil + + iam_signer_mock = Minitest::Mock.new + iam_signer_mock.expect :sign, "iam-signature", ["metadata_issuer@email.com", String] + + Google::Cloud.env.stub :metadata?, true do + Google::Cloud.env.stub :lookup_metadata, "metadata_issuer@email.com" do + Google::Cloud::Storage::IAMSigner.stub :new, iam_signer_mock do + signed_url = file.signed_url version: :v4 + + signed_url_params = CGI::parse(URI(signed_url).query) + _(signed_url_params["X-Goog-Credential"]).must_equal ["metadata_issuer@email.com/20120101/auto/storage/goog4_request"] + _(signed_url_params["X-Goog-Signature"]).must_equal ["iam-signature".unpack1("H*")] + end + end + end + + iam_signer_mock.verify + end end it "raises with issuer and lambda with incorrect argument count" do diff --git a/google-cloud-storage/test/google/cloud/storage/iam_signer_test.rb b/google-cloud-storage/test/google/cloud/storage/iam_signer_test.rb new file mode 100644 index 000000000000..23133d661968 --- /dev/null +++ b/google-cloud-storage/test/google/cloud/storage/iam_signer_test.rb @@ -0,0 +1,50 @@ +# Copyright 2024 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 +# +# https://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. + +require "helper" +require "google/cloud/storage/iam_signer" +require "google/apis/iamcredentials_v1" + +describe Google::Cloud::Storage::IAMSigner do + it "signs a payload using IAMCredentialsService" do + issuer = "test@email.com" + payload = "my-payload" + expected_resource = "projects/-/serviceAccounts/test@email.com" + expected_signature = "mocked-signature" + + mock_auth = Minitest::Mock.new + + mock_response = Minitest::Mock.new + mock_response.expect :signed_blob, expected_signature + + mock_service = Minitest::Mock.new + mock_service.expect :authorization=, nil, [mock_auth] + mock_service.expect :sign_service_account_blob, mock_response do |resource, request| + resource == expected_resource && request.payload == payload + end + + mock_credentials = Minitest::Mock.new + mock_credentials.expect :client, mock_auth + + Google::Apis::IamcredentialsV1::IAMCredentialsService.stub :new, mock_service do + signer = Google::Cloud::Storage::IAMSigner.new mock_credentials + signature = signer.sign issuer, payload + + _(signature).must_equal expected_signature + end + + mock_service.verify + mock_response.verify + end +end diff --git a/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v2_test.rb b/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v2_test.rb index aef004762e0f..7c3efa966fdf 100644 --- a/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v2_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v2_test.rb @@ -101,22 +101,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - bucket.signed_url file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - bucket.signed_url file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + bucket.signed_url file_path + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end describe "Files with spaces in them" do diff --git a/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v4_test.rb b/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v4_test.rb index 9b2251ad8ced..de7640928387 100644 --- a/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v4_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/lazy/bucket_signed_url_v4_test.rb @@ -120,22 +120,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - bucket.signed_url file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - bucket.signed_url file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + bucket.signed_url file_path, version: :v4 + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "allows query params to be passed in" do diff --git a/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v2_test.rb b/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v2_test.rb index 384651493002..ee4ef92cf6fd 100644 --- a/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v2_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v2_test.rb @@ -123,22 +123,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - file.signed_url - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - file.signed_url - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + file.signed_url + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end describe "Files with spaces in them" do diff --git a/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v4_test.rb b/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v4_test.rb index d6123dc6a5b8..e9483705f166 100644 --- a/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v4_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/lazy/file_signed_url_v4_test.rb @@ -118,22 +118,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - file.signed_url version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - file.signed_url version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + file.signed_url version: :v4 + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "allows query params to be passed in" do diff --git a/google-cloud-storage/test/google/cloud/storage/project_signed_url_v2_test.rb b/google-cloud-storage/test/google/cloud/storage/project_signed_url_v2_test.rb index bdeb28976e5a..a7fb33818223 100644 --- a/google-cloud-storage/test/google/cloud/storage/project_signed_url_v2_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/project_signed_url_v2_test.rb @@ -141,22 +141,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - storage.signed_url bucket_name, file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - storage.signed_url bucket_name, file_path - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + storage.signed_url bucket_name, file_path + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "raises with issuer and lambda with incorrect argument count" do diff --git a/google-cloud-storage/test/google/cloud/storage/project_signed_url_v4_test.rb b/google-cloud-storage/test/google/cloud/storage/project_signed_url_v4_test.rb index 8005f6fa84a9..3eeae0b47052 100644 --- a/google-cloud-storage/test/google/cloud/storage/project_signed_url_v4_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/project_signed_url_v4_test.rb @@ -142,22 +142,33 @@ end end - it "raises when missing issuer" do + it "raises SignedUrlUnavailable when missing both signing_key and issuer" do + + credentials.issuer = nil - credentials.signing_key = PoisonSigningKey.new - expect { - storage.signed_url bucket_name, file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable - end - it "raises when missing signing_key" do - credentials.issuer = "native_issuer" credentials.signing_key = nil - expect { - storage.signed_url bucket_name, file_path, version: :v4 - }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + + + Google::Cloud.env.stub :metadata?, false do + + + expect { + + + storage.signed_url bucket_name, file_path, version: :v4 + + + }.must_raise Google::Cloud::Storage::SignedUrlUnavailable + + + end + + end it "raises with issuer and lambda with incorrect argument count" do