From a4358ba3f7ff858af7562cb49bb9126a54cb2467 Mon Sep 17 00:00:00 2001 From: Anayeli Malvaez Colin Date: Mon, 28 Sep 2026 22:52:23 +0000 Subject: [PATCH 1/3] feat(auth): add service-to-service authentication sample --- auth/service-to-service/Gemfile | 20 ++++ auth/service-to-service/Gemfile.lock | 105 ++++++++++++++++++ auth/service-to-service/app.rb | 26 +++++ auth/service-to-service/service_auth.rb | 37 ++++++ .../spec/service_auth_spec.rb | 71 ++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 auth/service-to-service/Gemfile create mode 100644 auth/service-to-service/Gemfile.lock create mode 100644 auth/service-to-service/app.rb create mode 100644 auth/service-to-service/service_auth.rb create mode 100644 auth/service-to-service/spec/service_auth_spec.rb diff --git a/auth/service-to-service/Gemfile b/auth/service-to-service/Gemfile new file mode 100644 index 000000000..22f578015 --- /dev/null +++ b/auth/service-to-service/Gemfile @@ -0,0 +1,20 @@ +# 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. + +source "https://rubygems.org" + +gem "functions_framework" +gem "googleauth", "~> 1.17" +gem "rspec", "~> 3.0" +gem "webrick" diff --git a/auth/service-to-service/Gemfile.lock b/auth/service-to-service/Gemfile.lock new file mode 100644 index 000000000..b779ee543 --- /dev/null +++ b/auth/service-to-service/Gemfile.lock @@ -0,0 +1,105 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + cloud_events (0.9.0) + diff-lcs (1.6.2) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.4) + net-http (~> 0.5) + functions_framework (1.7.0) + cloud_events (>= 0.7.0, < 2.a) + puma (>= 4.3.0, < 9.a) + rack (>= 2.1, < 4.a) + google-cloud-env (2.4.0) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-logging-utils (0.2.0) + googleauth (1.17.3) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + net-http (0.9.1) + uri (>= 0.11.1) + nio4r (2.7.5) + os (1.1.4) + pstore (0.2.1) + public_suffix (7.0.5) + puma (8.0.2) + nio4r (~> 2.0) + rack (3.2.7) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + uri (1.1.1) + webrick (1.9.2) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + functions_framework + googleauth (~> 1.17) + rspec (~> 3.0) + webrick + +CHECKSUMS + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + cloud_events (0.9.0) sha256=f0a8421ff064a6c96f05c2e974d1f5957a6c1455a256a2f0e93926ff0c5c4797 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + functions_framework (1.7.0) sha256=e92e9f02209eb5e8d50c1ee65c1c965b152a82a8a08a9697dd1ac5f1d019bbac + google-cloud-env (2.4.0) sha256=01cb35ecee5d1c4d1cd4deaed8bb5bddc838e296bcc2a35c5d1ddd07ce40d28d + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.3) sha256=9a655455885c0c17a8b70537acd2a992a7c7842c43c06c6612dfb0d93e755d58 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 + +BUNDLED WITH + 4.0.3 diff --git a/auth/service-to-service/app.rb b/auth/service-to-service/app.rb new file mode 100644 index 000000000..8eb97b832 --- /dev/null +++ b/auth/service-to-service/app.rb @@ -0,0 +1,26 @@ +# 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. + +# This application acts as both the target function and relay function +# for testing service-to-service authentication using Cloud Functions. + +require "functions_framework" + +FunctionsFramework.http "make_get_request_cloud-echo" do |_request| + "Success!" +end + +FunctionsFramework.http "make_get_request_cloud" do |_request| + "Success!" +end diff --git a/auth/service-to-service/service_auth.rb b/auth/service-to-service/service_auth.rb new file mode 100644 index 000000000..4e5d93cd6 --- /dev/null +++ b/auth/service-to-service/service_auth.rb @@ -0,0 +1,37 @@ +# 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. + +require "net/http" +require "uri" +require "googleauth" + +def make_get_request writer, target_url, audience + credentials = Google::Auth.get_application_default target_audience: audience + token_info = credentials.fetch_access_token! + + uri = URI target_url + request = Net::HTTP::Get.new uri + + request["Authorization"] = "Bearer #{token_info['id_token']}" + + response = Net::HTTP.start uri.hostname, uri.port, use_ssl: uri.scheme == "https" do |http| + http.request request + end + + response.value + + writer.write response.body +rescue StandardError => e + raise "Failed to make get request: #{e.message}" +end diff --git a/auth/service-to-service/spec/service_auth_spec.rb b/auth/service-to-service/spec/service_auth_spec.rb new file mode 100644 index 000000000..dfc069f96 --- /dev/null +++ b/auth/service-to-service/spec/service_auth_spec.rb @@ -0,0 +1,71 @@ +# Copyright 2026 Google, Inc +# +# 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. + +require_relative "../service_auth" +require "rspec" +require "googleauth" +require "open3" + +describe "Service to service auth sample" do + before :all do + require "webrick" + @port = 8080 + @server = WEBrick::HTTPServer.new( + Port: @port, + Logger: WEBrick::Log.new("/dev/null"), + AccessLog: [] + ) + @server.mount_proc "/" do |_req, res| + res.body = "Success!" + end + @server_thread = Thread.new { @server.start } + + # Wait for server to start + sleep 1 + end + + after :all do + @server.shutdown + @server_thread.join + end + + it "makes an authenticated get request to the target url" do + url = "http://localhost:#{@port}/" + + max_attempts = 5 + attempts = 0 + success = false + + while attempts < max_attempts && !success + attempts += 1 + buffer = StringIO.new + + begin + make_get_request buffer, url, url + + # Verify the output matches expected + expect(buffer.string).to eq("Success!") + success = true + + rescue StandardError, RSpec::Expectations::ExpectationNotMetError => e + if attempts >= max_attempts + raise "Failed after #{max_attempts} attempts. Last error: #{e.message}" + else + puts "make_get_request attempt #{attempts} failed: #{e.message}. Retrying in 2s..." + sleep 2 + end + end + end + end +end From e4a8f1bcc8abe5143262e00c817ec0e8174ca3b8 Mon Sep 17 00:00:00 2001 From: Anayeli Malvaez Colin Date: Tue, 29 Sep 2026 00:30:10 +0000 Subject: [PATCH 2/3] fix(eventarc): linter issue --- eventarc/generic/app.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/eventarc/generic/app.rb b/eventarc/generic/app.rb index d73ec7554..c073df146 100644 --- a/eventarc/generic/app.rb +++ b/eventarc/generic/app.rb @@ -30,9 +30,8 @@ puts "\nHEADERS:" headers = request.env.select { |k, _v| k.start_with? "HTTP_" } - .collect { |key, val| [key.sub(/^HTTP_/, ""), val] } - .collect { |key, val| "#{key}: #{val}" } - .sort + headers = headers.collect { |key, val| [key.sub(/^HTTP_/, ""), val] } + headers = headers.collect { |key, val| "#{key}: #{val}" }.sort headers.each do |key, value| if key != "Authorization" puts "#{key}: #{value}
" From 21b2c958d17cdcafdea878fb8b6ae0716c0c2e24 Mon Sep 17 00:00:00 2001 From: Anayeli Malvaez Colin Date: Tue, 29 Sep 2026 00:41:46 +0000 Subject: [PATCH 3/3] fix(auth): fix comments --- auth/service-to-service/Gemfile.lock | 2 +- auth/service-to-service/app.rb | 12 ++- auth/service-to-service/service_auth.rb | 11 ++- .../spec/service_auth_spec.rb | 89 ++++++++++++++----- 4 files changed, 87 insertions(+), 27 deletions(-) diff --git a/auth/service-to-service/Gemfile.lock b/auth/service-to-service/Gemfile.lock index b779ee543..c65a6a070 100644 --- a/auth/service-to-service/Gemfile.lock +++ b/auth/service-to-service/Gemfile.lock @@ -102,4 +102,4 @@ CHECKSUMS webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 BUNDLED WITH - 4.0.3 + 2.3.15 diff --git a/auth/service-to-service/app.rb b/auth/service-to-service/app.rb index 8eb97b832..d90abfbad 100644 --- a/auth/service-to-service/app.rb +++ b/auth/service-to-service/app.rb @@ -15,12 +15,20 @@ # This application acts as both the target function and relay function # for testing service-to-service authentication using Cloud Functions. -require "functions_framework" +require_relative "service_auth" FunctionsFramework.http "make_get_request_cloud-echo" do |_request| "Success!" end FunctionsFramework.http "make_get_request_cloud" do |_request| - "Success!" + target_url = ENV["TARGET_URL"] + + if target_url.nil? || target_url.empty? + raise "TARGET_URL environment variable is missing" + end + + writer = StringIO.new + make_get_request writer, target_url, target_url + writer.string end diff --git a/auth/service-to-service/service_auth.rb b/auth/service-to-service/service_auth.rb index 4e5d93cd6..8026b9426 100644 --- a/auth/service-to-service/service_auth.rb +++ b/auth/service-to-service/service_auth.rb @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# [START cloudrun_service_to_service_auth] require "net/http" require "uri" require "googleauth" @@ -19,13 +20,17 @@ def make_get_request writer, target_url, audience credentials = Google::Auth.get_application_default target_audience: audience token_info = credentials.fetch_access_token! + id_token = token_info["id_token"] if token_info + raise "ID token is missing from credentials. Ensure you are using a service account or credentials that support ID tokens." if id_token.nil? - uri = URI target_url + uri = URI(target_url) request = Net::HTTP::Get.new uri - request["Authorization"] = "Bearer #{token_info['id_token']}" + request["Authorization"] = "Bearer #{id_token}" response = Net::HTTP.start uri.hostname, uri.port, use_ssl: uri.scheme == "https" do |http| + http.open_timeout = 10 + http.read_timeout = 10 http.request request end @@ -35,3 +40,5 @@ def make_get_request writer, target_url, audience rescue StandardError => e raise "Failed to make get request: #{e.message}" end + +# [END cloudrun_service_to_service_auth] diff --git a/auth/service-to-service/spec/service_auth_spec.rb b/auth/service-to-service/spec/service_auth_spec.rb index dfc069f96..09b34f991 100644 --- a/auth/service-to-service/spec/service_auth_spec.rb +++ b/auth/service-to-service/spec/service_auth_spec.rb @@ -19,29 +19,76 @@ describe "Service to service auth sample" do before :all do - require "webrick" - @port = 8080 - @server = WEBrick::HTTPServer.new( - Port: @port, - Logger: WEBrick::Log.new("/dev/null"), - AccessLog: [] - ) - @server.mount_proc "/" do |_req, res| - res.body = "Success!" + @project_id = ENV["GOOGLE_CLOUD_PROJECT"] + @credentials = ENV["GOOGLE_APPLICATION_CREDENTIALS"] + @env_vars = @credentials ? { "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE" => @credentials } : {} + + @location = ENV["LOCATION_ID"] || "us-central1" + @entry_point = "make_get_request_cloud" + @target_name = "#{@entry_point}-echo" + @runtime_version = "ruby32" + + # Initial Setup. Deploy target function + deploy_cmd = [ + "gcloud", "functions", "deploy", @target_name, + "--entry-point=#{@target_name}", + "--runtime=#{@runtime_version}", + "--no-allow-unauthenticated", + "--project=#{@project_id}", + "--trigger-http", + "--source=." + ] + puts "Running:#{deploy_cmd.join ' '}" + stdout, stderr, status = Open3.capture3(@env_vars, *deploy_cmd) + + # If the Cloud Functions API is disabled in the CI test project, skip the rest of the group + unless status.success? + if stderr.include?("PERMISSION_DENIED") && stderr.include?("Cloud Functions API") + skip "Cloud Functions API not enabled in this project (#{@project_id}). Skipping tests." + else + raise "Setup: Deploy target function failed:\n#{stderr}" + end end - @server_thread = Thread.new { @server.start } - - # Wait for server to start - sleep 1 + + # Deploy relay function + @target_url = "https://#{@location}-#{@project_id}.cloudfunctions.net/#{@target_name}" + + deploy_relay_cmd = [ + "gcloud", "functions", "deploy", @entry_point, + "--entry-point=#{@entry_point}", + "--runtime=#{@runtime_version}", + "--no-allow-unauthenticated", + "--project=#{@project_id}", + "--trigger-http", + "--set-env-vars=TARGET_URL=#{@target_url}", + "--source=." + ] + + puts "Running: #{deploy_relay_cmd.join ' '}" + stdout, stderr, status = Open3.capture3(@env_vars, *deploy_relay_cmd) + raise "Setup: Deploy relay function failed:\n#{stderr}" unless status.success? + + @base_url = "https://#{@location}-#{@project_id}.cloudfunctions.net/" end after :all do - @server.shutdown - @server_thread.join + next if @base_url.nil? # We skipped setup + + # Teardown the target function + delete_target_cmd = ["gcloud", "functions", "delete", @target_name, "--project=#{@project_id}", "--quiet"] + puts "Running: #{delete_target_cmd.join ' '}" + _, stderr, status = Open3.capture3(@env_vars, *delete_target_cmd) + puts "Teardown: Delete function #{@target_name} failed: #{stderr}" unless status.success? + + # Teardown the relay function + delete_relay_cmd = ["gcloud", "functions", "delete", @entry_point, "--project=#{@project_id}", "--quiet"] + puts "Running: #{delete_relay_cmd.join ' '}" + _, stderr, status = Open3.capture3(@env_vars, *delete_relay_cmd) + puts "Teardown: Delete function #{@entry_point} failed: #{stderr}" unless status.success? end - + it "makes an authenticated get request to the target url" do - url = "http://localhost:#{@port}/" + url = "#{@base_url}#{@entry_point}" max_attempts = 5 attempts = 0 @@ -53,17 +100,15 @@ begin make_get_request buffer, url, url - - # Verify the output matches expected + expect(buffer.string).to eq("Success!") success = true - rescue StandardError, RSpec::Expectations::ExpectationNotMetError => e if attempts >= max_attempts raise "Failed after #{max_attempts} attempts. Last error: #{e.message}" else - puts "make_get_request attempt #{attempts} failed: #{e.message}. Retrying in 2s..." - sleep 2 + puts "make_get_request attempt #{attempts} failed: #{e.message}. Retrying in 10s..." + sleep 10 end end end