Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .toys/linkinator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def run
end

def check_links
result = exec ["npx", "linkinator", "./doc"], out: :capture
result = exec ["npx", "linkinator", "./doc", "--skip", "stackoverflow.com"], out: :capture
puts result.captured_out
checked_links = result.captured_out.split "\n"
checked_links.select! { |link| link =~ /^\[(\d+)\]/ && ::Regexp.last_match[1] != "200" }
Expand Down
2 changes: 1 addition & 1 deletion Credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ that exposes common initialization functionality, such as creating credentials f
- Allows a GCP principal identified by a set of source credentials to impersonate a service account
- Useful for delegation of authority and managing permissions across service accounts
- Source credentials must have the Service Account Token Creator role on the target
- This credential type does not have a supported JSON form
- This credential type supports JSON configuration. The JSON form of this credential type has a `"type"` field with the value `"impersonated_service_account"`.

## User Authentication

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,5 +292,4 @@ hesitate to
about the client or APIs on [StackOverflow](http://stackoverflow.com).

[application default credentials]: https://cloud.google.com/docs/authentication/provide-credentials-adc
[contributing]: https://github.com/googleapis/google-auth-library-ruby/tree/main/.github/CONTRIBUTING.md
[license]: https://github.com/googleapis/google-auth-library-ruby/tree/main/LICENSE
3 changes: 3 additions & 0 deletions lib/googleauth/default_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require "googleauth/service_account"
require "googleauth/service_account_jwt_header"
require "googleauth/user_refresh"
require "googleauth/impersonated_service_account"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -114,6 +115,8 @@ def self.determine_creds_class json_key_io = nil
UserRefreshCredentials
when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME
ExternalAccount::Credentials
when ImpersonatedServiceAccountCredentials::CREDENTIAL_TYPE_NAME
ImpersonatedServiceAccountCredentials
else
raise InitializationError, "credentials type '#{type}' is not supported"
end
Expand Down
45 changes: 44 additions & 1 deletion lib/googleauth/impersonated_service_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ module Auth
# and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
# The short-lived token and its expiration time are cached.
class ImpersonatedServiceAccountCredentials
# @private
CREDENTIAL_TYPE_NAME = "impersonated_service_account".freeze

# @private
ERROR_SUFFIX = <<~ERROR.freeze
when trying to get security access token
Expand Down Expand Up @@ -84,11 +87,50 @@ class ImpersonatedServiceAccountCredentials
# defining the permissions required for the token.
# @option options [Object] :source_credentials The authenticated principal that will be used
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
# @option options [IO] :json_key_io The IO object that contains the credential configuration.
# It is exclusive with `:base_credentials` and `:source_credentials` options.
#
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
def self.make_creds options = {}
new options
if options[:json_key_io]
make_creds_from_json options
else
new options
end
end

# @private
def self.make_creds_from_json options
json_key_io = options[:json_key_io]
if options[:base_credentials] || options[:source_credentials]
raise Google::Auth::InitializationError,
"json_key_io is not compatible with base_credentials or source_credentials"
end

require "googleauth/default_credentials"
impersonated_json = MultiJson.load json_key_io.read
source_credentials_info = impersonated_json["source_credentials"]

if source_credentials_info["type"] == CREDENTIAL_TYPE_NAME
raise Google::Auth::InitializationError,
"Source credentials can't be of type impersonated_service_account, " \
"use delegates to chain impersonation."
end

source_credentials = DefaultCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(source_credentials_info))
)

impersonation_url = impersonated_json["service_account_impersonation_url"]
scope = options[:scope] || impersonated_json["scopes"]

new(
source_credentials: source_credentials,
impersonation_url: impersonation_url,
scope: scope
)
end
private_class_method :make_creds_from_json

# Initializes a new instance of ImpersonatedServiceAccountCredentials.
#
Expand All @@ -105,6 +147,7 @@ def self.make_creds options = {}
# - `{source_sa_email}` is the email address of the service account to impersonate.
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
# defining the permissions required for the token.
# It will override the scope from the `json_key_io` file if provided.
# @option options [Object] :source_credentials The authenticated principal that will be used
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
# It is redundant to provide both source and base credentials as only source will be used,
Expand Down
43 changes: 43 additions & 0 deletions spec/googleauth/get_application_default_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,49 @@ def cred_json_text
it_behaves_like "it cannot load misconfigured credentials"
end

describe "when credential type is impersonated_service_account" do
let :cred_json do
{
"type": "impersonated_service_account",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-account@example.com:generateAccessToken",
"source_credentials": {
"type": "authorized_user",
"client_id": "client_id",
"client_secret": "client_secret",
"refresh_token": "refresh_token",
"scope": "https://www.googleapis.com/auth/iam"
}
}
end

it "succeeds if the GOOGLE_APPLICATION_CREDENTIALS file is valid" do
Dir.mktmpdir do |dir|
key_path = File.join dir, "my_cert_file"
FileUtils.mkdir_p File.dirname(key_path)
File.write key_path, MultiJson.dump(cred_json)
ENV[@var_name] = key_path
creds = Google::Auth.get_application_default @scope, options
expect(creds).to be_a(Google::Auth::ImpersonatedServiceAccountCredentials)
end
end

it "fails if the GOOGLE_APPLICATION_CREDENTIALS path does not exist" do
Dir.mktmpdir do |dir|
key_path = File.join dir, "does-not-exist"
ENV[@var_name] = key_path
begin
Google::Auth.get_application_default @scope, options
fail "Expected to raise error"
rescue => e
expect(e).to be_a Google::Auth::InitializationError
expect(e).to be_a Google::Auth::Error
expect(e.message).to include "Unable to read the credential file"
expect(e.message).to include "does-not-exist"
end
end
end
end

describe "when credential type is unknown" do
let :cred_json do
{
Expand Down
101 changes: 101 additions & 0 deletions spec/googleauth/impersonated_service_account_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,107 @@

describe Google::Auth::ImpersonatedServiceAccountCredentials do

describe ".make_creds with json_key_io" do
let(:key) { OpenSSL::PKey::RSA.new 2048 }

let(:authorized_user_json) do
{
"type": "authorized_user",
"client_id": "client_id",
"client_secret": "client_secret",
"refresh_token": "refresh_token"
}
end

let(:service_account_json) do
{
"type": "service_account",
"private_key": key.to_pem,
"client_email": "client_email",
"scope": "https://www.googleapis.com/auth/iam"
}
end

let(:impersonated_json) do
{
"type": "impersonated_service_account",
"service_account_impersonation_url": impersonation_url,
"scopes": ["scope1"],
"source_credentials": source_credentials_json
}
end

context "with authorized_user source credentials" do
let(:source_credentials_json) { authorized_user_json }

it "creates credentials with UserRefreshCredentials as source" do
creds = Google::Auth::ImpersonatedServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(impersonated_json))
)

expect(creds).to be_a(Google::Auth::ImpersonatedServiceAccountCredentials)
expect(creds.source_credentials).to be_a(Google::Auth::UserRefreshCredentials)
expect(creds.impersonation_url).to eq(impersonation_url)
expect(creds.scope).to eq(["scope1"])
end
end

context "with service_account source credentials" do
let(:source_credentials_json) { service_account_json }

it "creates credentials with ServiceAccountCredentials as source" do
creds = Google::Auth::ImpersonatedServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(impersonated_json))
)

expect(creds).to be_a(Google::Auth::ImpersonatedServiceAccountCredentials)
expect(creds.source_credentials).to be_a(Google::Auth::ServiceAccountCredentials)
expect(creds.impersonation_url).to eq(impersonation_url)
expect(creds.scope).to eq(["scope1"])
end
end

context "with recursive impersonated_service_account source credentials" do
let(:recursive_impersonated_json) do
{
"type": "impersonated_service_account",
"service_account_impersonation_url": impersonation_url,
"scopes": ["scope1"],
"source_credentials": {
"type": "impersonated_service_account"
}
}
end

it "raises a runtime error" do
expect {
Google::Auth::ImpersonatedServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(recursive_impersonated_json))
)
}.to raise_error(Google::Auth::InitializationError, /Source credentials can't be of type.*use delegates/)
end
end

context "scope handling" do
let(:source_credentials_json) { authorized_user_json }

it "uses scope from JSON if not provided in options" do
creds = Google::Auth::ImpersonatedServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(impersonated_json))
)
expect(creds.scope).to eq(["scope1"])
end

it "uses scope from options if provided" do
creds = Google::Auth::ImpersonatedServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(MultiJson.dump(impersonated_json)),
scope: ["scope2"]
)
expect(creds.scope).to eq(["scope2"])
end
end
end

let(:impersonation_url) {"https://iamcredentials.example.com/v1/projects/-/serviceAccounts/test:generateAccessToken"}

def make_auth_stubs opts
Expand Down
Loading