Skip to content

Commit

Permalink
feat: Include universe_domain in credentials (#460)
Browse files Browse the repository at this point in the history
  • Loading branch information
dazuma committed Dec 7, 2023
1 parent de4d4e5 commit a7ff57b
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 26 deletions.
18 changes: 11 additions & 7 deletions lib/googleauth/compute_engine.rb
Expand Up @@ -83,7 +83,7 @@ def reset_cache
# Overrides the super class method to change how access tokens are
# fetched.
def fetch_access_token _options = {}
if target_audience
if token_type == :id_token
query = { "audience" => target_audience, "format" => "full" }
entry = "service-accounts/default/identity"
else
Expand Down Expand Up @@ -113,12 +113,16 @@ def fetch_access_token _options = {}
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
hash =
if ["text/html", "application/text"].include? content_type
{ token_type.to_s => body }
else
Signet::OAuth2.parse_credentials body, content_type
end
universe_domain = Google::Cloud.env.lookup_metadata "universe", "universe_domain"
universe_domain = "googleapis.com" if !universe_domain || universe_domain.empty?
hash["universe_domain"] = universe_domain.strip
hash
end
end
end
Expand Down
19 changes: 13 additions & 6 deletions lib/googleauth/credentials.rb
Expand Up @@ -259,7 +259,7 @@ def self.paths= new_paths
# @return [Object] The value
#
def self.lookup_auth_param name, method_name = name
val = instance_variable_get "@#{name}".to_sym
val = instance_variable_get :"@#{name}"
val = yield if val.nil? && block_given?
return val unless val.nil?
return superclass.send method_name if superclass.respond_to? method_name
Expand Down Expand Up @@ -328,9 +328,13 @@ def self.lookup_local_constant name
# @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
# suitable for passing as a closure.
#
# @!attribute [rw] universe_domain
# @return [String] The universe domain issuing these credentials.
#
def_delegators :@client,
:token_credential_uri, :audience,
:scope, :issuer, :signing_key, :updater_proc, :target_audience
:scope, :issuer, :signing_key, :updater_proc, :target_audience,
:universe_domain, :universe_domain=

##
# Creates a new Credentials instance with the provided auth credentials, and with the default
Expand Down Expand Up @@ -506,12 +510,15 @@ def client_options options

needs_scope = options["target_audience"].nil?
# client options for initializing signet client
{ token_credential_uri: options["token_credential_uri"],
{
token_credential_uri: options["token_credential_uri"],
audience: options["audience"],
scope: (needs_scope ? Array(options["scope"]) : nil),
target_audience: options["target_audience"],
issuer: options["client_email"],
signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
signing_key: OpenSSL::PKey::RSA.new(options["private_key"]),
universe_domain: options["universe_domain"] || "googleapis.com"
}
end

# rubocop:enable Metrics/AbcSize
Expand All @@ -526,7 +533,7 @@ def update_from_hash hash, options
hash = stringify_hash_keys hash
hash["scope"] ||= options[:scope]
hash["target_audience"] ||= options[:target_audience]
@project_id ||= (hash["project_id"] || hash["project"])
@project_id ||= hash["project_id"] || hash["project"]
@quota_project_id ||= hash["quota_project_id"]
@client = init_client hash, options
end
Expand All @@ -536,7 +543,7 @@ def update_from_filepath path, options
json = JSON.parse ::File.read(path)
json["scope"] ||= options[:scope]
json["target_audience"] ||= options[:target_audience]
@project_id ||= (json["project_id"] || json["project"])
@project_id ||= json["project_id"] || json["project"]
@quota_project_id ||= json["quota_project_id"]
@client = init_client json, options
end
Expand Down
3 changes: 2 additions & 1 deletion lib/googleauth/external_account.rb
Expand Up @@ -73,7 +73,8 @@ def make_aws_credentials user_creds, scope
subject_token_type: user_creds[:subject_token_type],
token_url: user_creds[:token_url],
credential_source: user_creds[:credential_source],
service_account_impersonation_url: user_creds[:service_account_impersonation_url]
service_account_impersonation_url: user_creds[:service_account_impersonation_url],
universe_domain: user_creds[:universe_domain]
)
end

Expand Down
2 changes: 2 additions & 0 deletions lib/googleauth/external_account/base_credentials.rb
Expand Up @@ -42,6 +42,7 @@ module BaseCredentials

attr_reader :expires_at
attr_accessor :access_token
attr_accessor :universe_domain

def expires_within? seconds
# This method is needed for BaseClient
Expand Down Expand Up @@ -110,6 +111,7 @@ def base_setup options
@quota_project_id = options[:quota_project_id]
@project_id = nil
@workforce_pool_user_project = options[:workforce_pool_user_project]
@universe_domain = options[:universe_domain] || "googleapis.com"

@expires_at = nil
@access_token = nil
Expand Down
3 changes: 2 additions & 1 deletion lib/googleauth/json_key_reader.rb
Expand Up @@ -27,7 +27,8 @@ def read_json_key json_key_io
json_key["private_key"],
json_key["client_email"],
json_key["project_id"],
json_key["quota_project_id"]
json_key["quota_project_id"],
json_key["universe_domain"]
]
end
end
Expand Down
16 changes: 11 additions & 5 deletions lib/googleauth/service_account.rb
Expand Up @@ -53,12 +53,13 @@ def self.make_creds options = {}
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience

if json_key_io
private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
else
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
quota_project_id = nil
universe_domain = nil
end
project_id ||= CredentialsLoader.load_gcloud_project_id

Expand All @@ -70,7 +71,8 @@ def self.make_creds options = {}
issuer: client_email,
signing_key: OpenSSL::PKey::RSA.new(private_key),
project_id: project_id,
quota_project_id: quota_project_id)
quota_project_id: quota_project_id,
universe_domain: universe_domain || "googleapis.com")
.configure_connection(options)
end

Expand All @@ -95,8 +97,9 @@ def initialize options = {}
def apply! a_hash, opts = {}
# Use a self-singed JWT if there's no information that can be used to
# obtain an OAuth token, OR if there are scopes but also an assertion
# that they are default scopes that shouldn't be used to fetch a token.
if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
# that they are default scopes that shouldn't be used to fetch a token,
# OR we are not in the default universe and thus OAuth isn't supported.
if target_audience.nil? && (scope.nil? || enable_self_signed_jwt? || universe_domain != "googleapis.com")
apply_self_signed_jwt! a_hash
else
super
Expand Down Expand Up @@ -138,6 +141,7 @@ class ServiceAccountJwtHeaderCredentials
extend JsonKeyReader
attr_reader :project_id
attr_reader :quota_project_id
attr_accessor :universe_domain

# Create a ServiceAccountJwtHeaderCredentials.
#
Expand All @@ -154,14 +158,16 @@ def self.make_creds options = {}
def initialize options = {}
json_key_io = options[:json_key_io]
if json_key_io
@private_key, @issuer, @project_id, @quota_project_id =
@private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
self.class.read_json_key json_key_io
else
@private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
@issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
@project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
@quota_project_id = nil
@universe_domain = nil
end
@universe_domain ||= "googleapis.com"
@project_id ||= CredentialsLoader.load_gcloud_project_id
@signing_key = OpenSSL::PKey::RSA.new @private_key
@scope = options[:scope]
Expand Down
12 changes: 12 additions & 0 deletions lib/googleauth/signet.rb
Expand Up @@ -25,6 +25,15 @@ module OAuth2
class Client
include Google::Auth::BaseClient

alias update_token_signet_base update_token!

def update_token! options = {}
options = deep_hash_normalize options
update_token_signet_base options
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
self
end

def configure_connection options
@connection_info =
options[:connection_builder] || options[:default_connection]
Expand All @@ -36,6 +45,9 @@ def token_type
target_audience ? :id_token : :access_token
end

# Set the universe domain
attr_accessor :universe_domain

alias orig_fetch_access_token! fetch_access_token!
def fetch_access_token! options = {}
unless options[:connection]
Expand Down
6 changes: 4 additions & 2 deletions lib/googleauth/user_refresh.rb
Expand Up @@ -50,15 +50,17 @@ def self.make_creds options = {}
"client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
"refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
"project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR],
"quota_project_id" => nil
"quota_project_id" => nil,
"universe_domain" => nil
}
new(token_credential_uri: TOKEN_CRED_URI,
client_id: user_creds["client_id"],
client_secret: user_creds["client_secret"],
refresh_token: user_creds["refresh_token"],
project_id: user_creds["project_id"],
quota_project_id: user_creds["quota_project_id"],
scope: scope)
scope: scope,
universe_domain: user_creds["universe_domain"] || "googleapis.com")
.configure_connection(options)
end

Expand Down
56 changes: 53 additions & 3 deletions spec/googleauth/compute_engine_spec.rb
Expand Up @@ -38,6 +38,15 @@
end

def make_auth_stubs opts
universe_stub = stub_request(:get, "http://169.254.169.254/computeMetadata/v1/universe/universe_domain")
.with(headers: { "Metadata-Flavor" => "Google" })
if !defined?(@universe_domain) || !@universe_domain
universe_stub.to_return body: "", status: 404, headers: {"Metadata-Flavor" => "Google" }
elsif @universe_domain.is_a? Class
universe_stub.to_raise @universe_domain
else
universe_stub.to_return body: @universe_domain, status: 200, headers: {"Metadata-Flavor" => "Google" }
end
if opts[:access_token]
body = MultiJson.dump("access_token" => opts[:access_token],
"token_type" => "Bearer",
Expand All @@ -50,17 +59,58 @@ def make_auth_stubs opts
.with(headers: { "Metadata-Flavor" => "Google" })
.to_return(body: body,
status: 200,
headers: { "Content-Type" => "application/json" })
headers: { "Content-Type" => "application/json", "Metadata-Flavor" => "Google" })
elsif opts[:id_token]
stub_request(:get, MD_ID_URI)
.with(headers: { "Metadata-Flavor" => "Google" })
.to_return(body: opts[:id_token],
status: 200,
headers: { "Content-Type" => "text/html" })
headers: { "Content-Type" => "text/html", "Metadata-Flavor" => "Google" })
end
end

context "default universe" do
it_behaves_like "apply/apply! are OK"

it "sets the universe" do
make_auth_stubs access_token: "1/abcde"
@client.fetch_access_token!
expect(@client.universe_domain).to eq("googleapis.com")
end
end

it_behaves_like "apply/apply! are OK"
context "custom universe" do
before :example do
@universe_domain = "myuniverse.com"
end

it_behaves_like "apply/apply! are OK"

it "sets the universe" do
make_auth_stubs access_token: "1/abcde"
@client.fetch_access_token!
expect(@client.universe_domain).to eq("myuniverse.com")
end

it "supports updating the universe_domain" do
make_auth_stubs access_token: "1/abcde"
@client.fetch_access_token!
@client.universe_domain = "anotheruniverse.com"
expect(@client.universe_domain).to eq("anotheruniverse.com")
end
end

context "error in universe_domain" do
before :example do
@universe_domain = Errno::EHOSTDOWN
end

it "results in an error" do
make_auth_stubs access_token: "1/abcde"
expect { @client.fetch_access_token! }
.to raise_error Signet::AuthorizationError
end
end

context "metadata is unavailable" do
describe "#fetch_access_token" do
Expand Down

0 comments on commit a7ff57b

Please sign in to comment.