aws_assume_role 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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