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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +57 -0
- data/.ruby-version +1 -0
- data/.simplecov +22 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +57 -0
- data/Gemfile +18 -0
- data/LICENSE.md +201 -0
- data/README.md +303 -0
- data/Rakefile +47 -0
- data/aws_assume_role.gemspec +56 -0
- data/bin/aws-assume-role +4 -0
- data/i18n/en.yml +109 -0
- data/lib/aws_assume_role/cli/actions/abstract_action.rb +61 -0
- data/lib/aws_assume_role/cli/actions/configure_profile.rb +24 -0
- data/lib/aws_assume_role/cli/actions/configure_role_assumption.rb +22 -0
- data/lib/aws_assume_role/cli/actions/console.rb +70 -0
- data/lib/aws_assume_role/cli/actions/delete_profile.rb +22 -0
- data/lib/aws_assume_role/cli/actions/includes.rb +12 -0
- data/lib/aws_assume_role/cli/actions/list_profiles.rb +12 -0
- data/lib/aws_assume_role/cli/actions/migrate_profile.rb +20 -0
- data/lib/aws_assume_role/cli/actions/reset_environment.rb +50 -0
- data/lib/aws_assume_role/cli/actions/run.rb +36 -0
- data/lib/aws_assume_role/cli/actions/set_environment.rb +62 -0
- data/lib/aws_assume_role/cli/actions/test.rb +35 -0
- data/lib/aws_assume_role/cli/commands/configure.rb +32 -0
- data/lib/aws_assume_role/cli/commands/console.rb +19 -0
- data/lib/aws_assume_role/cli/commands/delete.rb +13 -0
- data/lib/aws_assume_role/cli/commands/environment.rb +34 -0
- data/lib/aws_assume_role/cli/commands/list.rb +12 -0
- data/lib/aws_assume_role/cli/commands/migrate.rb +13 -0
- data/lib/aws_assume_role/cli/commands/run.rb +19 -0
- data/lib/aws_assume_role/cli/commands/test.rb +20 -0
- data/lib/aws_assume_role/cli/includes.rb +3 -0
- data/lib/aws_assume_role/cli.rb +20 -0
- data/lib/aws_assume_role/configuration.rb +30 -0
- data/lib/aws_assume_role/core_ext/aws-sdk/credential_provider_chain.rb +4 -0
- data/lib/aws_assume_role/core_ext/aws-sdk/includes.rb +9 -0
- data/lib/aws_assume_role/credentials/factories/abstract_factory.rb +33 -0
- data/lib/aws_assume_role/credentials/factories/assume_role.rb +39 -0
- data/lib/aws_assume_role/credentials/factories/default_chain_provider.rb +113 -0
- data/lib/aws_assume_role/credentials/factories/environment.rb +26 -0
- data/lib/aws_assume_role/credentials/factories/includes.rb +15 -0
- data/lib/aws_assume_role/credentials/factories/instance_profile.rb +19 -0
- data/lib/aws_assume_role/credentials/factories/repository.rb +37 -0
- data/lib/aws_assume_role/credentials/factories/shared.rb +19 -0
- data/lib/aws_assume_role/credentials/factories/static.rb +18 -0
- data/lib/aws_assume_role/credentials/factories.rb +11 -0
- data/lib/aws_assume_role/credentials/includes.rb +6 -0
- data/lib/aws_assume_role/credentials/providers/assume_role_credentials.rb +60 -0
- data/lib/aws_assume_role/credentials/providers/includes.rb +9 -0
- data/lib/aws_assume_role/credentials/providers/mfa_session_credentials.rb +119 -0
- data/lib/aws_assume_role/credentials/providers/shared_keyring_credentials.rb +41 -0
- data/lib/aws_assume_role/includes.rb +38 -0
- data/lib/aws_assume_role/logging.rb +27 -0
- data/lib/aws_assume_role/profile_configuration.rb +73 -0
- data/lib/aws_assume_role/runner.rb +40 -0
- data/lib/aws_assume_role/store/includes.rb +8 -0
- data/lib/aws_assume_role/store/keyring.rb +61 -0
- data/lib/aws_assume_role/store/serialization.rb +20 -0
- data/lib/aws_assume_role/store/shared_config_with_keyring.rb +250 -0
- data/lib/aws_assume_role/types.rb +31 -0
- data/lib/aws_assume_role/ui.rb +57 -0
- data/lib/aws_assume_role/vendored/aws/README.md +2 -0
- data/lib/aws_assume_role/vendored/aws/assume_role_credentials.rb +67 -0
- data/lib/aws_assume_role/vendored/aws/includes.rb +9 -0
- data/lib/aws_assume_role/vendored/aws/refreshing_credentials.rb +58 -0
- data/lib/aws_assume_role/vendored/aws/shared_config.rb +223 -0
- data/lib/aws_assume_role/vendored/aws.rb +4 -0
- data/lib/aws_assume_role/version.rb +5 -0
- data/lib/aws_assume_role.rb +4 -0
- 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,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,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,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
|