aws_assume_role 0.0.3 → 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +31 -11
  4. data/Gemfile +7 -13
  5. data/LICENSE.md +201 -19
  6. data/README.md +176 -145
  7. data/aws_assume_role.gemspec +35 -21
  8. data/bin/aws-assume-role +1 -83
  9. data/i18n/en.yml +106 -0
  10. data/lib/aws_assume_role.rb +2 -3
  11. data/lib/aws_assume_role/cli.rb +15 -0
  12. data/lib/aws_assume_role/cli/actions/abstract_action.rb +53 -0
  13. data/lib/aws_assume_role/cli/actions/configure_profile.rb +21 -0
  14. data/lib/aws_assume_role/cli/actions/configure_role_assumption.rb +19 -0
  15. data/lib/aws_assume_role/cli/actions/console.rb +68 -0
  16. data/lib/aws_assume_role/cli/actions/delete_profile.rb +20 -0
  17. data/lib/aws_assume_role/cli/actions/includes.rb +18 -0
  18. data/lib/aws_assume_role/cli/actions/list_profiles.rb +10 -0
  19. data/lib/aws_assume_role/cli/actions/migrate_profile.rb +18 -0
  20. data/lib/aws_assume_role/cli/actions/reset_environment.rb +48 -0
  21. data/lib/aws_assume_role/cli/actions/run.rb +34 -0
  22. data/lib/aws_assume_role/cli/actions/set_environment.rb +60 -0
  23. data/lib/aws_assume_role/cli/actions/test.rb +31 -0
  24. data/lib/aws_assume_role/cli/commands/configure.rb +29 -0
  25. data/lib/aws_assume_role/cli/commands/console.rb +17 -0
  26. data/lib/aws_assume_role/cli/commands/delete.rb +11 -0
  27. data/lib/aws_assume_role/cli/commands/environment.rb +32 -0
  28. data/lib/aws_assume_role/cli/commands/list.rb +10 -0
  29. data/lib/aws_assume_role/cli/commands/migrate.rb +11 -0
  30. data/lib/aws_assume_role/cli/commands/run.rb +17 -0
  31. data/lib/aws_assume_role/cli/commands/test.rb +18 -0
  32. data/lib/aws_assume_role/configuration.rb +19 -0
  33. data/lib/aws_assume_role/core_ext/aws-sdk/credential_provider_chain.rb +2 -0
  34. data/lib/aws_assume_role/core_ext/aws-sdk/includes.rb +7 -0
  35. data/lib/aws_assume_role/credentials/factories.rb +9 -0
  36. data/lib/aws_assume_role/credentials/factories/abstract_factory.rb +31 -0
  37. data/lib/aws_assume_role/credentials/factories/assume_role.rb +38 -0
  38. data/lib/aws_assume_role/credentials/factories/default_chain_provider.rb +101 -0
  39. data/lib/aws_assume_role/credentials/factories/environment.rb +24 -0
  40. data/lib/aws_assume_role/credentials/factories/includes.rb +17 -0
  41. data/lib/aws_assume_role/credentials/factories/instance_profile.rb +17 -0
  42. data/lib/aws_assume_role/credentials/factories/repository.rb +35 -0
  43. data/lib/aws_assume_role/credentials/factories/shared.rb +15 -0
  44. data/lib/aws_assume_role/credentials/factories/shared_keyring.rb +16 -0
  45. data/lib/aws_assume_role/credentials/factories/static.rb +16 -0
  46. data/lib/aws_assume_role/credentials/providers/assume_role_credentials.rb +58 -0
  47. data/lib/aws_assume_role/credentials/providers/includes.rb +9 -0
  48. data/lib/aws_assume_role/credentials/providers/mfa_session_credentials.rb +102 -0
  49. data/lib/aws_assume_role/credentials/providers/shared_keyring_credentials.rb +22 -0
  50. data/lib/aws_assume_role/includes.rb +30 -0
  51. data/lib/aws_assume_role/logging.rb +16 -28
  52. data/lib/aws_assume_role/profile_configuration.rb +71 -0
  53. data/lib/aws_assume_role/runner.rb +39 -0
  54. data/lib/aws_assume_role/store/includes.rb +16 -0
  55. data/lib/aws_assume_role/store/keyring.rb +59 -0
  56. data/lib/aws_assume_role/store/serialization.rb +18 -0
  57. data/lib/aws_assume_role/store/shared_config_with_keyring.rb +175 -0
  58. data/lib/aws_assume_role/types.rb +30 -0
  59. data/lib/aws_assume_role/ui.rb +55 -0
  60. data/lib/aws_assume_role/vendored/aws.rb +4 -0
  61. data/lib/aws_assume_role/vendored/aws/README.md +2 -0
  62. data/lib/aws_assume_role/vendored/aws/assume_role_credentials.rb +68 -0
  63. data/lib/aws_assume_role/vendored/aws/includes.rb +9 -0
  64. data/lib/aws_assume_role/vendored/aws/refreshing_credentials.rb +60 -0
  65. data/lib/aws_assume_role/vendored/aws/shared_config.rb +220 -0
  66. data/lib/aws_assume_role/version.rb +3 -0
  67. metadata +264 -20
  68. data/.rspec +0 -2
  69. data/Rakefile +0 -2
  70. data/bin/test.rb +0 -39
  71. data/lib/aws_assume_role/credentials.rb +0 -92
  72. data/lib/aws_assume_role/profile.rb +0 -203
  73. data/lib/aws_assume_role/profile/assume_role.rb +0 -127
  74. data/lib/aws_assume_role/profile/basic.rb +0 -152
  75. data/lib/aws_assume_role/profile/list.rb +0 -57
@@ -0,0 +1,30 @@
1
+ require "dry-types"
2
+
3
+ module AwsAssumeRole
4
+ module Types
5
+ Dry = Dry::Types.module
6
+
7
+ ::Dry::Types.register_class(::Aws::Credentials)
8
+ AwsAssumeRole::Types::Credentials = ::Dry::Types["aws.credentials"]
9
+
10
+ ACCESS_KEY_REGEX = /[\w]+/
11
+ ACCESS_KEY_VALIDATOR = proc { filled? & str? & format?(ACCESS_KEY_REGEX) & min_size?(16) & max_size?(32) }
12
+ ARN_REGEX = %r{arn:[\w+=\/,.@-]+:[\w+=\/,.@-]+:[\w+=\/,.@-]*:[0-9]+:[\w+=,.@-]+(\/[\w+=\/,.@-]+)*}
13
+ EXTERNAL_ID_REGEX = %r{[\w+=,.@:\/-]*}
14
+ MFA_REGEX = %r{arn:aws:iam::[0-9]+:mfa\/([\w+=,.@-]+)*|automatic}
15
+ REGION_REGEX = /^(us|eu|ap|sa|ca)\-\w+\-\d+$|^cn\-\w+\-\d+$|^us\-gov\-\w+\-\d+$/
16
+ REGION_VALIDATOR = proc { filled? & str? & format?(REGION_REGEX) }
17
+ ROLE_REGEX = %r{arn:aws:iam::[0-9]+:role\/([\w+=,.@-]+)*}
18
+ ROLE_SESSION_NAME_REGEX = /[\w+=,.@-]*/
19
+ SECRET_ACCESS_KEY_REGEX = //
20
+ SECRET_ACCESS_KEY_VALIDATOR = proc { filled? & str? & format?(SECRET_ACCESS_KEY_REGEX) }
21
+
22
+ AwsAssumeRole::Types::Region = Dry::Strict::String.constrained(
23
+ format: REGION_REGEX,
24
+ )
25
+
26
+ AwsAssumeRole::Types::MfaSerial = Dry::Strict::String.constrained(
27
+ format: MFA_REGEX,
28
+ )
29
+ end
30
+ end
@@ -0,0 +1,55 @@
1
+ require_relative "includes"
2
+
3
+ module AwsAssumeRole::Ui
4
+ include AwsAssumeRole
5
+
6
+ ::I18n.load_path += Dir.glob(File.join(File.realpath(__dir__), "..", "..", "i18n", "*.{rb,yml,yaml}"))
7
+ ::I18n.locale = ENV.fetch("LANG", nil).split(".").first.split("_").first || I18n.default_locale
8
+
9
+ module_function
10
+
11
+ def out(text)
12
+ puts text
13
+ end
14
+
15
+ def pastel
16
+ @pastel ||= Pastel.new
17
+ end
18
+
19
+ def input
20
+ @input ||= HighLine.new
21
+ end
22
+
23
+ def validation_errors_to_s(result)
24
+ text = result.errors.keys.map do |k|
25
+ result.errors[k].join(";")
26
+ end.join(" ")
27
+ text
28
+ end
29
+
30
+ def error(text)
31
+ puts pastel.red(text)
32
+ end
33
+
34
+ def show_validation_errors(result)
35
+ error validation_errors_to_s(result)
36
+ end
37
+
38
+ def ask_with_validation(variable_name, question, type: Dry::Types["coercible.string"], &block)
39
+ STDOUT.puts pastel.yellow question
40
+ validator = Dry::Validation.Schema do
41
+ configure do
42
+ config.messages = :i18n
43
+ end
44
+ required(variable_name) { instance_eval(&block) }
45
+ end
46
+ result = validator.call(variable_name => type[STDIN.gets.chomp])
47
+ return result.to_h[variable_name] if result.success?
48
+ show_validation_errors result
49
+ ask_with_validation variable_name, question, &block
50
+ end
51
+
52
+ def t(*options)
53
+ ::I18n.t(options).first
54
+ end
55
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "aws/assume_role_credentials"
2
+ require_relative "aws/refreshing_credentials"
3
+ require_relative "../store/shared_config_with_keyring"
4
+ require_relative "aws/shared_config"
@@ -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,68 @@
1
+ require_relative "includes"
2
+ require "set"
3
+
4
+ module AwsAssumeRole::Vendored::Aws
5
+ # An auto-refreshing credential provider that works by assuming
6
+ # a role via {Aws::STS::Client#assume_role}.
7
+ #
8
+ # role_credentials = Aws::AssumeRoleCredentials.new(
9
+ # client: Aws::STS::Client.new(...),
10
+ # role_arn: "linked::account::arn",
11
+ # role_session_name: "session-name"
12
+ # )
13
+ #
14
+ # ec2 = Aws::EC2::Client.new(credentials: role_credentials)
15
+ #
16
+ # If you omit `:client` option, a new {STS::Client} object will be
17
+ # constructed.
18
+ class AssumeRoleCredentials
19
+ include ::Aws::CredentialProvider
20
+ include ::Aws::RefreshingCredentials
21
+
22
+ # @option options [required, String] :role_arn
23
+ # @option options [required, String] :role_session_name
24
+ # @option options [String] :policy
25
+ # @option options [Integer] :duration_seconds
26
+ # @option options [String] :external_id
27
+ # @option options [STS::Client] :client
28
+ def initialize(options = {}, **)
29
+
30
+ client_opts = {}
31
+ @assume_role_params = {}
32
+ options.each_pair do |key, value|
33
+ if self.class.assume_role_options.include?(key)
34
+ @assume_role_params[key] = value
35
+ else
36
+ client_opts[key] = value
37
+ end
38
+ end
39
+ @client = client_opts[:client] || ::Aws::STS::Client.new(client_opts)
40
+ super
41
+ end
42
+
43
+ # @return [STS::Client]
44
+ attr_reader :client
45
+
46
+ private
47
+
48
+ def refresh
49
+ c = @client.assume_role(@assume_role_params).credentials
50
+ @credentials = ::Aws::Credentials.new(
51
+ c.access_key_id,
52
+ c.secret_access_key,
53
+ c.session_token,
54
+ )
55
+ @expiration = c.expiration
56
+ end
57
+
58
+ class << self
59
+ # @api private
60
+ def assume_role_options
61
+ @aro ||= begin
62
+ input = ::Aws::STS::Client.api.operation(:assume_role).input
63
+ Set.new(input.shape.member_names)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,9 @@
1
+ require "aws-sdk"
2
+
3
+ module AwsAssumeRole
4
+ module Vendored
5
+ module Aws
6
+ include ::Aws
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ require "thread"
2
+
3
+ module AwsAssumeRole::Vendored::Aws
4
+ # Base class used credential classes that can be refreshed. This
5
+ # provides basic refresh logic in a thread-safe manor. Classes mixing in
6
+ # this module are expected to implement a #refresh method that populates
7
+ # the following instance variables:
8
+ #
9
+ # * `@access_key_id`
10
+ # * `@secret_access_key`
11
+ # * `@session_token`
12
+ # * `@expiration`
13
+ #
14
+ # @api private
15
+ module RefreshingCredentials
16
+ def initialize(_options = {})
17
+ @mutex = Mutex.new
18
+ refresh
19
+ end
20
+
21
+ # @return [Credentials]
22
+ def credentials
23
+ refresh_if_near_expiration
24
+ @credentials
25
+ end
26
+
27
+ # @return [Time,nil]
28
+ def expiration
29
+ refresh_if_near_expiration
30
+ @expiration
31
+ end
32
+
33
+ # Refresh credentials.
34
+ # @return [void]
35
+ def refresh!
36
+ @mutex.synchronize { refresh }
37
+ end
38
+
39
+ private
40
+
41
+ # Refreshes instance metadata credentials if they are within
42
+ # 5 minutes of expiration.
43
+ def refresh_if_near_expiration
44
+ if near_expiration?
45
+ @mutex.synchronize do
46
+ refresh if near_expiration?
47
+ end
48
+ end
49
+ end
50
+
51
+ def near_expiration?
52
+ if @expiration
53
+ # are we within 5 minutes of expiration?
54
+ (Time.now.to_i + 5 * 60) > @expiration.to_i
55
+ else
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,220 @@
1
+ require_relative "includes"
2
+ module AwsAssumeRole::Vendored::Aws
3
+ # @api private
4
+ class SharedConfig
5
+ # @return [String]
6
+ attr_reader :credentials_path
7
+
8
+ # @return [String]
9
+ attr_reader :config_path
10
+
11
+ # @return [String]
12
+ attr_reader :profile_name
13
+
14
+ # Constructs a new SharedConfig provider object. This will load the shared
15
+ # credentials file, and optionally the shared configuration file, as ini
16
+ # files which support profiles.
17
+ #
18
+ # By default, the shared credential file (the default path for which is
19
+ # `~/.aws/credentials`) and the shared config file (the default path for
20
+ # which is `~/.aws/config`) are loaded. However, if you set the
21
+ # `ENV['AWS_SDK_CONFIG_OPT_OUT']` environment variable, only the shared
22
+ # credential file will be loaded.
23
+ #
24
+ # The default profile name is 'default'. You can specify the profile name
25
+ # with the `ENV['AWS_PROFILE']` environment variable or with the
26
+ # `:profile_name` option.
27
+ #
28
+ # @param [Hash] options
29
+ # @option options [String] :credentials_path Path to the shared credentials
30
+ # file. Defaults to "#{Dir.home}/.aws/credentials".
31
+ # @option options [String] :config_path Path to the shared config file.
32
+ # Defaults to "#{Dir.home}/.aws/config".
33
+ # @option options [String] :profile_name The credential/config profile name
34
+ # to use. If not specified, will check `ENV['AWS_PROFILE']` before using
35
+ # the fixed default value of 'default'.
36
+ # @option options [Boolean] :config_enabled If true, loads the shared config
37
+ # file and enables new config values outside of the old shared credential
38
+ # spec.
39
+ def initialize(options = {})
40
+ @profile_name = determine_profile(options)
41
+ @config_enabled = options[:config_enabled]
42
+ @credentials_path = options[:credentials_path] ||
43
+ determine_credentials_path
44
+ @parsed_credentials = {}
45
+ load_credentials_file if loadable?(@credentials_path)
46
+ if @config_enabled
47
+ @config_path = options[:config_path] || determine_config_path
48
+ load_config_file if loadable?(@config_path)
49
+ end
50
+ end
51
+
52
+ # @api private
53
+ def fresh(options = {})
54
+ @profile_name = nil
55
+ @credentials_path = nil
56
+ @config_path = nil
57
+ @parsed_credentials = {}
58
+ @parsed_config = nil
59
+ @config_enabled = options[:config_enabled] ? true : false
60
+ @profile_name = determine_profile(options)
61
+ @credentials_path = options[:credentials_path] ||
62
+ determine_credentials_path
63
+ load_credentials_file if loadable?(@credentials_path)
64
+ if @config_enabled
65
+ @config_path = options[:config_path] || determine_config_path
66
+ load_config_file if loadable?(@config_path)
67
+ end
68
+ end
69
+
70
+ # @return [Boolean] Returns `true` if a credential file
71
+ # exists and has appropriate read permissions at {#path}.
72
+ # @note This method does not indicate if the file found at {#path}
73
+ # will be parsable, only if it can be read.
74
+ def loadable?(path)
75
+ !path.nil? && File.exist?(path) && File.readable?(path)
76
+ end
77
+
78
+ # @return [Boolean] returns `true` if use of the shared config file is
79
+ # enabled.
80
+ def config_enabled?
81
+ @config_enabled ? true : false
82
+ end
83
+
84
+ # Sources static credentials from shared credential/config files.
85
+ #
86
+ # @param [Hash] options
87
+ # @option options [String] :profile the name of the configuration file from
88
+ # which credentials are being sourced.
89
+ # @return [Aws::Credentials] credentials sourced from configuration values,
90
+ # or `nil` if no valid credentials were found.
91
+ def credentials(opts = {})
92
+ p = opts[:profile] || @profile_name
93
+ validate_profile_exists(p) if credentials_present?
94
+ if credentials = credentials_from_shared(p, opts)
95
+ credentials
96
+ elsif credentials = credentials_from_config(p, opts)
97
+ credentials
98
+ end
99
+ end
100
+
101
+ # Attempts to assume a role from shared config or shared credentials file.
102
+ # Will always attempt first to assume a role from the shared credentials
103
+ # file, if present.
104
+ def assume_role_credentials_from_config(opts = {})
105
+ p = opts.delete(:profile) || @profile_name
106
+ credentials = assume_role_from_profile(@parsed_credentials, p, opts)
107
+ if @parsed_config
108
+ credentials ||= assume_role_from_profile(@parsed_config, p, opts)
109
+ end
110
+ credentials
111
+ end
112
+
113
+ def region(opts = {})
114
+ p = opts[:profile] || @profile_name
115
+ if @config_enabled
116
+ if @parsed_credentials
117
+ region = @parsed_credentials.fetch(p, {})["region"]
118
+ end
119
+ region ||= @parsed_config.fetch(p, {})["region"] if @parsed_config
120
+ region
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def credentials_present?
127
+ (@parsed_credentials && !@parsed_credentials.empty?) ||
128
+ (@parsed_config && !@parsed_config.empty?)
129
+ end
130
+
131
+ def assume_role_from_profile(cfg, profile, opts)
132
+ if cfg && prof_cfg = cfg[profile]
133
+ opts[:source_profile] ||= prof_cfg["source_profile"]
134
+ if opts[:source_profile]
135
+ opts[:credentials] = credentials(profile: opts[:source_profile])
136
+ if opts[:credentials]
137
+ opts[:role_session_name] ||= prof_cfg["role_session_name"]
138
+ opts[:role_session_name] ||= "default_session"
139
+ opts[:role_arn] ||= prof_cfg["role_arn"]
140
+ opts[:external_id] ||= prof_cfg["external_id"]
141
+ opts[:serial_number] ||= prof_cfg["mfa_serial"]
142
+ opts[:profile] = opts.delete(:source_profile)
143
+ AwsAssumeRole::Vendored::Aws::AssumeRoleCredentials.new(opts)
144
+ else
145
+ raise ::Aws::Errors::NoSourceProfileError, "Profile #{profile} has a role_arn, and source_profile, but the"\
146
+ " source_profile does not have credentials."
147
+ end
148
+ elsif prof_cfg["role_arn"]
149
+ raise ::Aws::Errors::NoSourceProfileError, "Profile #{profile} has a role_arn, but no source_profile."
150
+ end
151
+ end
152
+ end
153
+
154
+ def credentials_from_shared(profile, _opts)
155
+ if @parsed_credentials && prof_config = @parsed_credentials[profile]
156
+ credentials_from_profile(prof_config)
157
+ end
158
+ end
159
+
160
+ def credentials_from_config(profile, _opts)
161
+ if @parsed_config && prof_config = @parsed_config[profile]
162
+ credentials_from_profile(prof_config)
163
+ end
164
+ end
165
+
166
+ def credentials_from_profile(prof_config)
167
+ creds = Aws::Credentials.new(
168
+ prof_config["aws_access_key_id"],
169
+ prof_config["aws_secret_access_key"],
170
+ prof_config["aws_session_token"],
171
+ )
172
+ creds if credentials_complete(creds)
173
+ end
174
+
175
+ def credentials_complete(creds)
176
+ creds.set?
177
+ end
178
+
179
+ def load_credentials_file
180
+ @parsed_credentials = Aws::IniParser.ini_parse(
181
+ File.read(@credentials_path),
182
+ )
183
+ end
184
+
185
+ def load_config_file
186
+ @parsed_config = Aws::IniParser.ini_parse(File.read(@config_path))
187
+ end
188
+
189
+ def determine_credentials_path
190
+ default = default_shared_config_path("credentials")
191
+ end
192
+
193
+ def determine_config_path
194
+ default = default_shared_config_path("config")
195
+ end
196
+
197
+ def default_shared_config_path(file)
198
+ File.join(Dir.home, ".aws", file)
199
+ rescue ArgumentError
200
+ # Dir.home raises ArgumentError when ENV['home'] is not set
201
+ nil
202
+ end
203
+
204
+ def validate_profile_exists(profile)
205
+ unless (@parsed_credentials && @parsed_credentials[profile]) ||
206
+ (@parsed_config && @parsed_config[profile])
207
+ msg = "Profile `#{profile}' not found in #{@credentials_path}"
208
+ msg << " or #{@config_path}" if @config_path
209
+ raise ::Aws::Errors::NoSuchProfileError, msg
210
+ end
211
+ end
212
+
213
+ def determine_profile(options)
214
+ ret = options[:profile_name]
215
+ ret ||= ENV["AWS_PROFILE"]
216
+ ret ||= "default"
217
+ ret
218
+ end
219
+ end
220
+ end