Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 build_tools/spec/region_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def gem_lib_paths
def whitelist
{
"core" => {
"errors.rb" => 137,
"errors.rb" => 141,
"signature_v4.rb" => 35,
"stub_responses.rb" => 19
},
Expand Down
2 changes: 2 additions & 0 deletions gems/aws-sdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Extensible Credential Providers, allows you to declare an executable to be run that outputs the credentials as a JSON payload allowing you to develop custom credential providers and easily add them to the credential resolution chain, [Docs](https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes)

3.22.1 (2018-06-28)
------------------

Expand Down
1 change: 1 addition & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require_relative 'aws-sdk-core/ecs_credentials'
require_relative 'aws-sdk-core/instance_profile_credentials'
require_relative 'aws-sdk-core/shared_credentials'
require_relative 'aws-sdk-core/process_credentials'

# client modules

Expand Down
15 changes: 15 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def providers
[:env_credentials, {}],
[:assume_role_credentials, {}],
[:shared_credentials, {}],
[:process_credentials, {}],
[:instance_profile_credentials, {
retries: @config ? @config.instance_profile_credentials_retries : 0,
http_open_timeout: @config ? @config.instance_profile_credentials_timeout : 1,
Expand Down Expand Up @@ -69,6 +70,20 @@ def shared_credentials(options)
nil
end

def process_credentials(options)
profile_name = options[:config].profile if options[:config]
profile_name ||= ENV['AWS_PROFILE'].nil? ? 'default' : ENV['AWS_PROFILE']

config = Aws.shared_config
if config.config_enabled? && process_provider = config.credentials_process(profile_name)
ProcessCredentials.new(process_provider)
else
nil
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make the "else nil" explicit here.

rescue Errors::NoSuchProfileError
nil
end

def assume_role_credentials(options)
if Aws.shared_config.config_enabled?
profile, region = nil, nil
Expand Down
4 changes: 4 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ def initialize(*args)
end
end

# Raised when a credentials provider process returns a JSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surprise! ;) you need to update the region line here: https://github.com/aws/aws-sdk-ruby/blob/master/build_tools/spec/region_spec.rb#L12

That spec is for testing we don't hard code region names, since the error file has been modified, the line number need to be modified as well :D That' why you are seeing the travis error

# payload with either invalid version number or malformed contents
class InvalidProcessCredentialsPayload < RuntimeError; end

# Raised when a client is constructed and region is not specified.
class MissingRegionError < ArgumentError
def initialize(*args)
Expand Down
74 changes: 74 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'open3'

module Aws

# A credential provider that executes a given process and attempts
# to read its stdout to recieve a JSON payload containing the credentials
#
# Automatically handles refreshing credentials if an Expiration time is
# provided in the credentials payload
#
# credentials = Aws::ProcessCredentials.new('/usr/bin/credential_proc').credentials
#
# ec2 = Aws::EC2::Client.new(credentials: credentials)
#
# More documentation on process based credentials can be found here:
# https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
class ProcessCredentials

include CredentialProvider
include RefreshingCredentials

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add some doc here like an example of how to use this?
e.g https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/AssumeRoleCredentials.html

# Creates a new ProcessCredentials object, which allows an
# external process to be used as a credential provider.
#
# @param [String] process Invocation string for process
# credentials provider.
def initialize(process)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add more documentation for #initialize like what kind of process variable it's expecting?

@process = process
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're close now, at this point we just need to make sure we're splitting and escaping the string in the same way Python/CLI does: https://github.com/boto/botocore/blob/25a6a4c7c8a8b1797fc78c6a34dc7bca3f0bfba1/botocore/credentials.py#L793-L796

@credentials = credentials_from_process(@process)
end

private
def credentials_from_process(proc_invocation)
begin
raw_out, process_status = Open3.capture2(proc_invocation)
rescue Errno::ENOENT
raise Errors::InvalidProcessCredentialsPayload.new("Could not find process #{proc_invocation}")
end

if process_status.success?
creds_json = JSON.parse(raw_out)
payload_version = creds_json['Version']
if payload_version == 1
_parse_payload_format_v1(creds_json)
else
raise Errors::InvalidProcessCredentialsPayload.new("Invalid version #{payload_version} for credentials payload")
end
else
raise Errors::InvalidProcessCredentialsPayload.new('credential_process provider failure, the credential process had non zero exit status and failed to provide credentials')
end
end

def _parse_payload_format_v1(creds_json)
creds = Credentials.new(
creds_json['AccessKeyId'],
creds_json['SecretAccessKey'],
creds_json['SessionToken']
)

@expiration = creds_json['Expiration'] ? Time.iso8601(creds_json['Expiration']) : nil
return creds if creds.set?
raise Errors::InvalidProcessCredentialsPayload.new("Invalid payload for JSON credentials version 1")
end

def refresh
@credentials = credentials_from_process(@process)
end

def near_expiration?
# are we within 5 minutes of expiration?
@expiration && (Time.now.to_i + 5 * 60) > @expiration.to_i
end
end
end
5 changes: 5 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ def region(opts = {})
end
end

def credentials_process(profile)
validate_profile_exists(profile)
@parsed_config[profile]['credential_process']
end

private
def credentials_present?
(@parsed_credentials && !@parsed_credentials.empty?) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ module Aws
expect(client.config.credentials.access_key_id).to eq("ACCESS_KEY_SC1")
end

it 'prefers process credentials over metadata credentials' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last bit here, I would also add some tests with this same profile where we ensure that static profile credentials, for example, are pulled in first. It's a bit thorough, but important for regression.

client = ApiHelper.sample_rest_xml::Client.new(profile: "creds_from_process", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AK_PROC1")
end

it 'attempts to fetch metadata credentials last' do
stub_request(
:get,
Expand Down
43 changes: 43 additions & 0 deletions gems/aws-sdk-core/spec/aws/process_credentials_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require_relative '../spec_helper'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test suites looks good, just checking does these covered all cli test suits for the process credentials as well?

module Aws
describe ProcessCredentials do

before(:each) do
stub_const('ENV', {})
allow(Dir).to receive(:home).and_raise(ArgumentError)
end

it 'will read credentials from a process' do
creds = ProcessCredentials.new('echo \'{"Version":1,"AccessKeyId":"AK_PROC1","SecretAccessKey":"SECRET_AK_PROC1","SessionToken":"TOKEN_PROC1"}\'').credentials
expect(creds.access_key_id).to eq('AK_PROC1')
expect(creds.secret_access_key).to eq('SECRET_AK_PROC1')
expect(creds.session_token).to eq('TOKEN_PROC1')
end

it 'will throw an error when the process credentials payload version is invalid' do
expect {
creds = ProcessCredentials.new('echo \'{"Version":3,"AccessKeyId":"","SecretAccessKey":"","SessionToken":""}\'').credentials
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
end

it 'will throw an error when the process credentials payload is malformed' do
expect {
creds = ProcessCredentials.new('echo \'{"Version":1}\'').credentials
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
end

it 'will throw an error and expose the stderr output when the credential process has a nonzero exit status' do
expect {
creds = ProcessCredentials.new('>&2 echo "Credential Provider Error"; false').credentials
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
.and output("Credential Provider Error\n").to_stderr_from_any_process
end

it 'will throw an error when the credential process cant be found' do
expect {
creds = ProcessCredentials.new('fake_proc').credentials
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,6 @@ role_arn = arn:aws:iam:123456789012:role/bar
aws_access_key_id = ACCESS_KEY_ARPC
aws_secret_access_key = SECRET_KEY_ARPC
aws_session_token = TOKEN_ARPC

[profile creds_from_process]
credential_process = echo '{ "Version": 1, "AccessKeyId": "AK_PROC1", "SecretAccessKey": "SECRET_AK_PROC1", "SessionToken": "TOKEN_PROC1" }'