# frozen_string_literal: true
require_relative '../spec_helper'
module Aws
describe 'Credential Resolution Chain' do
let(:mock_credential_file) do
File.expand_path(
File.join(
File.dirname(__FILE__),
'..', 'fixtures', 'credentials', 'mock_shared_credentials'
)
)
end
let(:mock_config_file) do
File.expand_path(
File.join(
File.dirname(__FILE__),
'..', 'fixtures', 'credentials', 'mock_shared_config'
)
)
end
let(:mock_instance_creds) { double('InstanceProfileCredentials', set?: false) }
before(:all) do
# Ensure the sample_rest_xml is built before any mocks occur
ApiHelper.sample_rest_xml
end
before(:each) do
allow(InstanceProfileCredentials).to receive(:new).and_return(mock_instance_creds)
end
describe 'default behavior' do
before(:each) do
stub_const('ENV', {})
# AWS_SDK_CONFIG_OPT_OUT not present
Aws.shared_config.fresh(
config_enabled: true,
credentials_path: mock_credential_file,
config_path: mock_config_file
)
end
it 'prefers direct credentials above other sources' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
access_key_id: 'ACCESS_DIRECT',
secret_access_key: 'SECRET_DIRECT',
profile: 'fooprofile',
region: 'us-east-1'
)
expect(client.config.credentials.access_key_id).to eq('ACCESS_DIRECT')
end
it 'prefers assume role credentials when profile explicitly set over ENV credentials' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'ACCESS_KEY_1', # from 'fooprofile'
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assumerole_sc', region: 'us-east-1'
)
expect(client.config.credentials.credentials.access_key_id).to eq('AR_AKID')
end
it 'prefers assume role web identity over sso' do
assume_role_web_identity_stub(
'arn:aws:iam:123456789012:role/foo',
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_web_identity', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'prefers sso credentials over assume role' do
expect(SSOCredentials).to receive(:new).with(
sso_start_url: nil,
sso_region: 'us-east-1',
sso_account_id: 'SSO_ACCOUNT_ID',
sso_role_name: 'SSO_ROLE_NAME',
sso_session: 'sso-test-session'
).and_return(
double(
'creds',
set?: true,
credentials: double(access_key_id: 'SSO_AKID')
)
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds',
token_provider: nil
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('SSO_AKID')
end
it 'loads SSO credentials from a legacy profile' do
expect(SSOCredentials).to receive(:new).with(
sso_start_url: 'START_URL',
sso_region: 'us-east-1',
sso_account_id: 'SSO_ACCOUNT_ID',
sso_role_name: 'SSO_ROLE_NAME',
sso_session: nil
).and_return(
double(
'creds',
set?: true,
credentials: double(access_key_id: 'SSO_AKID')
)
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds_legacy'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('SSO_AKID')
end
it 'loads SSO credentials from a mixed legacy profile when values match' do
expect(SSOCredentials).to receive(:new).with(
sso_start_url: 'START_URL',
sso_region: 'us-east-1',
sso_account_id: 'SSO_ACCOUNT_ID',
sso_role_name: 'SSO_ROLE_NAME',
sso_session: 'sso-test-session'
).and_return(
double(
'creds',
set?: true,
credentials: double(access_key_id: 'SSO_AKID')
)
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds_mixed_legacy',
token_provider: nil,
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('SSO_AKID')
end
it 'raises when attempting to load an incomplete SSO Profile' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds_bad',
region: 'us-east-1'
)
end.to raise_error(ArgumentError, /Missing required keys/)
end
it 'raises when attempting to load a mixed legacy SSO Profile with mismatched values' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds_mixed_legacy_mismatch',
region: 'us-east-1'
)
end.to raise_error(ArgumentError, /does not match the profile/)
end
it 'raises when attempting to load an SSO profile with a missing sso-session' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'sso_creds_bad_session',
region: 'us-east-1'
)
end.to raise_error(ArgumentError,
/sso-session session-does-not-exist must be defined in the config file/)
end
it 'prefers assume role over shared config' do
assume_role_stub(
'arn:aws:iam:123456789012:role/bar',
'ACCESS_KEY_1', # from 'fooprofile'
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_plus_creds', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
sts_client = client.config.credentials.client
expect(
sts_client.config.region
).to eq('us-east-1')
end
it 'prefers shared credential file static credentials over shared config' do
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'credentials_first', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('ACCESS_KEY_CRD')
end
it 'will source static credentials from shared config after shared credentials' do
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'incomplete_cred', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('ACCESS_KEY_SC1')
end
it 'prefers process credentials over metadata credentials' do
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'creds_from_process', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_PROC1')
end
it 'prefers direct credentials over process credentials when profile not set' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AKID_ENV_STUB')
end
it 'prefers process credentials from direct profile over env' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'creds_from_process', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_PROC1')
end
it 'attempts to fetch metadata credentials last' do
allow(InstanceProfileCredentials).to receive(:new).and_call_original
stub_request(:put, 'http://169.254.169.254/latest/api/token')
.to_return(
status: 200,
body: "my-token\n",
headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' }
)
stub_request(
:get,
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
).with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: "profile-name\n")
stub_request(
:get,
'http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name'
).with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: <<-JSON.strip)
{
"Code" : "Success",
"LastUpdated" : "2013-11-22T20:03:48Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "akid-md",
"SecretAccessKey" : "secret-md",
"Token" : "session-token-md",
"Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}"
}
JSON
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'nonexistant', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('akid-md')
end
describe 'Assume Role Resolution' do
it 'will not assume a role without a source present' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_no_src', region: 'us-east-1'
)
end.to raise_error(Errors::NoSourceProfileError)
end
it 'will explicitly raise if source_profile is present but invalid' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_bad_src', region: 'us-east-1'
)
end.to raise_error(Errors::NoSourceProfileError)
end
it 'supports :source_profile from assume_role_web_identity' do
assume_role_web_identity_stub(
'arn:aws:iam:123456789012:role/foo',
'AR_AKID_WEB',
'AR_SECRET',
'AR_TOKEN'
)
assume_role_stub(
'arn:aws:iam:123456789012:role/bar',
'AR_AKID_WEB', # from web_only
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_web_src', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
sts_client = client.config.credentials.client
expect(
sts_client.config.region
).to eq('us-east-1')
end
it 'supports :source_profile from process credentials' do
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'AK_PROC1',
'AK_PROC1',
'SECRET_AK_PROC1',
'TOKEN_PROC1'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'creds_from_sc_process', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_PROC1')
end
it 'supports :source_profile from sso credentials' do
expect(SSOCredentials).to receive(:new).with(
sso_start_url: nil,
sso_region: 'us-east-1',
sso_account_id: 'SSO_ACCOUNT_ID',
sso_role_name: 'SSO_ROLE_NAME',
sso_session: 'sso-test-session'
).and_return(
double(
'SSOCreds',
set?: true,
credentials: Credentials.new('SSO_AKID', 'sak')
)
)
allow(SSOTokenProvider).to receive(:new)
.and_return(double('SSOToken', set?: true))
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'SSO_AKID',
'AR_AKID',
'SECRET_AK',
'TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_sso_src', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'supports assume role chaining' do
assume_role_stub(
'arn:aws:iam:123456789012:role/role_b',
'ACCESS_KEY_BASE',
'AK_1',
'SECRET_AK_1',
'TOKEN_1'
)
assume_role_stub(
'arn:aws:iam:123456789012:role/role_a',
'AK_1',
'AK_2',
'SECRET_AK_2',
'TOKEN_2'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assume_role_chain_b', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_2')
end
it 'uses source credentials when source and static are both set' do
assume_role_stub(
'arn:aws:iam:123456789012:role/role_a',
'ACCESS_KEY_BASE',
'AK_2',
'SECRET_AK_2',
'TOKEN_2'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assume_role_source_and_credentials', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_2')
end
it 'uses static credentials when the profile self references' do
assume_role_stub(
'arn:aws:iam:123456789012:role/role_a',
'ACCESS_KEY_SELF',
'AK_2',
'SECRET_AK_2',
'TOKEN_2'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assume_role_self_reference', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AK_2')
end
it 'raises if there is a loop in chained profiles' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'assume_role_chain_loop_a', region: 'us-east-1'
)
end.to raise_error(Errors::SourceProfileCircularReferenceError)
end
it 'raises if credential_source is present but invalid' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_bad_csrc', region: 'us-east-1'
)
end.to raise_error(Errors::InvalidCredentialSourceError)
end
it 'raises if source_profile and credential_source both present' do
expect do
ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_src_conflict', region: 'us-east-1'
)
end.to raise_error(Errors::CredentialSourceConflictError)
end
it 'will assume a role from shared credentials before shared config' do
assume_role_stub(
'arn:aws:iam:123456789012:role/bar',
'ACCESS_KEY_1', # from 'fooprofile'
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assumerole_sc', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'will then try to assume a role from shared config' do
assume_role_stub(
'arn:aws:iam:123456789012:role/bar',
'ACCESS_KEY_ARPC', # from 'ar_from_self'
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'ar_from_self', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'assumes a role from config using source in shared credentials' do
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'ACCESS_KEY_1', # from 'creds_from_sc'
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'creds_from_sc', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'allows region to be resolved when unspecified' do
assume_role_stub(
'arn:aws:iam:123456789012:role/bar',
'ACCESS_KEY_ARPC',
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with('AWS_PROFILE').and_return('ar_from_self')
allow(ENV).to receive(:values_at).and_return(['us-east-1'])
credentials = CredentialProviderChain.new.resolve
expect(
credentials.credentials.access_key_id
).to eq('AR_AKID')
end
end
it 'can assume a role with EC2 Instance Metadata as a source' do
allow(InstanceProfileCredentials).to receive(:new).and_call_original
profile = 'ar_ec2_src'
resp = <<-JSON.strip
{
"Code" : "Success",
"LastUpdated" : "2013-11-22T20:03:48Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ACCESS_KEY_EC2",
"SecretAccessKey" : "secret",
"Token" : "session-token",
"Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}"
}
JSON
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'ACCESS_KEY_EC2',
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
stub_request(:put, 'http://169.254.169.254/latest/api/token')
.to_return(
status: 200,
body: "my-token\n",
headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' }
)
stub_request(:get, 'http://169.254.169.254/latest/meta-data/iam/security-credentials/')
.with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: "profile-name\n")
stub_request(:get, 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name')
.with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: resp)
client = ApiHelper.sample_rest_xml::Client.new(
profile: profile,
region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
it 'can assume a role with ECS Credentials as a source' do
profile = 'ar_ecs_src'
path = '/latest/credentials?id=foobarbaz'
resp = <<-JSON.strip
{
"RoleArn" : "arn:aws:iam::123456789012:role/BarFooRole",
"AccessKeyId" : "ACCESS_KEY_ECS",
"SecretAccessKey" : "secret",
"Token" : "session-token",
"Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}"
}
JSON
stub_const('ENV',
'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' => path)
stub_request(:get, "http://169.254.170.2#{path}")
.to_return(status: 200, body: resp)
assume_role_stub(
'arn:aws:iam:123456789012:role/foo',
'ACCESS_KEY_ECS',
'AR_AKID',
'AR_SECRET',
'AR_TOKEN'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: profile,
region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AR_AKID')
end
end
describe 'AWS_SDK_CONFIG_OPT_OUT set' do
before(:each) do
stub_const('ENV', {})
Aws.shared_config.fresh(
config_enabled: false,
credentials_path: mock_credential_file,
# The config file exists but should not be used.
config_path: mock_config_file
)
end
it 'prefers direct credentials above other sources' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
access_key_id: 'ACCESS_DIRECT',
secret_access_key: 'SECRET_DIRECT',
profile: 'fooprofile',
region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('ACCESS_DIRECT')
end
it 'prefers ENV credentials over shared config when profile not set' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('AKID_ENV_STUB')
end
it 'prefers config from profile over ENV credentials when profile is set on client' do
stub_const(
'ENV',
'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB',
'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB'
)
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'fooprofile', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('ACCESS_KEY_1')
end
it 'will not load credentials from shared config' do
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'creds_from_cfg', region: 'us-east-1'
)
expect(client.config.credentials).to eq(nil)
end
it 'will not attempt to assume a role' do
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'assumerole_sc', region: 'us-east-1'
)
expect(client.config.credentials).to eq(nil)
end
it 'attempts to fetch metadata credentials last' do
allow(InstanceProfileCredentials).to receive(:new).and_call_original
stub_request(:put, 'http://169.254.169.254/latest/api/token')
.to_return(
status: 200,
body: "my-token\n",
headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' }
)
stub_request(
:get,
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
).with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: "profile-name\n")
stub_request(
:get,
'http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name'
).with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' })
.to_return(status: 200, body: <<-JSON.strip)
{
"Code" : "Success",
"LastUpdated" : "2013-11-22T20:03:48Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "akid-md",
"SecretAccessKey" : "secret-md",
"Token" : "session-token-md",
"Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}"
}
JSON
client = ApiHelper.sample_rest_xml::Client.new(
profile: 'nonexistant', region: 'us-east-1'
)
expect(
client.config.credentials.credentials.access_key_id
).to eq('akid-md')
end
end
def assume_role_stub(role_arn, input_access_key, access_key, secret_key, token)
stub_request(:post, 'https://sts.us-east-1.amazonaws.com/')
.with(headers: { 'authorization' => /Credential=#{input_access_key}/ })
.to_return(body: <<-RESP)
#{role_arn}
ASSUMEROLEID:session
#{access_key}
#{secret_key}
#{token}
#{(Time.now + 3600).utc.iso8601}
MyStubRequest
RESP
end
def assume_role_web_identity_stub(role_arn, access_key, secret_key, token)
stub_token_file('token')
stub_request(:post, 'https://sts.us-east-1.amazonaws.com/')
.to_return(body: <<-RESP)
my-cluster.sk1.us-west-2.eks.amazonaws.com
StubbedRoleId
#{role_arn}
MockProvider
#{access_key}
#{secret_key}
#{token}
#{(Time.now + 3600).utc.iso8601}
system:serviceaccount:default:default
MyStubbedRequest
RESP
end
def stub_token_file(token)
allow(File).to receive(:exist?).with('my-token.jwt').and_return(true)
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with('my-token.jwt').and_return(token)
end
end
end