aws_session_token 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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