aws_assume_role 1.0.6-linux

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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +57 -0
  4. data/.ruby-version +1 -0
  5. data/.simplecov +22 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +57 -0
  8. data/Gemfile +18 -0
  9. data/LICENSE.md +201 -0
  10. data/README.md +303 -0
  11. data/Rakefile +47 -0
  12. data/aws_assume_role.gemspec +56 -0
  13. data/bin/aws-assume-role +4 -0
  14. data/i18n/en.yml +109 -0
  15. data/lib/aws_assume_role/cli/actions/abstract_action.rb +61 -0
  16. data/lib/aws_assume_role/cli/actions/configure_profile.rb +24 -0
  17. data/lib/aws_assume_role/cli/actions/configure_role_assumption.rb +22 -0
  18. data/lib/aws_assume_role/cli/actions/console.rb +70 -0
  19. data/lib/aws_assume_role/cli/actions/delete_profile.rb +22 -0
  20. data/lib/aws_assume_role/cli/actions/includes.rb +12 -0
  21. data/lib/aws_assume_role/cli/actions/list_profiles.rb +12 -0
  22. data/lib/aws_assume_role/cli/actions/migrate_profile.rb +20 -0
  23. data/lib/aws_assume_role/cli/actions/reset_environment.rb +50 -0
  24. data/lib/aws_assume_role/cli/actions/run.rb +36 -0
  25. data/lib/aws_assume_role/cli/actions/set_environment.rb +62 -0
  26. data/lib/aws_assume_role/cli/actions/test.rb +35 -0
  27. data/lib/aws_assume_role/cli/commands/configure.rb +32 -0
  28. data/lib/aws_assume_role/cli/commands/console.rb +19 -0
  29. data/lib/aws_assume_role/cli/commands/delete.rb +13 -0
  30. data/lib/aws_assume_role/cli/commands/environment.rb +34 -0
  31. data/lib/aws_assume_role/cli/commands/list.rb +12 -0
  32. data/lib/aws_assume_role/cli/commands/migrate.rb +13 -0
  33. data/lib/aws_assume_role/cli/commands/run.rb +19 -0
  34. data/lib/aws_assume_role/cli/commands/test.rb +20 -0
  35. data/lib/aws_assume_role/cli/includes.rb +3 -0
  36. data/lib/aws_assume_role/cli.rb +20 -0
  37. data/lib/aws_assume_role/configuration.rb +30 -0
  38. data/lib/aws_assume_role/core_ext/aws-sdk/credential_provider_chain.rb +4 -0
  39. data/lib/aws_assume_role/core_ext/aws-sdk/includes.rb +9 -0
  40. data/lib/aws_assume_role/credentials/factories/abstract_factory.rb +33 -0
  41. data/lib/aws_assume_role/credentials/factories/assume_role.rb +39 -0
  42. data/lib/aws_assume_role/credentials/factories/default_chain_provider.rb +113 -0
  43. data/lib/aws_assume_role/credentials/factories/environment.rb +26 -0
  44. data/lib/aws_assume_role/credentials/factories/includes.rb +15 -0
  45. data/lib/aws_assume_role/credentials/factories/instance_profile.rb +19 -0
  46. data/lib/aws_assume_role/credentials/factories/repository.rb +37 -0
  47. data/lib/aws_assume_role/credentials/factories/shared.rb +19 -0
  48. data/lib/aws_assume_role/credentials/factories/static.rb +18 -0
  49. data/lib/aws_assume_role/credentials/factories.rb +11 -0
  50. data/lib/aws_assume_role/credentials/includes.rb +6 -0
  51. data/lib/aws_assume_role/credentials/providers/assume_role_credentials.rb +60 -0
  52. data/lib/aws_assume_role/credentials/providers/includes.rb +9 -0
  53. data/lib/aws_assume_role/credentials/providers/mfa_session_credentials.rb +119 -0
  54. data/lib/aws_assume_role/credentials/providers/shared_keyring_credentials.rb +41 -0
  55. data/lib/aws_assume_role/includes.rb +38 -0
  56. data/lib/aws_assume_role/logging.rb +27 -0
  57. data/lib/aws_assume_role/profile_configuration.rb +73 -0
  58. data/lib/aws_assume_role/runner.rb +40 -0
  59. data/lib/aws_assume_role/store/includes.rb +8 -0
  60. data/lib/aws_assume_role/store/keyring.rb +61 -0
  61. data/lib/aws_assume_role/store/serialization.rb +20 -0
  62. data/lib/aws_assume_role/store/shared_config_with_keyring.rb +250 -0
  63. data/lib/aws_assume_role/types.rb +31 -0
  64. data/lib/aws_assume_role/ui.rb +57 -0
  65. data/lib/aws_assume_role/vendored/aws/README.md +2 -0
  66. data/lib/aws_assume_role/vendored/aws/assume_role_credentials.rb +67 -0
  67. data/lib/aws_assume_role/vendored/aws/includes.rb +9 -0
  68. data/lib/aws_assume_role/vendored/aws/refreshing_credentials.rb +58 -0
  69. data/lib/aws_assume_role/vendored/aws/shared_config.rb +223 -0
  70. data/lib/aws_assume_role/vendored/aws.rb +4 -0
  71. data/lib/aws_assume_role/version.rb +5 -0
  72. data/lib/aws_assume_role.rb +4 -0
  73. metadata +438 -0
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "logging"
5
+
6
+ class AwsAssumeRole::Runner < Dry::Struct
7
+ include AwsAssumeRole::Logging
8
+ constructor_type :schema
9
+ attribute :command, Dry::Types["coercible.array"].of(Dry::Types["strict.string"]).default([])
10
+ attribute :exit_on_error, Dry::Types["strict.bool"].default(true)
11
+ attribute :expected_exit_code, Dry::Types["strict.int"].default(0)
12
+ attribute :environment, Dry::Types["strict.hash"].default({})
13
+ attribute :credentials, Dry::Types["object"].optional
14
+
15
+ def initialize(options)
16
+ super(options)
17
+ command_to_exec = command.map(&:shellescape).join(" ")
18
+ process_credentials unless credentials.blank?
19
+ system environment, command_to_exec
20
+ exit_status = $CHILD_STATUS.exitstatus
21
+ process_error(exit_status) if exit_status != expected_exit_code
22
+ end
23
+
24
+ private
25
+
26
+ def process_credentials
27
+ cred_env = {
28
+ "AWS_ACCESS_KEY_ID" => credentials.credentials.access_key_id,
29
+ "AWS_SECRET_ACCESS_KEY" => credentials.credentials.secret_access_key,
30
+ "AWS_SESSION_TOKEN" => credentials.credentials.session_token,
31
+ }
32
+ @environment = environment.merge cred_env
33
+ end
34
+
35
+ def process_error(exit_status)
36
+ logger.error "#{command} failed with #{exit_status}"
37
+ exit exit_status if exit_on_error
38
+ raise "#{command} failed with #{exit_status}"
39
+ end
40
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../includes"
4
+
5
+ module AwsAssumeRole
6
+ module Store
7
+ end
8
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "serialization"
5
+ require_relative "../configuration"
6
+ require_relative "../logging"
7
+
8
+ module AwsAssumeRole::Store::Keyring
9
+ include AwsAssumeRole
10
+ include AwsAssumeRole::Store
11
+ include AwsAssumeRole::Logging
12
+
13
+ module_function
14
+
15
+ KEYRING_KEY = "AwsAssumeRole".freeze
16
+
17
+ def semaphore
18
+ @semaphore ||= Mutex.new
19
+ end
20
+
21
+ def keyrings
22
+ @keyrings ||= {}
23
+ end
24
+
25
+ def try_backend_plugin
26
+ return if AwsAssumeRole::Config.backend_plugin.blank?
27
+ logger.info "Attempting to load #{AwsAssumeRole::Config.backend_plugin} plugin"
28
+ require AwsAssumeRole::Config.backend_plugin
29
+ end
30
+
31
+ def keyring(backend = AwsAssumeRole::Config.backend)
32
+ keyrings[backend] ||= begin
33
+ try_backend_plugin
34
+ klass = backend ? "Keyring::Backend::#{backend}".constantize : nil
35
+ logger.debug "Initializing #{klass} backend"
36
+ ::Keyring.new(klass)
37
+ end
38
+ end
39
+
40
+ def fetch(id, backend: nil)
41
+ logger.debug "Fetching #{id} from keyring"
42
+ fetched = keyring(backend).get_password(KEYRING_KEY, id)
43
+ raise Aws::Errors::NoSuchProfileError if fetched == "null" || fetched.nil? || !fetched
44
+ JSON.parse(fetched, symbolize_names: true)
45
+ end
46
+
47
+ def delete_credentials(id, backend: nil)
48
+ semaphore.synchronize do
49
+ keyring(backend).delete_password(KEYRING_KEY, id)
50
+ end
51
+ end
52
+
53
+ def save_credentials(id, credentials, expiration: nil, backend: nil)
54
+ credentials_to_persist = Serialization.credentials_to_hash(credentials)
55
+ credentials_to_persist[:expiration] = expiration if expiration
56
+ semaphore.synchronize do
57
+ keyring(backend).delete_password(KEYRING_KEY, id)
58
+ keyring(backend).set_password(KEYRING_KEY, id, credentials_to_persist.to_json)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsAssumeRole::Store::Serialization
4
+ module_function
5
+
6
+ def credentials_from_hash(credentials)
7
+ creds_for_deserialization = credentials.respond_to?("[]") ? credentials : credentials_to_hash(credentials)
8
+ Aws::Credentials.new(creds_for_deserialization[:access_key_id],
9
+ creds_for_deserialization[:secret_access_key],
10
+ creds_for_deserialization[:session_token])
11
+ end
12
+
13
+ def credentials_to_hash(credentials)
14
+ {
15
+ access_key_id: credentials.access_key_id,
16
+ secret_access_key: credentials.secret_access_key,
17
+ session_token: credentials.session_token,
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "../logging"
5
+ require_relative "keyring"
6
+ require_relative "../profile_configuration"
7
+ require_relative "../credentials/providers/mfa_session_credentials"
8
+
9
+ class AwsAssumeRole::Store::SharedConfigWithKeyring < AwsAssumeRole::Vendored::Aws::SharedConfig
10
+ include AwsAssumeRole::Store
11
+ include AwsAssumeRole::Logging
12
+
13
+ attr_reader :parsed_config
14
+
15
+ # @param [Hash] options
16
+ # @option options [String] :credentials_path Path to the shared credentials
17
+ # file. Defaults to "#{Dir.home}/.aws/credentials".
18
+ # @option options [String] :config_path Path to the shared config file.
19
+ # Defaults to "#{Dir.home}/.aws/config".
20
+ # @option options [String] :profile_name The credential/config profile name
21
+ # to use. If not specified, will check `ENV['AWS_PROFILE']` before using
22
+ # the fixed default value of 'default'.
23
+ # @option options [Boolean] :config_enabled If true, loads the shared config
24
+ # file and enables new config values outside of the old shared credential
25
+ # spec.
26
+ def initialize(options = {})
27
+ @profile_name = determine_profile(options)
28
+ @config_enabled = options[:config_enabled]
29
+ @credentials_path = options[:credentials_path] ||
30
+ determine_credentials_path
31
+ @parsed_credentials = {}
32
+ load_credentials_file if loadable?(@credentials_path)
33
+ return unless @config_enabled
34
+ @config_path = options[:config_path] || determine_config_path
35
+ load_config_file if loadable?(@config_path)
36
+ end
37
+
38
+ # @api private
39
+ def fresh(options = {})
40
+ @configuration = nil
41
+ @semaphore = nil
42
+ @assume_role_shared_config = nil
43
+ @profile_name = nil
44
+ @credentials_path = nil
45
+ @config_path = nil
46
+ @parsed_credentials = {}
47
+ @parsed_config = nil
48
+ @config_enabled = options[:config_enabled] ? true : false
49
+ @profile_name = determine_profile(options)
50
+ @credentials_path = options[:credentials_path] ||
51
+ determine_credentials_path
52
+ load_credentials_file if loadable?(@credentials_path)
53
+ return unless @config_enabled
54
+ @config_path = options[:config_path] || determine_config_path
55
+ load_config_file if loadable?(@config_path)
56
+ end
57
+
58
+ def credentials(opts = {})
59
+ logger.debug "SharedConfigWithKeyring asked for credentials with opts #{opts}"
60
+ p = opts[:profile] || @profile_name
61
+ validate_profile_exists(p) if credentials_present?
62
+ credentials_from_keyring(p, opts) || credentials_from_shared(p, opts) || credentials_from_config(p, opts)
63
+ end
64
+
65
+ def save_profile(profile_name, hash)
66
+ ckey = "profile #{profile_name}"
67
+ merged_config = configuration[ckey].deep_symbolize_keys.merge hash.to_h
68
+ merged_config[:mfa_serial] = merged_config[:serial_number] if merged_config[:serial_number]
69
+ credentials = Aws::Credentials.new(merged_config.delete(:aws_access_key_id),
70
+ merged_config.delete(:aws_secret_access_key))
71
+ semaphore.synchronize do
72
+ Keyring.save_credentials profile_name, credentials if credentials.set?
73
+ merged_config = merged_config.slice :region, :role_arn, :mfa_serial, :source_profile,
74
+ :role_session_name, :external_id, :duration_seconds,
75
+ :yubikey_oath_name
76
+ configuration.delete_section ckey
77
+ configuration[ckey] = merged_config.compact
78
+ save_configuration
79
+ end
80
+ end
81
+
82
+ def profiles
83
+ configuration.sections.map { |c| c.gsub("profile ", "") }
84
+ end
85
+
86
+ def delete_profile(profile_name)
87
+ # Keyring does not return errors for non-existent things, so always attempt.
88
+ Keyring.delete_credentials(profile_name)
89
+ semaphore.synchronize do
90
+ raise KeyError if configuration["profile #{profile_name}"].blank?
91
+ configuration.delete_section("profile #{profile_name}")
92
+ save_configuration
93
+ end
94
+ end
95
+
96
+ def migrate_profile(profile_name)
97
+ validate_profile_exists(profile_name)
98
+ save_profile(profile_name, configuration["profile #{profile_name}"])
99
+ end
100
+
101
+ def profile_region(profile_name)
102
+ resolve_profile_parameter(profile_name, "region")
103
+ end
104
+
105
+ def profile_role(profile_name)
106
+ resolve_profile_parameter(profile_name, "role_arn")
107
+ end
108
+
109
+ def profile_hash(profile_name)
110
+ {} || @parsed_config[profile_key(profile_name)]
111
+ end
112
+
113
+ private
114
+
115
+ def profile_key(profile)
116
+ logger.debug "About to lookup #{profile}"
117
+ if profile == "default" || profile.nil? || profile == ""
118
+ "default"
119
+ else
120
+ profile
121
+ end
122
+ end
123
+
124
+ def resolve_profile_parameter(profile_name, param)
125
+ return unless @parsed_config
126
+ prof_cfg = @parsed_config[profile_key(profile_name)]
127
+ resolve_parameter(param, @parsed_config, prof_cfg)
128
+ end
129
+
130
+ def resolve_parameter(param, cfg, prof_cfg)
131
+ return unless prof_cfg && cfg
132
+ return prof_cfg[param] if prof_cfg.key? param
133
+ source_profile = prof_cfg["source_profile"]
134
+ return unless source_profile
135
+ source_cfg = cfg[source_profile]
136
+ return unless source_cfg
137
+ cfg[prof_cfg["source_profile"]][param] if source_cfg.key?(param)
138
+ end
139
+
140
+ def resolve_region(cfg, prof_cfg)
141
+ resolve_parameter("region", cfg, prof_cfg)
142
+ end
143
+
144
+ def resolve_arn(cfg, prof_cfg)
145
+ resolve_parameter("role_arn", cfg, prof_cfg)
146
+ end
147
+
148
+ def assume_role_from_profile(cfg, profile, opts)
149
+ logger.debug "Entering assume_role_from_profile with #{cfg}, #{profile}, #{opts}"
150
+ prof_cfg = cfg[profile]
151
+ return unless cfg && prof_cfg
152
+ opts[:source_profile] ||= prof_cfg["source_profile"]
153
+ if opts[:source_profile]
154
+ opts[:credentials] = credentials(profile: opts[:source_profile])
155
+ if opts[:credentials]
156
+ opts[:role_session_name] ||= prof_cfg["role_session_name"]
157
+ opts[:role_session_name] ||= "default_session"
158
+ opts[:role_arn] ||= prof_cfg["role_arn"]
159
+ opts[:external_id] ||= prof_cfg["external_id"]
160
+ opts[:serial_number] ||= prof_cfg["mfa_serial"]
161
+ opts[:yubikey_oath_name] ||= prof_cfg["yubikey_oath_name"]
162
+ opts[:region] ||= profile_region(profile)
163
+ if opts[:serial_number]
164
+ mfa_opts = {
165
+ credentials: opts[:credentials],
166
+ region: opts[:region],
167
+ serial_number: opts[:serial_number],
168
+ yubikey_oath_name: opts[:yubikey_oath_name],
169
+ }
170
+ mfa_creds = mfa_session(cfg, opts[:source_profile], mfa_opts)
171
+ opts.delete :serial_number
172
+ end
173
+ opts[:credentials] = mfa_creds if mfa_creds
174
+ opts[:profile] = opts.delete(:source_profile)
175
+ AwsAssumeRole::Credentials::Providers::AssumeRoleCredentials.new(opts)
176
+ else
177
+ raise ::Aws::Errors::NoSourceProfileError, "Profile #{profile} has a role_arn, and source_profile, but the"\
178
+ " source_profile does not have credentials."
179
+ end
180
+ elsif prof_cfg["role_arn"]
181
+ raise ::Aws::Errors::NoSourceProfileError, "Profile #{profile} has a role_arn, but no source_profile."
182
+ end
183
+ end
184
+
185
+ def mfa_session(cfg, profile, opts)
186
+ prof_cfg = cfg[profile]
187
+ return unless cfg && prof_cfg
188
+ opts[:serial_number] ||= opts[:mfa_serial] || prof_cfg["mfa_serial"]
189
+ opts[:source_profile] ||= prof_cfg["source_profile"]
190
+ opts[:region] ||= profile_region(profile)
191
+ return unless opts[:serial_number]
192
+ opts[:credentials] ||= credentials(profile: opts[:profile])
193
+ AwsAssumeRole::Credentials::Providers::MfaSessionCredentials.new(opts)
194
+ end
195
+
196
+ def credentials_from_keyring(profile, opts)
197
+ logger.debug "Entering credentials_from_keyring"
198
+ return unless @parsed_config
199
+ logger.debug "credentials_from_keyring: @parsed_config found"
200
+ prof_cfg = @parsed_config[profile]
201
+ return unless prof_cfg
202
+ logger.debug "credentials_from_keyring: prof_cfg found"
203
+ opts[:serial_number] ||= opts[:mfa_serial] || prof_cfg[:mfa_serial] || prof_cfg[:serial_number]
204
+ if opts[:serial_number]
205
+ logger.debug "credentials_from_keyring detected mfa requirement"
206
+ mfa_session(@parsed_config, profile, opts)
207
+ else
208
+ logger.debug "Attempt to fetch #{profile} from keyring"
209
+ keyring_creds = Keyring.fetch(profile)
210
+ return unless keyring_creds
211
+ creds = Serialization.credentials_from_hash Keyring.fetch(profile)
212
+ creds if credentials_complete(creds)
213
+ end
214
+ rescue Aws::Errors::NoSourceProfileError, Aws::Errors::NoSuchProfileError
215
+ nil
216
+ end
217
+
218
+ def semaphore
219
+ @semaphore ||= Mutex.new
220
+ end
221
+
222
+ def configuration
223
+ @configuration ||= IniFile.new(filename: determine_config_path, default: "default")
224
+ end
225
+
226
+ # Please run in a mutex
227
+ def save_configuration
228
+ if File.exist? determine_config_path
229
+ bytes_required = File.size(determine_config_path)
230
+ # Overwrite the current .config file with random bytes to eliminate
231
+ # unencrypted credentials.
232
+ # This won't account for COW filesystems or SSD wear-levelling but
233
+ # is a best effort protection.
234
+ random_bytes = SecureRandom.random_bytes(bytes_required)
235
+ File.write(determine_config_path, random_bytes)
236
+ else
237
+ FileUtils.mkdir_p(Pathname.new(determine_config_path).dirname)
238
+ end
239
+ configuration.save
240
+ end
241
+ end
242
+
243
+ module AwsAssumeRole
244
+ module_function
245
+
246
+ def shared_config
247
+ enabled = ENV["AWS_SDK_CONFIG_OPT_OUT"] ? false : true
248
+ @assume_role_shared_config ||= ::AwsAssumeRole::Store::SharedConfigWithKeyring.new(config_enabled: enabled)
249
+ end
250
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ module AwsAssumeRole
5
+ module Types
6
+ Dry = Dry::Types.module
7
+
8
+ ::Dry::Types.register_class(::Aws::Credentials)
9
+ AwsAssumeRole::Types::Credentials = ::Dry::Types["aws.credentials"]
10
+
11
+ ACCESS_KEY_REGEX = /[\w]+/
12
+ ACCESS_KEY_VALIDATOR = proc { filled? & str? & format?(ACCESS_KEY_REGEX) & min_size?(16) & max_size?(32) }
13
+ ARN_REGEX = %r{arn:[\w+=\/,.@-]+:[\w+=\/,.@-]+:[\w+=\/,.@-]*:[0-9]+:[\w+=,.@-]+(\/[\w+=\/,.@-]+)*}
14
+ EXTERNAL_ID_REGEX = %r{[\w+=,.@:\/-]*}
15
+ MFA_REGEX = %r{arn:aws:iam::[0-9]+:mfa\/([\w+=,.@-]+)*|automatic}
16
+ REGION_REGEX = /^(us|eu|ap|sa|ca)\-\w+\-\d+$|^cn\-\w+\-\d+$|^us\-gov\-\w+\-\d+$/
17
+ REGION_VALIDATOR = proc { filled? & str? & format?(REGION_REGEX) }
18
+ ROLE_REGEX = %r{arn:aws:iam::[0-9]+:role\/([\w+=,.@-]+)*}
19
+ ROLE_SESSION_NAME_REGEX = /[\w+=,.@-]*/
20
+ SECRET_ACCESS_KEY_REGEX = //
21
+ SECRET_ACCESS_KEY_VALIDATOR = proc { filled? & str? & format?(SECRET_ACCESS_KEY_REGEX) }
22
+
23
+ AwsAssumeRole::Types::Region = Dry::Strict::String.constrained(
24
+ format: REGION_REGEX,
25
+ )
26
+
27
+ AwsAssumeRole::Types::MfaSerial = Dry::Strict::String.constrained(
28
+ format: MFA_REGEX,
29
+ )
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+
5
+ module AwsAssumeRole::Ui
6
+ include AwsAssumeRole
7
+
8
+ ::I18n.load_path += Dir.glob(File.join(File.realpath(__dir__), "..", "..", "i18n", "*.{rb,yml,yaml}"))
9
+ ::I18n.locale = ENV.fetch("LANG", nil).split(".").first.split("_").first || I18n.default_locale
10
+
11
+ module_function
12
+
13
+ def out(text)
14
+ puts text
15
+ end
16
+
17
+ def pastel
18
+ @pastel ||= Pastel.new
19
+ end
20
+
21
+ def input
22
+ @input ||= HighLine.new($stdin, $stderr)
23
+ end
24
+
25
+ def validation_errors_to_s(result)
26
+ text = result.errors.keys.map do |k|
27
+ result.errors[k].join(";")
28
+ end.join(" ")
29
+ text
30
+ end
31
+
32
+ def error(text)
33
+ puts pastel.red(text)
34
+ end
35
+
36
+ def show_validation_errors(result)
37
+ error validation_errors_to_s(result)
38
+ end
39
+
40
+ def ask_with_validation(variable_name, question, type: Dry::Types["coercible.string"], &block)
41
+ STDOUT.puts pastel.yellow question
42
+ validator = Dry::Validation.Schema do
43
+ configure do
44
+ config.messages = :i18n
45
+ end
46
+ required(variable_name) { instance_eval(&block) }
47
+ end
48
+ result = validator.call(variable_name => type[(STDIN.gets || "").chomp])
49
+ return result.to_h[variable_name] if result.success?
50
+ show_validation_errors result
51
+ ask_with_validation variable_name, question, &block
52
+ end
53
+
54
+ def t(*options)
55
+ ::I18n.t(options).first
56
+ end
57
+ end
@@ -0,0 +1,2 @@
1
+ These are copies of the private API from the AWS SDK v2, as partial protection to changes, for which the source is available at
2
+ https://github.com/aws/aws-sdk-ruby
@@ -0,0 +1,67 @@
1
+ require_relative "includes"
2
+
3
+ module AwsAssumeRole::Vendored::Aws
4
+ # An auto-refreshing credential provider that works by assuming
5
+ # a role via {Aws::STS::Client#assume_role}.
6
+ #
7
+ # role_credentials = Aws::AssumeRoleCredentials.new(
8
+ # client: Aws::STS::Client.new(...),
9
+ # role_arn: "linked::account::arn",
10
+ # role_session_name: "session-name"
11
+ # )
12
+ #
13
+ # ec2 = Aws::EC2::Client.new(credentials: role_credentials)
14
+ #
15
+ # If you omit `:client` option, a new {STS::Client} object will be
16
+ # constructed.
17
+ class AssumeRoleCredentials
18
+ include ::Aws::CredentialProvider
19
+ include ::Aws::RefreshingCredentials
20
+
21
+ # @option options [required, String] :role_arn
22
+ # @option options [required, String] :role_session_name
23
+ # @option options [String] :policy
24
+ # @option options [Integer] :duration_seconds
25
+ # @option options [String] :external_id
26
+ # @option options [STS::Client] :client
27
+ def initialize(options = {}, **)
28
+
29
+ client_opts = {}
30
+ @assume_role_params = {}
31
+ options.each_pair do |key, value|
32
+ if self.class.assume_role_options.include?(key)
33
+ @assume_role_params[key] = value
34
+ else
35
+ client_opts[key] = value
36
+ end
37
+ end
38
+ @client = client_opts[:client] || ::Aws::STS::Client.new(client_opts)
39
+ super
40
+ end
41
+
42
+ # @return [STS::Client]
43
+ attr_reader :client
44
+
45
+ private
46
+
47
+ def refresh
48
+ c = @client.assume_role(@assume_role_params).credentials
49
+ @credentials = ::Aws::Credentials.new(
50
+ c.access_key_id,
51
+ c.secret_access_key,
52
+ c.session_token,
53
+ )
54
+ @expiration = c.expiration
55
+ end
56
+
57
+ class << self
58
+ # @api private
59
+ def assume_role_options
60
+ @aro ||= begin
61
+ input = ::Aws::STS::Client.api.operation(:assume_role).input
62
+ Set.new(input.shape.member_names)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "../../includes"
2
+
3
+ module AwsAssumeRole
4
+ module Vendored
5
+ module Aws
6
+ include ::Aws
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,58 @@
1
+ module AwsAssumeRole::Vendored::Aws
2
+ # Base class used credential classes that can be refreshed. This
3
+ # provides basic refresh logic in a thread-safe manor. Classes mixing in
4
+ # this module are expected to implement a #refresh method that populates
5
+ # the following instance variables:
6
+ #
7
+ # * `@access_key_id`
8
+ # * `@secret_access_key`
9
+ # * `@session_token`
10
+ # * `@expiration`
11
+ #
12
+ # @api private
13
+ module RefreshingCredentials
14
+ def initialize(_options = {})
15
+ @mutex = Mutex.new
16
+ refresh
17
+ end
18
+
19
+ # @return [Credentials]
20
+ def credentials
21
+ refresh_if_near_expiration
22
+ @credentials
23
+ end
24
+
25
+ # @return [Time,nil]
26
+ def expiration
27
+ refresh_if_near_expiration
28
+ @expiration
29
+ end
30
+
31
+ # Refresh credentials.
32
+ # @return [void]
33
+ def refresh!
34
+ @mutex.synchronize { refresh }
35
+ end
36
+
37
+ private
38
+
39
+ # Refreshes instance metadata credentials if they are within
40
+ # 5 minutes of expiration.
41
+ def refresh_if_near_expiration
42
+ if near_expiration?
43
+ @mutex.synchronize do
44
+ refresh if near_expiration?
45
+ end
46
+ end
47
+ end
48
+
49
+ def near_expiration?
50
+ if @expiration
51
+ # are we within 5 minutes of expiration?
52
+ (Time.now.to_i + 5 * 60) > @expiration.to_i
53
+ else
54
+ true
55
+ end
56
+ end
57
+ end
58
+ end