Skip to content

Commit

Permalink
feat: Use google-cloud-env for more robust Metadata Service access (#459
Browse files Browse the repository at this point in the history
)
  • Loading branch information
dazuma committed Dec 5, 2023
1 parent 35755f0 commit de4d4e5
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 84 deletions.
9 changes: 4 additions & 5 deletions .github/sync-repo-settings.yaml
Expand Up @@ -6,14 +6,13 @@ branchProtectionRules:
isAdminEnforced: false
requiredStatusCheckContexts:
- 'cla/google'
- 'CI (macos-latest, 3.0, test , spec)'
- 'CI (ubuntu-20.04, 2.6, test , spec)'
- 'CI (macos-latest, 3.2, test , spec)'
- 'CI (ubuntu-20.04, 2.7, test , spec)'
- 'CI (ubuntu-20.04, 3.0, test , spec)'
- 'CI (ubuntu-20.04, 3.1, test , spec)'
- 'CI (ubuntu-22.04, 3.1, test , spec)'
- 'CI (ubuntu-latest, 3.0, rubocop , integration , build , yardoc , linkinator)'
- 'CI (windows-latest, 3.0, test , spec)'
- 'CI (ubuntu-22.04, 3.2, test , spec)'
- 'CI (ubuntu-latest, 3.2, rubocop , integration , build , yardoc , linkinator)'
- 'CI (windows-latest, 3.2, test , spec)'
requiredApprovingReviewCount: 1
requiresCodeOwnerReviews: true
requiresStrictStatusChecks: true
Expand Down
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Expand Up @@ -13,9 +13,6 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-20.04
ruby: "2.6"
task: test , spec
- os: ubuntu-20.04
ruby: "2.7"
task: test , spec
Expand All @@ -26,16 +23,16 @@ jobs:
ruby: "3.1"
task: test , spec
- os: ubuntu-22.04
ruby: "3.1"
ruby: "3.2"
task: test , spec
- os: macos-latest
ruby: "3.0"
ruby: "3.2"
task: test , spec
- os: windows-latest
ruby: "3.0"
ruby: "3.2"
task: test , spec
- os: ubuntu-latest
ruby: "3.0"
ruby: "3.2"
task: rubocop , integration , build , yardoc , linkinator
fail-fast: false
runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Expand Up @@ -6,7 +6,7 @@ gemspec
gem "fakefs", ">= 1.0", "< 3"
gem "fakeredis", "~> 0.5"
gem "gems", "~> 1.2"
gem "google-style", "~> 1.26.1"
gem "google-style", "~> 1.27.0"
gem "logging", "~> 2.0"
gem "minitest", "~> 5.14"
gem "minitest-focus", "~> 1.1"
Expand Down
5 changes: 3 additions & 2 deletions googleauth.gemspec
Expand Up @@ -20,9 +20,10 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]

gem.platform = Gem::Platform::RUBY
gem.required_ruby_version = ">= 2.6"
gem.required_ruby_version = ">= 2.7"

gem.add_dependency "faraday", ">= 0.17.3", "< 3.a"
gem.add_dependency "faraday", ">= 1.0", "< 3.a"
gem.add_dependency "google-cloud-env", "~> 2.0", ">= 2.0.1"
gem.add_dependency "jwt", ">= 1.4", "< 3.0"
gem.add_dependency "multi_json", "~> 1.11"
gem.add_dependency "os", ">= 0.9", "< 2.0"
Expand Down
6 changes: 1 addition & 5 deletions lib/googleauth/application_default.rb
Expand Up @@ -55,11 +55,7 @@ def get_application_default scope = nil, options = {}
DefaultCredentials.from_well_known_path(scope, options) ||
DefaultCredentials.from_system_default_path(scope, options)
return creds unless creds.nil?
unless GCECredentials.on_gce? options
# Clear cache of the result of GCECredentials.on_gce?
GCECredentials.reset_cache
raise NOT_FOUND_ERROR
end
raise NOT_FOUND_ERROR unless GCECredentials.on_gce? options
GCECredentials.new options.merge(scope: scope)
end
end
Expand Down
79 changes: 39 additions & 40 deletions lib/googleauth/compute_engine.rb
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

require "faraday"
require "google-cloud-env"
require "googleauth/signet"

module Google
Expand All @@ -33,83 +33,69 @@ module Auth
# Extends Signet::OAuth2::Client so that the auth token is obtained from
# the GCE metadata server.
class GCECredentials < Signet::OAuth2::Client
# The IP Address is used in the URIs to speed up failures on non-GCE
# systems.
# @private Unused and deprecated but retained to prevent breaking changes
DEFAULT_METADATA_HOST = "169.254.169.254".freeze

# @private Unused and deprecated
# @private Unused and deprecated but retained to prevent breaking changes
COMPUTE_AUTH_TOKEN_URI =
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
# @private Unused and deprecated
# @private Unused and deprecated but retained to prevent breaking changes
COMPUTE_ID_TOKEN_URI =
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
# @private Unused and deprecated
# @private Unused and deprecated but retained to prevent breaking changes
COMPUTE_CHECK_URI = "http://169.254.169.254".freeze

@on_gce_cache = {}

class << self
# @private Unused and deprecated
def metadata_host
ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
end

# @private Unused and deprecated
def compute_check_uri
"http://#{metadata_host}".freeze
end

# @private Unused and deprecated
def compute_auth_token_uri
"#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
end

# @private Unused and deprecated
def compute_id_token_uri
"#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
end

# Detect if this appear to be a GCE instance, by checking if metadata
# is available.
def on_gce? options = {}, reload = false # rubocop:disable Style/OptionalBooleanParameter
# We can follow OptionalBooleanParameter here because it's a public interface, we can't change it.
@on_gce_cache.delete options if reload
@on_gce_cache.fetch options do
@on_gce_cache[options] = begin
# TODO: This should use google-cloud-env instead.
c = options[:connection] || Faraday.default_connection
headers = { "Metadata-Flavor" => "Google" }
resp = c.get compute_check_uri, nil, headers do |req|
req.options.timeout = 1.0
req.options.open_timeout = 0.1
end
return false unless resp.status == 200
resp.headers["Metadata-Flavor"] == "Google"
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
false
end
end
# The parameters are deprecated and unused.
def on_gce? _options = {}, _reload = false # rubocop:disable Style/OptionalBooleanParameter
Google::Cloud.env.metadata?
end

def reset_cache
@on_gce_cache.clear
Google::Cloud.env.compute_metadata.reset_existence!
Google::Cloud.env.compute_metadata.cache.expire_all!
end
alias unmemoize_all reset_cache
end

# Overrides the super class method to change how access tokens are
# fetched.
def fetch_access_token options = {}
c = options[:connection] || Faraday.default_connection
retry_with_error do
uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
query[:scopes] = Array(scope).join "," if scope
resp = c.get uri, query, "Metadata-Flavor" => "Google"
def fetch_access_token _options = {}
if target_audience
query = { "audience" => target_audience, "format" => "full" }
entry = "service-accounts/default/identity"
else
query = {}
entry = "service-accounts/default/token"
end
query[:scopes] = Array(scope).join "," if scope
begin
resp = Google::Cloud.env.lookup_metadata_response "instance", entry, query: query
case resp.status
when 200
content_type = resp.headers["content-type"]
if ["text/html", "application/text"].include? content_type
{ (target_audience ? "id_token" : "access_token") => resp.body }
else
Signet::OAuth2.parse_credentials resp.body, content_type
end
build_token_hash resp.body, resp.headers["content-type"]
when 403, 500
msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
raise Signet::UnexpectedStatusError, msg
Expand All @@ -119,6 +105,19 @@ def fetch_access_token options = {}
msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
raise Signet::AuthorizationError, msg
end
rescue Google::Cloud::Env::MetadataServerNotResponding => e
raise Signet::AuthorizationError, e.message
end
end

private

def build_token_hash body, content_type
if ["text/html", "application/text"].include? content_type
key = target_audience ? "id_token" : "access_token"
{ key => body }
else
Signet::OAuth2.parse_credentials body, content_type
end
end
end
Expand Down
3 changes: 1 addition & 2 deletions lib/googleauth/external_account/base_credentials.rb
Expand Up @@ -85,8 +85,7 @@ def retrieve_subject_token!
# true if the credentials represent a workforce pool.
# false if they represent a workload.
def is_workforce_pool?
pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
/#{pattern}/.match?(@audience || "")
%r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
end

private
Expand Down
1 change: 1 addition & 0 deletions spec/googleauth/apply_auth_examples.rb
Expand Up @@ -150,6 +150,7 @@
want = { :foo => "bar", auth_key => "Bearer #{t}" }
expect(got).to eq(want)
@client.expires_at -= 3601 # default is to expire in 1hr
Google::Cloud.env.compute_metadata.cache.expire_all!
end
end
end
Expand Down
21 changes: 9 additions & 12 deletions spec/googleauth/compute_engine_spec.rb
Expand Up @@ -27,10 +27,16 @@
GCECredentials = Google::Auth::GCECredentials

before :example do
Google::Cloud.env.compute_smbios.override_product_name = "Google Compute Engine"
GCECredentials.reset_cache
@client = GCECredentials.new
@id_client = GCECredentials.new target_audience: "https://pubsub.googleapis.com/"
end

after :example do
Google::Cloud.env.compute_smbios.override_product_name = nil
end

def make_auth_stubs opts
if opts[:access_token]
body = MultiJson.dump("access_token" => opts[:access_token],
Expand Down Expand Up @@ -200,13 +206,14 @@ def make_auth_stubs opts
stub = stub_request(:get, "http://169.254.169.254")
.with(headers: { "Metadata-Flavor" => "Google" })
.to_return(status: 404,
headers: { "Metadata-Flavor" => "NotGoogle" })
headers: { "Metadata-Flavor" => "Google" })
expect(GCECredentials.on_gce?({}, true)).to eq(false)
expect(stub).to have_been_requested
end

it "should honor GCE_METADATA_HOST environment variable" do
ENV["GCE_METADATA_HOST"] = "mymetadata.example.com"
Google::Cloud.env.compute_metadata.reset!
begin
stub = stub_request(:get, "http://mymetadata.example.com")
.with(headers: { "Metadata-Flavor" => "Google" })
Expand All @@ -216,17 +223,7 @@ def make_auth_stubs opts
expect(stub).to have_been_requested
ensure
ENV.delete "GCE_METADATA_HOST"
end
end

it "should honor reload flag" do
begin
stub = stub_request(:get, "http://169.254.169.254")
.with(headers: { "Metadata-Flavor" => "Google" })
.to_return(status: 200,
headers: { "Metadata-Flavor" => "Google" })
expect(GCECredentials.on_gce?({}, false)).to eq(true)
expect(stub).to_not have_been_requested
Google::Cloud.env.compute_metadata.reset!
end
end
end
Expand Down
21 changes: 11 additions & 10 deletions spec/googleauth/get_application_default_spec.rb
Expand Up @@ -27,6 +27,8 @@
let(:options) { |example| { dememoize: example } }

before :example do
Google::Cloud.env.compute_smbios.override_product_name = "Google Compute Engine"
GCECredentials.reset_cache
@key = OpenSSL::PKey::RSA.new 2048
@var_name = ENV_VAR
@credential_vars = [
Expand All @@ -42,6 +44,7 @@
end

after :example do
Google::Cloud.env.compute_smbios.override_product_name = nil
@credential_vars.each { |var| ENV[var] = @original_env_vals[var] }
ENV["HOME"] = @home unless @home == ENV["HOME"]
ENV["APPDATA"] = @app_data unless @app_data == ENV["APPDATA"]
Expand All @@ -59,17 +62,15 @@
end

it "fails without default file or env if not on compute engine" do
stub = stub_request(:get, "http://169.254.169.254")
.to_return(status: 404,
headers: { "Metadata-Flavor" => "NotGoogle" })
Dir.mktmpdir do |dir|
ENV.delete @var_name unless ENV[@var_name].nil? # no env var
ENV["HOME"] = dir # no config present in this tmp dir
expect do
Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError
Google::Cloud.env.compute_smbios.with_override_product_name "Someone else" do
Dir.mktmpdir do |dir|
ENV.delete @var_name unless ENV[@var_name].nil? # no env var
ENV["HOME"] = dir # no config present in this tmp dir
expect do
Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError
end
end
expect(stub).to have_been_requested
end
end

Expand Down

0 comments on commit de4d4e5

Please sign in to comment.