Skip to content

Commit 1d2698a

Browse files
authored
Support array arguments for process credentials (#3048)
1 parent 03e4f2d commit 1d2698a

File tree

4 files changed

+104
-45
lines changed

4 files changed

+104
-45
lines changed

gems/aws-sdk-core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Issue - Support an array of string arguments for `Aws::ProcessCredentials` to be executed by `system`.
5+
46
3.197.0 (2024-06-05)
57
------------------
68

gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
module Aws
44
# A credential provider that executes a given process and attempts
5-
# to read its stdout to recieve a JSON payload containing the credentials.
5+
# to read its stdout to receive a JSON payload containing the credentials.
66
#
7-
# credentials = Aws::ProcessCredentials.new('/usr/bin/credential_proc')
7+
# credentials = Aws::ProcessCredentials.new(['/usr/bin/credential_proc'])
8+
# ec2 = Aws::EC2::Client.new(credentials: credentials)
9+
#
10+
# Arguments should be provided as strings in the array, for example:
11+
#
12+
# process = ['/usr/bin/credential_proc', 'arg1', 'arg2']
13+
# credentials = Aws::ProcessCredentials.new(process)
814
# ec2 = Aws::EC2::Client.new(credentials: credentials)
915
#
1016
# Automatically handles refreshing credentials if an Expiration time is
@@ -19,40 +25,49 @@ class ProcessCredentials
1925
# Creates a new ProcessCredentials object, which allows an
2026
# external process to be used as a credential provider.
2127
#
22-
# @param [String] process Invocation string for process
23-
# credentials provider.
28+
# @param [Array<String>, String] process An array of strings including
29+
# the process name and its arguments to execute, or a single string to be
30+
# executed by the shell (deprecated and insecure).
2431
def initialize(process)
32+
if process.is_a?(String)
33+
warn('Passing a single string to Aws::ProcessCredentials.new '\
34+
'is insecure, please use use an array of system arguments instead')
35+
end
2536
@process = process
26-
@credentials = credentials_from_process(@process)
37+
@credentials = credentials_from_process
2738
@async_refresh = false
2839

2940
super
3041
end
3142

3243
private
33-
def credentials_from_process(proc_invocation)
34-
begin
35-
raw_out = `#{proc_invocation}`
36-
process_status = $?
37-
rescue Errno::ENOENT
38-
raise Errors::InvalidProcessCredentialsPayload.new("Could not find process #{proc_invocation}")
44+
45+
def credentials_from_process
46+
r, w = IO.pipe
47+
success = system(*@process, out: w)
48+
w.close
49+
raw_out = r.read
50+
r.close
51+
52+
unless success
53+
raise Errors::InvalidProcessCredentialsPayload.new(
54+
'credential_process provider failure, the credential process had '\
55+
'non zero exit status and failed to provide credentials'
56+
)
3957
end
4058

41-
if process_status.success?
42-
begin
43-
creds_json = Aws::Json.load(raw_out)
44-
rescue Aws::Json::ParseError
45-
raise Errors::InvalidProcessCredentialsPayload.new("Invalid JSON response")
46-
end
47-
payload_version = creds_json['Version']
48-
if payload_version == 1
49-
_parse_payload_format_v1(creds_json)
50-
else
51-
raise Errors::InvalidProcessCredentialsPayload.new("Invalid version #{payload_version} for credentials payload")
52-
end
53-
else
54-
raise Errors::InvalidProcessCredentialsPayload.new('credential_process provider failure, the credential process had non zero exit status and failed to provide credentials')
59+
begin
60+
creds_json = Aws::Json.load(raw_out)
61+
rescue Aws::Json::ParseError
62+
raise Errors::InvalidProcessCredentialsPayload.new('Invalid JSON response')
5563
end
64+
65+
payload_version = creds_json['Version']
66+
return _parse_payload_format_v1(creds_json) if payload_version == 1
67+
68+
raise Errors::InvalidProcessCredentialsPayload.new(
69+
"Invalid version #{payload_version} for credentials payload"
70+
)
5671
end
5772

5873
def _parse_payload_format_v1(creds_json)
@@ -64,11 +79,14 @@ def _parse_payload_format_v1(creds_json)
6479

6580
@expiration = creds_json['Expiration'] ? Time.iso8601(creds_json['Expiration']) : nil
6681
return creds if creds.set?
67-
raise Errors::InvalidProcessCredentialsPayload.new("Invalid payload for JSON credentials version 1")
82+
83+
raise Errors::InvalidProcessCredentialsPayload.new(
84+
'Invalid payload for JSON credentials version 1'
85+
)
6886
end
6987

7088
def refresh
71-
@credentials = credentials_from_process(@process)
89+
@credentials = credentials_from_process
7290
end
7391

7492
def near_expiration?(expiration_length)

gems/aws-sdk-core/spec/aws/process_credentials_spec.rb

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,87 @@
55
module Aws
66
describe ProcessCredentials do
77

8-
before(:each) do
9-
stub_const('ENV', {})
8+
before do
109
allow(Dir).to receive(:home).and_raise(ArgumentError)
1110
end
12-
11+
1312
it 'will read credentials from a process' do
14-
creds = ProcessCredentials.new('echo \'{"Version":1,"AccessKeyId":"AK_PROC1","SecretAccessKey":"SECRET_AK_PROC1","SessionToken":"TOKEN_PROC1"}\'').credentials
13+
process = %w[echo {"Version":1,"AccessKeyId":"AK_PROC1","SecretAccessKey":"SECRET_AK_PROC1","SessionToken":"TOKEN_PROC1"}]
14+
creds = ProcessCredentials.new(process).credentials
1515
expect(creds.access_key_id).to eq('AK_PROC1')
1616
expect(creds.secret_access_key).to eq('SECRET_AK_PROC1')
1717
expect(creds.session_token).to eq('TOKEN_PROC1')
1818
end
1919

2020
it 'will throw an error when invalid JSON is returned' do
21+
process = %w[echo {"Version":3,"AccessKeyId":"","SecretAccessKey":""\']
2122
expect {
22-
creds = ProcessCredentials.new('echo \'{"Version":3,"AccessKeyId":"","SecretAccessKey":"","SessionToken":""\'').credentials
23+
ProcessCredentials.new(process).credentials
2324
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
2425
end
2526

26-
it 'will throw an error when the process credentials payload version is invalid' do
27+
it 'will throw an error when the process credentials payload version is invalid' do
28+
process = %w[echo {"Version":3,"AccessKeyId":"","SecretAccessKey":""}]
2729
expect {
28-
creds = ProcessCredentials.new('echo \'{"Version":3,"AccessKeyId":"","SecretAccessKey":"","SessionToken":""}\'').credentials
30+
ProcessCredentials.new(process).credentials
2931
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
3032
end
3133

32-
it 'will throw an error when the process credentials payload is malformed' do
34+
it 'will throw an error when the process credentials payload is malformed' do
35+
process = %w[echo {"Version":1}]
3336
expect {
34-
creds = ProcessCredentials.new('echo \'{"Version":1}\'').credentials
37+
ProcessCredentials.new(process).credentials
3538
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
3639
end
3740

38-
it 'will throw an error and expose the stderr output when the credential process has a nonzero exit status' do
41+
it 'will throw an error when the credential process has a nonzero exit status' do
42+
process = ['fake_proc']
3943
expect {
40-
creds = ProcessCredentials.new('>&2 echo "Credential Provider Error"; false').credentials
44+
ProcessCredentials.new(process).credentials
4145
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
42-
.and output("Credential Provider Error\n").to_stderr_from_any_process
4346
end
4447

45-
it 'will throw an error when the credential process cant be found' do
46-
expect {
47-
creds = ProcessCredentials.new('fake_proc').credentials
48-
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
48+
context 'legacy process string' do
49+
before do
50+
expect_any_instance_of(ProcessCredentials)
51+
.to receive(:warn).with(/array of system arguments/)
52+
end
53+
54+
it 'will read credentials from a process' do
55+
process = 'echo \'{"Version":1,"AccessKeyId":"AK_PROC1","SecretAccessKey":"SECRET_AK_PROC1","SessionToken":"TOKEN_PROC1"}\''
56+
creds = ProcessCredentials.new(process).credentials
57+
expect(creds.access_key_id).to eq('AK_PROC1')
58+
expect(creds.secret_access_key).to eq('SECRET_AK_PROC1')
59+
expect(creds.session_token).to eq('TOKEN_PROC1')
60+
end
61+
62+
it 'will throw an error when invalid JSON is returned' do
63+
process = 'echo \'{"Version":3,"AccessKeyId":"","SecretAccessKey":""\''
64+
expect {
65+
ProcessCredentials.new(process).credentials
66+
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
67+
end
68+
69+
it 'will throw an error when the process credentials payload version is invalid' do
70+
process = 'echo \'{"Version":3,"AccessKeyId":"","SecretAccessKey":""}\''
71+
expect {
72+
ProcessCredentials.new(process).credentials
73+
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
74+
end
75+
76+
it 'will throw an error when the process credentials payload is malformed' do
77+
process = 'echo \'{"Version":1}\''
78+
expect {
79+
ProcessCredentials.new(process).credentials
80+
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
81+
end
82+
83+
it 'will throw an error when the credential process has a nonzero exit status' do
84+
process = 'fake_proc'
85+
expect {
86+
ProcessCredentials.new(process).credentials
87+
}.to raise_error(Errors::InvalidProcessCredentialsPayload)
88+
end
4989
end
5090
end
5191
end

gems/aws-sdk-core/spec/shared_spec_helper.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323

2424
config.before(:each) do
2525
# Clear the current ENV to avoid loading credentials.
26-
# This was previously mocked with stub_const but was provided a hash.
27-
ENV.clear
26+
ENV.keep_if { |k, _| k == 'PATH' }
2827

2928
# disable loading credentials from shared file
3029
allow(Dir).to receive(:home).and_raise(ArgumentError)

0 commit comments

Comments
 (0)