aws_session_token 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # AWS Session Token Gem - Tool to wrap AWS API to create and store
5
+ # Session tokens so that other commands/tools (e.g. Terraform) can function as
6
+ # necessary.
7
+ #
8
+ #
9
+ # Copyright 2018 Bryan Stopp <bryan.stopp@gmail.com>
10
+ #
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions and
21
+ # limitations under the License.
22
+ #
23
+
24
+ module AwsSessionToken
25
+
26
+ # Helper class for interacting with the Credentials file.
27
+ class CredentialsFile
28
+
29
+ Profile = Struct.new(:name, :data)
30
+
31
+ def write(filename, profile, credentials)
32
+ file = nil
33
+ begin
34
+ profiles = read_profiles(filename)
35
+ file = File.open(filename, 'w')
36
+ write_file(credentials, file, profile, profiles)
37
+ ensure
38
+ file&.close
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def read_profiles(credentials_file)
45
+ profiles = []
46
+ profile = nil
47
+ File.readlines(credentials_file).each do |line|
48
+ if line =~ /^\[/
49
+ profile = Profile.new(line, [])
50
+ profiles << profile
51
+ else
52
+ profile.data << line
53
+ end
54
+ end
55
+ profiles
56
+ end
57
+
58
+ def write_file(credentials, file, profile, profiles)
59
+ found = false
60
+ profiles.each do |p|
61
+ if p.name =~ /^\[#{profile}\]/
62
+ write_session(file, profile, credentials)
63
+ found = true
64
+ else
65
+ write_profile(file, p)
66
+ end
67
+ end
68
+ write_session(file, profile, credentials) unless found
69
+ end
70
+
71
+ def write_profile(file, profile)
72
+ file.puts(profile.name)
73
+ profile.data.each do |l|
74
+ file.puts(l)
75
+ end
76
+ end
77
+
78
+ def write_session(file, profile, creds)
79
+ file.puts("[#{profile}]")
80
+ file.puts("aws_access_key_id = #{creds.access_key_id}")
81
+ file.puts("aws_secret_access_key = #{creds.secret_access_key}")
82
+ file.puts("aws_session_token = #{creds.session_token}")
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # AWS Session Token Gem - Tool to wrap AWS API to create and store Session tokens
5
+ # so that other commands/tools (e.g. Terraform) can function as necessary.
6
+ #
7
+ # Copyright 2018 Bryan Stopp <bryan.stopp@gmail.com>
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the 'License');
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an 'AS IS' BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+
21
+ module AwsSessionToken
22
+
23
+ # Options class to define properties for the command line.
24
+ class Options
25
+
26
+ SESSION_PROFILE = 'session_profile'
27
+ DURATION = 3600
28
+
29
+ attr_accessor :credentials_file, :duration, :profile, :profile_provided, :session_profile, :token, :user
30
+
31
+ def initialize
32
+ creds = Aws::SharedCredentials.new
33
+ self.credentials_file = creds.path
34
+ self.profile = creds.profile_name
35
+ self.session_profile = SESSION_PROFILE
36
+ self.duration = DURATION
37
+ self.profile_provided = false
38
+ end
39
+
40
+ def parse(args)
41
+ define_options.parse!(args)
42
+ validate
43
+ end
44
+
45
+ private
46
+
47
+ def define_options
48
+ opts = OptionParser.new
49
+ opts.banner = 'Usage: aws_session_token [options]'
50
+ opts.separator('')
51
+
52
+ # Additional options
53
+ file_option(opts)
54
+ user_option(opts)
55
+ profile_option(opts)
56
+ session_profile_option(opts)
57
+ duration_option(opts)
58
+ token_option(opts)
59
+ common_options(opts)
60
+ opts
61
+ end
62
+
63
+ def file_option(opts)
64
+ opts.on('-f', '--file FILE', 'Specify a custom credentials file.') do |f|
65
+ self.credentials_file = f
66
+ end
67
+ end
68
+
69
+ def user_option(opts)
70
+ opts.on('-u', '--user USER',
71
+ 'Specify the AWS User name for passing to API.') do |u|
72
+ self.user = u
73
+ end
74
+ end
75
+
76
+ def profile_option(opts)
77
+ opts.on('-p', '--profile PROFILE',
78
+ 'Specify the AWS credentials profile to use. Also sets user, if user is not provided.') do |p|
79
+ self.profile = p
80
+ self.profile_provided = true
81
+ end
82
+ end
83
+
84
+ def session_profile_option(opts)
85
+ opts.on('-s', '--session SESSION_PROFILE',
86
+ 'Specify the name of the profile used to store the session credentials.') do |s|
87
+ self.session_profile = s
88
+ end
89
+ end
90
+
91
+ def duration_option(opts)
92
+ opts.on('-d', '--duration DURATION', Integer,
93
+ 'Specify the duration the of the token in seconds. (Default 3600)') do |d|
94
+ self.duration = d
95
+ end
96
+ end
97
+
98
+ def token_option(opts)
99
+ opts.on('-t', '--token TOKEN', Integer,
100
+ 'Specify the OTP Token to use for creating the session credentials.') do |t|
101
+ self.token = t
102
+ end
103
+ end
104
+
105
+ def common_options(opts)
106
+ opts.separator('')
107
+ opts.separator('Common options:')
108
+ opts.on_tail('-h', '--help', 'Show this message.') do
109
+ puts opts
110
+ exit
111
+ end
112
+ opts.on_tail('-v', '--version', 'Show version.') do
113
+ puts SemVer.find.format(+ '%M.%m.%p%s')
114
+ exit
115
+ end
116
+ end
117
+
118
+ def validate
119
+ validate_profiles
120
+ end
121
+
122
+ def validate_profiles
123
+ raise ArgumentError, 'Profile and Session Profile must be different.' if profile == session_profile
124
+ self.user ||= profile if profile_provided
125
+ end
126
+ end
127
+
128
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # AWS Session Token Gem - Tool to wrap AWS API to create and store Session tokens
5
+ # so that other commands/tools (e.g. Terraform) can function as necessary.
6
+ #
7
+ # Copyright 2018 Bryan Stopp <bryan.stopp@gmail.com>
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+
22
+ require 'spec_helper'
23
+ require 'aws_session_token/credentials_file'
24
+
25
+ describe AwsSessionToken::CLI do
26
+
27
+ before do
28
+ $stdout = StringIO.new
29
+ $stderr = StringIO.new
30
+ end
31
+
32
+ after do
33
+ $stdout = STDOUT
34
+ $stderr = STDERR
35
+ end
36
+
37
+ subject(:cli) { described_class.new }
38
+
39
+ let(:creds_data) do
40
+ Aws::STS::Types::Credentials.new(
41
+ access_key_id: 'AKIAIOSFODNN7EXAMPLE',
42
+ expiration: Time.parse('2011-07-11T19:55:29.611Z'),
43
+ secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY',
44
+ session_token: 'EXAMPLE SESSION TOKEN'
45
+ )
46
+ end
47
+
48
+ let(:mfa_arn) { 'Fake Serial Number' }
49
+
50
+ let(:mfa_token) { 123_456 }
51
+
52
+ describe 'run' do
53
+ it 'should work' do
54
+ expect(cli).to receive(:set_aws_creds)
55
+ expect(cli).to receive(:mfa_device).and_return(mfa_arn)
56
+ expect(cli).to receive(:token_prompt).and_return(mfa_token)
57
+ expect(cli).to receive(:session_token).with(mfa_arn, mfa_token).and_return(creds_data)
58
+ expect_any_instance_of(AwsSessionToken::CredentialsFile).to receive(:write).with(
59
+ Aws::SharedCredentials.new.path,
60
+ AwsSessionToken::Options::SESSION_PROFILE,
61
+ creds_data
62
+ )
63
+ cli.run
64
+ end
65
+
66
+ describe 'token provided' do
67
+ before do
68
+ ARGV << '-t'
69
+ ARGV << mfa_token.to_s
70
+ end
71
+
72
+ it 'should skip token prompt' do
73
+ expect(cli).to receive(:set_aws_creds)
74
+ expect(cli).to receive(:mfa_device).and_return(mfa_arn)
75
+ expect(cli).to receive(:session_token).with(mfa_arn, mfa_token).and_return(creds_data)
76
+ expect_any_instance_of(AwsSessionToken::CredentialsFile).to receive(:write).with(
77
+ Aws::SharedCredentials.new.path,
78
+ AwsSessionToken::Options::SESSION_PROFILE,
79
+ creds_data
80
+ )
81
+ cli.run
82
+ end
83
+ end
84
+ end
85
+
86
+ describe 'validate_creds_file' do
87
+ it 'should succeed if file exists' do
88
+ creds = Aws::SharedCredentials.new
89
+ expect(File).to receive(:exist?).with(creds.path).and_return(true)
90
+ expect { cli.validate_creds_file }.to_not raise_error
91
+ end
92
+
93
+ it 'should fail if file is missing' do
94
+ creds = Aws::SharedCredentials.new
95
+ expect(File).to receive(:exist?).with(creds.path).at_least(:once).and_return(false)
96
+ expect { cli.validate_creds_file }.to raise_error(ArgumentError, /Specified credentials file is missing/)
97
+ end
98
+
99
+ it 'should fail if file is not writable by current user' do
100
+ creds = Aws::SharedCredentials.new
101
+ expect(File).to receive(:exist?).with(creds.path).at_least(:once).and_return(true)
102
+ expect(File).to receive(:writable?).with(creds.path).at_least(:once).and_return(false)
103
+ expect { cli.validate_creds_file }.to raise_error(ArgumentError, /Specified credentials file cannot /)
104
+ end
105
+ end
106
+
107
+ describe 'set_aws_creds' do
108
+ it 'is should work' do
109
+ expect(Aws.config).to receive(:update).with(
110
+ credentials: instance_of(Aws::SharedCredentials)
111
+ )
112
+ cli.set_aws_creds
113
+ end
114
+ it 'is should fail if profile does not exist' do
115
+ cli.options.profile = 'nonexistent'
116
+ expect { cli.set_aws_creds }.to exit_with_code(1)
117
+ expect($stderr.string).to match(/Specified AWS Profile doesn't exist: nonexistent/)
118
+ end
119
+ end
120
+
121
+ describe 'mfa_device' do
122
+ let(:mfa_device) do
123
+ mfa = Aws::IAM::Types::MFADevice.new
124
+ mfa.user_name = 'username'
125
+ mfa.serial_number = 'serial-number'
126
+ mfa.enable_date = 'enable-date'
127
+ mfa
128
+ end
129
+
130
+ it 'should work without user' do
131
+ params = { max_items: 1 }
132
+ client = double('iam_client')
133
+ response = Aws::IAM::Types::ListMFADevicesResponse.new(mfa_devices: [mfa_device])
134
+ expect(Aws::IAM::Client).to receive(:new).and_return(client)
135
+ expect(client).to receive(:list_mfa_devices).with(params).and_return(response)
136
+ expect(cli.mfa_device).to eq('serial-number')
137
+ end
138
+ it 'should work with user' do
139
+ cli.options.user = 'foo'
140
+ params = { user_name: 'foo', max_items: 1 }
141
+ client = double('iam_client')
142
+ response = Aws::IAM::Types::ListMFADevicesResponse.new(mfa_devices: [mfa_device])
143
+ expect(Aws::IAM::Client).to receive(:new).and_return(client)
144
+ expect(client).to receive(:list_mfa_devices).with(params).and_return(response)
145
+ expect(cli.mfa_device).to eq('serial-number')
146
+ end
147
+ it 'should exit if no MFA devices' do
148
+
149
+ client = double('iam_client')
150
+ response = Aws::IAM::Types::ListMFADevicesResponse.new(mfa_devices: [])
151
+ expect(Aws::IAM::Client).to receive(:new).and_return(client)
152
+ expect(client).to receive(:list_mfa_devices).and_return(response)
153
+ begin
154
+ expect { cli.mfa_device }.to exit_with_code(0)
155
+ rescue SystemExit # rubocop:disable Lint/HandleExceptions
156
+ end
157
+ expected = 'Script execution unnecessary.'
158
+ expect($stderr.string).to match(/#{expected}/)
159
+ end
160
+ end
161
+
162
+ context 'token_prompt' do
163
+ before do
164
+ $stdin = StringIO.new
165
+ $stdin.puts('123456')
166
+ $stdin.rewind
167
+ end
168
+ it 'should work' do
169
+ expect { cli.token_prompt }.to_not raise_error
170
+ end
171
+ end
172
+
173
+ describe 'session_token' do
174
+ let(:response) do
175
+ creds = Aws::STS::Types::Credentials.new
176
+ creds.access_key_id = 'access_key_id'
177
+ creds.secret_access_key = 'secret_access_key'
178
+ creds.expiration = 'expiration'
179
+ creds.session_token = 'session_token'
180
+
181
+ resp = Aws::STS::Types::GetSessionTokenResponse.new
182
+ resp.credentials = creds
183
+ resp
184
+ end
185
+
186
+ it 'should work' do
187
+ device = 'device'
188
+ token = 'token'
189
+ client = double('sts-client')
190
+ expect(Aws::STS::Client).to receive(:new).and_return(client)
191
+ expect(client).to receive(:get_session_token).with(
192
+ duration_seconds: 3600, serial_number: device, token_code: token
193
+ ).and_return(response)
194
+ expect(cli.session_token(device, token)).to eq(response.credentials)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # AWS Session Token Gem - Tool to wrap AWS API to create and store
5
+ # Session tokens so that other commands/tools (e.g. Terraform) can function as
6
+ # necessary.
7
+ #
8
+ #
9
+ # Copyright 2018 Bryan Stopp <bryan.stopp@gmail.com>
10
+ #
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions and
21
+ # limitations under the License.
22
+ #
23
+
24
+ require 'spec_helper'
25
+
26
+ describe AwsSessionToken::CredentialsFile, :isolated_environment do
27
+ before do
28
+ $stdout = StringIO.new
29
+ $stderr = StringIO.new
30
+ end
31
+
32
+ after do
33
+ $stdout = STDOUT
34
+ $stderr = STDERR
35
+ end
36
+
37
+ subject(:cf) { described_class.new }
38
+
39
+ let(:creds) do
40
+ creds = Aws::STS::Types::Credentials.new
41
+ creds.access_key_id = 'access_key_id'
42
+ creds.secret_access_key = 'secret_access_key'
43
+ creds.expiration = 'expiration'
44
+ creds.session_token = 'session_token'
45
+ creds
46
+ end
47
+
48
+ describe 'write' do
49
+ let(:file_contents) do
50
+ <<~CREDS
51
+ [stopp]
52
+ aws_access_key_id = EXAMPLESTOPPACCESSID
53
+ aws_secret_access_key = EXAMPLESTOPPSECRETKEY
54
+ [admin]
55
+ aws_access_key_id = EXAMPLEADMINACCESSID
56
+ aws_secret_access_key = EXAMPLEADMINSECRETKEY
57
+ CREDS
58
+ end
59
+
60
+ shared_examples 'update file' do |opts|
61
+ it do
62
+ file = StringIO.new
63
+ default_creds = Aws::SharedCredentials.new
64
+ expect(File).to receive(:readlines).with(default_creds.path).and_return(file_contents.split("\n"))
65
+ expect(File).to receive(:open).with(default_creds.path, 'w').and_return(file)
66
+ cf.write(default_creds.path, opts[:profile], creds)
67
+ expect(file.string).to eq(opts[:expected])
68
+ end
69
+ end
70
+
71
+ describe 'profile does not exist' do
72
+ contents = <<~CREDS
73
+ [stopp]
74
+ aws_access_key_id = EXAMPLESTOPPACCESSID
75
+ aws_secret_access_key = EXAMPLESTOPPSECRETKEY
76
+ [admin]
77
+ aws_access_key_id = EXAMPLEADMINACCESSID
78
+ aws_secret_access_key = EXAMPLEADMINSECRETKEY
79
+ [session_profile]
80
+ aws_access_key_id = access_key_id
81
+ aws_secret_access_key = secret_access_key
82
+ aws_session_token = session_token
83
+ CREDS
84
+
85
+ it_should_behave_like('update file', expected: contents, profile: 'session_profile')
86
+ end
87
+
88
+ describe 'profile exists' do
89
+ describe 'first profile' do
90
+ contents = <<~CREDS
91
+ [stopp]
92
+ aws_access_key_id = access_key_id
93
+ aws_secret_access_key = secret_access_key
94
+ aws_session_token = session_token
95
+ [admin]
96
+ aws_access_key_id = EXAMPLEADMINACCESSID
97
+ aws_secret_access_key = EXAMPLEADMINSECRETKEY
98
+ CREDS
99
+
100
+ it_should_behave_like('update file', expected: contents, profile: 'stopp')
101
+ end
102
+ describe 'last profile' do
103
+ contents = <<~CREDS
104
+ [stopp]
105
+ aws_access_key_id = EXAMPLESTOPPACCESSID
106
+ aws_secret_access_key = EXAMPLESTOPPSECRETKEY
107
+ [admin]
108
+ aws_access_key_id = access_key_id
109
+ aws_secret_access_key = secret_access_key
110
+ aws_session_token = session_token
111
+ CREDS
112
+ it_should_behave_like('update file', expected: contents, profile: 'admin')
113
+ end
114
+ end
115
+ end
116
+ end