aws_assume_role 1.1.0-universal-darwin

Sign up to get free protection for your applications and to get access to all the features.
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 +24 -0
  7. data/CHANGELOG.md +61 -0
  8. data/Gemfile +18 -0
  9. data/LICENSE.md +201 -0
  10. data/README.md +303 -0
  11. data/Rakefile +63 -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.rb +4 -0
  16. data/lib/aws_assume_role/cli.rb +20 -0
  17. data/lib/aws_assume_role/cli/actions/abstract_action.rb +61 -0
  18. data/lib/aws_assume_role/cli/actions/configure_profile.rb +24 -0
  19. data/lib/aws_assume_role/cli/actions/configure_role_assumption.rb +22 -0
  20. data/lib/aws_assume_role/cli/actions/console.rb +70 -0
  21. data/lib/aws_assume_role/cli/actions/delete_profile.rb +22 -0
  22. data/lib/aws_assume_role/cli/actions/includes.rb +12 -0
  23. data/lib/aws_assume_role/cli/actions/list_profiles.rb +12 -0
  24. data/lib/aws_assume_role/cli/actions/migrate_profile.rb +20 -0
  25. data/lib/aws_assume_role/cli/actions/reset_environment.rb +50 -0
  26. data/lib/aws_assume_role/cli/actions/run.rb +36 -0
  27. data/lib/aws_assume_role/cli/actions/set_environment.rb +62 -0
  28. data/lib/aws_assume_role/cli/actions/test.rb +35 -0
  29. data/lib/aws_assume_role/cli/commands/configure.rb +32 -0
  30. data/lib/aws_assume_role/cli/commands/console.rb +19 -0
  31. data/lib/aws_assume_role/cli/commands/delete.rb +13 -0
  32. data/lib/aws_assume_role/cli/commands/environment.rb +34 -0
  33. data/lib/aws_assume_role/cli/commands/list.rb +12 -0
  34. data/lib/aws_assume_role/cli/commands/migrate.rb +13 -0
  35. data/lib/aws_assume_role/cli/commands/run.rb +19 -0
  36. data/lib/aws_assume_role/cli/commands/test.rb +20 -0
  37. data/lib/aws_assume_role/cli/includes.rb +3 -0
  38. data/lib/aws_assume_role/configuration.rb +30 -0
  39. data/lib/aws_assume_role/core_ext/aws-sdk/credential_provider_chain.rb +4 -0
  40. data/lib/aws_assume_role/core_ext/aws-sdk/includes.rb +9 -0
  41. data/lib/aws_assume_role/credentials/factories.rb +11 -0
  42. data/lib/aws_assume_role/credentials/factories/abstract_factory.rb +33 -0
  43. data/lib/aws_assume_role/credentials/factories/assume_role.rb +39 -0
  44. data/lib/aws_assume_role/credentials/factories/default_chain_provider.rb +113 -0
  45. data/lib/aws_assume_role/credentials/factories/environment.rb +26 -0
  46. data/lib/aws_assume_role/credentials/factories/includes.rb +15 -0
  47. data/lib/aws_assume_role/credentials/factories/instance_profile.rb +19 -0
  48. data/lib/aws_assume_role/credentials/factories/repository.rb +37 -0
  49. data/lib/aws_assume_role/credentials/factories/shared.rb +19 -0
  50. data/lib/aws_assume_role/credentials/factories/static.rb +18 -0
  51. data/lib/aws_assume_role/credentials/includes.rb +6 -0
  52. data/lib/aws_assume_role/credentials/providers/assume_role_credentials.rb +60 -0
  53. data/lib/aws_assume_role/credentials/providers/includes.rb +9 -0
  54. data/lib/aws_assume_role/credentials/providers/mfa_session_credentials.rb +119 -0
  55. data/lib/aws_assume_role/credentials/providers/shared_keyring_credentials.rb +41 -0
  56. data/lib/aws_assume_role/includes.rb +38 -0
  57. data/lib/aws_assume_role/logging.rb +27 -0
  58. data/lib/aws_assume_role/profile_configuration.rb +73 -0
  59. data/lib/aws_assume_role/runner.rb +40 -0
  60. data/lib/aws_assume_role/store/includes.rb +8 -0
  61. data/lib/aws_assume_role/store/keyring.rb +61 -0
  62. data/lib/aws_assume_role/store/serialization.rb +20 -0
  63. data/lib/aws_assume_role/store/shared_config_with_keyring.rb +250 -0
  64. data/lib/aws_assume_role/types.rb +31 -0
  65. data/lib/aws_assume_role/ui.rb +57 -0
  66. data/lib/aws_assume_role/vendored/aws.rb +4 -0
  67. data/lib/aws_assume_role/vendored/aws/README.md +2 -0
  68. data/lib/aws_assume_role/vendored/aws/assume_role_credentials.rb +67 -0
  69. data/lib/aws_assume_role/vendored/aws/includes.rb +9 -0
  70. data/lib/aws_assume_role/vendored/aws/refreshing_credentials.rb +58 -0
  71. data/lib/aws_assume_role/vendored/aws/shared_config.rb +223 -0
  72. data/lib/aws_assume_role/version.rb +5 -0
  73. metadata +438 -0
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../includes"
4
+ require_relative "../../vendored/aws"
5
+ require_relative "../../ui"
6
+ require_relative "../../logging"
7
+ module AwsAssumeRole::Credentials
8
+ module Providers end
9
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "../../types"
5
+ require_relative "../../configuration"
6
+ begin
7
+ require "smartcard"
8
+ require "yubioath"
9
+ SMARTCARD_SUPPORT = true
10
+ rescue LoadError
11
+ SMARTCARD_SUPPORT = false
12
+ end
13
+
14
+ class AwsAssumeRole::Credentials::Providers::MfaSessionCredentials < Dry::Struct
15
+ constructor_type :schema
16
+ include AwsAssumeRole::Vendored::Aws::CredentialProvider
17
+ include AwsAssumeRole::Vendored::Aws::RefreshingCredentials
18
+ include AwsAssumeRole::Ui
19
+ include AwsAssumeRole::Logging
20
+
21
+ attribute :permanent_credentials, Dry::Types["object"].optional
22
+ attribute :credentials, Dry::Types["object"].optional
23
+ attribute :expiration, Dry::Types["strict.time"].default(Time.now)
24
+ attribute :first_time, Dry::Types["strict.bool"].default(true)
25
+ attribute :persist_session, Dry::Types["strict.bool"].default(true)
26
+ attribute :duration_seconds, Dry::Types["coercible.int"].default(3600)
27
+ attribute :region, AwsAssumeRole::Types::Region.optional
28
+ attribute :serial_number, AwsAssumeRole::Types::MfaSerial.optional.default("automatic")
29
+ attribute :yubikey_oath_name, Dry::Types["strict.string"].optional
30
+
31
+ def initialize(options)
32
+ options.each { |key, value| instance_variable_set("@#{key}", value) }
33
+ @permanent_credentials ||= @credentials
34
+ @credentials = nil
35
+ @serial_number = resolve_serial_number(serial_number)
36
+ AwsAssumeRole::Vendored::Aws::RefreshingCredentials.instance_method(:initialize).bind(self).call(options)
37
+ end
38
+
39
+ private
40
+
41
+ def keyring_username
42
+ @keyring_username ||= "#{@identity.to_json}|#{@serial_number}"
43
+ end
44
+
45
+ def sts_client
46
+ @sts_client ||= Aws::STS::Client.new(region: @region, credentials: @permanent_credentials)
47
+ end
48
+
49
+ def prompt_for_token
50
+ text = @first_time ? t("options.mfa_token.first_time") : t("options.mfa_token.other_times")
51
+ Ui.input.ask text
52
+ end
53
+
54
+ def initialized
55
+ @first_time = false
56
+ end
57
+
58
+ def refresh
59
+ return set_credentials_from_keyring if @persist_session && @first_time
60
+ refresh_using_mfa if near_expiration?
61
+ broadcast(:mfa_completed)
62
+ end
63
+
64
+ def retrieve_yubikey_token
65
+ raise t("options.mfa_token.smartcard_not_supported") unless SMARTCARD_SUPPORT
66
+ context = Smartcard::PCSC::Context.new
67
+ raise "Yubikey not found" unless context.readers.length == 1
68
+ reader_name = context.readers.first
69
+ card = Smartcard::PCSC::Card.new(context, reader_name, :shared)
70
+ codes = YubiOATH.new(card).calculate_all(timestamp: Time.now)
71
+ codes.fetch(BinData::String.new(@yubikey_oath_name))
72
+ end
73
+
74
+ def refresh_using_mfa
75
+ token_code = @yubikey_oath_name ? retrieve_yubikey_token : prompt_for_token
76
+ token = sts_client.get_session_token(
77
+ duration_seconds: @duration_seconds,
78
+ serial_number: @serial_number,
79
+ token_code: token_code,
80
+ )
81
+ initialized
82
+ instance_credentials token.credentials
83
+ persist_credentials if @persist_session
84
+ end
85
+
86
+ def credentials_from_keyring
87
+ @credentials_from_keyring ||= AwsAssumeRole::Store::Keyring.fetch keyring_username
88
+ rescue Aws::Errors::NoSuchProfileError
89
+ logger.debug "Key not found"
90
+ @credentials_from_keyring = nil
91
+ return nil
92
+ end
93
+
94
+ def persist_credentials
95
+ AwsAssumeRole::Store::Keyring.save_credentials keyring_username, @credentials, expiration: @expiration
96
+ end
97
+
98
+ def instance_credentials(credentials)
99
+ return unless credentials
100
+ @credentials = AwsAssumeRole::Store::Serialization.credentials_from_hash(credentials)
101
+ @expiration = credentials.respond_to?(:expiration) ? credentials.expiration : Time.parse(credentials[:expiration])
102
+ end
103
+
104
+ def set_credentials_from_keyring
105
+ instance_credentials credentials_from_keyring if credentials_from_keyring
106
+ initialized
107
+ refresh_using_mfa unless @credentials && !near_expiration?
108
+ end
109
+
110
+ def identity
111
+ @identity ||= sts_client.get_caller_identity
112
+ end
113
+
114
+ def resolve_serial_number(serial_number)
115
+ return serial_number unless serial_number.nil? || serial_number == "automatic"
116
+ user_name = identity.arn.split("/")[1]
117
+ "arn:aws:iam::#{identity.account}:mfa/#{user_name}"
118
+ end
119
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "../../types"
5
+
6
+ class AwsAssumeRole::Credentials::Providers::SharedKeyringCredentials < ::Aws::SharedCredentials
7
+ include AwsAssumeRole::Logging
8
+ attr_reader :region, :role_arn
9
+
10
+ def initialize(options = {})
11
+ logger.debug "SharedKeyringCredentials initiated with #{options}"
12
+ @path = options[:path]
13
+ @path ||= AwsAssumeRole.shared_config.credentials_path
14
+ @profile_name = options[:profile_name] ||= options[:profile]
15
+ @profile_name ||= ENV["AWS_PROFILE"]
16
+ @profile_name ||= AwsAssumeRole.shared_config.profile_name
17
+ logger.debug "SharedKeyringCredentials resolved profile name #{@profile_name}"
18
+ config = determine_config(@path, @profile_name)
19
+ @role_arn = config.profile_hash(@profile_name)
20
+ @region = config.profile_region(@profile_name)
21
+ @role_arn = config.profile_role(@profile_name)
22
+ attempted_credential = config.credentials(options)
23
+ return unless attempted_credential && attempted_credential.set?
24
+ @credentials = attempted_credential
25
+ end
26
+
27
+ private
28
+
29
+ def determine_config(path, profile_name)
30
+ if path && path == AwsAssumeRole.shared_config.credentials_path
31
+ logger.debug "SharedKeyringCredentials found shared credential path"
32
+ AwsAssumeRole.shared_config
33
+ else
34
+ logger.debug "SharedKeyringCredentials found custom credential path"
35
+ AwsAssumeRole::Store::SharedConfigWithKeyring.new(
36
+ credentials_path: path,
37
+ profile_name: profile_name,
38
+ )
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "i18n"
4
+ require "active_support/json"
5
+ require "active_support/core_ext/object/blank"
6
+ require "active_support/core_ext/string/inflections"
7
+ require "active_support/core_ext/hash/compact"
8
+ require "active_support/core_ext/hash/keys"
9
+ require "active_support/core_ext/hash/slice"
10
+ require "aws-sdk"
11
+ require "aws-sdk-core/ini_parser"
12
+ require "dry-configurable"
13
+ require "dry-struct"
14
+ require "dry-validation"
15
+ require "dry-types"
16
+ require "English"
17
+ require "gli"
18
+ require "highline"
19
+ require "inifile"
20
+ require "json"
21
+ require "keyring"
22
+ require "launchy"
23
+ require "logger"
24
+ require "open-uri"
25
+ require "pastel"
26
+ require "securerandom"
27
+ require "set"
28
+ require "thread"
29
+ require "time"
30
+
31
+ module AwsAssumeRole
32
+ module_function
33
+
34
+ def shared_config
35
+ enabled = ENV["AWS_SDK_CONFIG_OPT_OUT"] ? false : true
36
+ @shared_config ||= SharedConfigWithKeyring.new(config_enabled: enabled)
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "configuration"
5
+ module AwsAssumeRole::Logging
6
+ module ClassMethods
7
+ def logger
8
+ @logger ||= begin
9
+ logger = Logger.new($stderr)
10
+ logger.level = AwsAssumeRole::Config.log_level
11
+ ENV["GLI_DEBUG"] = "true" if AwsAssumeRole::Config.log_level.zero?
12
+ logger
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def logger
19
+ self.class.logger
20
+ end
21
+ end
22
+
23
+ def self.included(base)
24
+ base.extend ClassMethods
25
+ base.include InstanceMethods
26
+ end
27
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "includes"
4
+ require_relative "logging"
5
+
6
+ class AwsAssumeRole::ProfileConfiguration < Dry::Struct
7
+ constructor_type :schema
8
+ include AwsAssumeRole::Logging
9
+ attribute :access_key_id, Dry::Types["strict.string"].optional
10
+ attribute :credentials, Dry::Types["object"].optional
11
+ attribute :secret_access_key, Dry::Types["strict.string"].optional
12
+ attribute :session_token, Dry::Types["strict.string"].optional
13
+ attribute :duration_seconds, Dry::Types["coercible.int"].optional
14
+ attribute :external_id, Dry::Types["strict.string"].optional
15
+ attribute :path, Dry::Types["strict.string"].optional
16
+ attribute :persist_session, Dry::Types["strict.bool"].optional.default(true)
17
+ attribute :profile, Dry::Types["strict.string"].optional
18
+ attribute :region, Dry::Types["strict.string"].optional
19
+ attribute :role_arn, Dry::Types["strict.string"].optional
20
+ attribute :role_session_name, Dry::Types["strict.string"].optional
21
+ attribute :serial_number, Dry::Types["strict.string"].optional
22
+ attribute :mfa_serial, Dry::Types["strict.string"].optional
23
+ attribute :yubikey_oath_name, Dry::Types["strict.string"].optional
24
+ attribute :use_mfa, Dry::Types["strict.bool"].optional.default(false)
25
+ attribute :no_profile, Dry::Types["strict.bool"].optional.default(false)
26
+ attribute :shell_type, Dry::Types["strict.string"].optional
27
+ attribute :source_profile, Dry::Types["strict.string"].optional
28
+ attribute :args, Dry::Types["strict.array"].optional.default([])
29
+ attribute :instance_profile_credentials_retries, Dry::Types["strict.int"].default(0)
30
+ attribute :instance_profile_credentials_timeout, Dry::Types["coercible.float"].default(1.0)
31
+
32
+ attr_writer :credentials
33
+
34
+ def self.merge_mfa_variable(options)
35
+ new_hash = options.key?(:mfa_serial) ? options.merge(serial_number: options[:mfa_serial]) : options
36
+ new_hash[:use_mfa] ||= new_hash.fetch(:serial_number, nil) ? true : false
37
+ if new_hash.key?(:serial_number) && new_hash[:serial_number] == "automatic"
38
+ new_hash.delete(:serial_number)
39
+ end
40
+ new_hash
41
+ end
42
+
43
+ def self.new_from_cli(global_options, options, args)
44
+ options = global_options.merge options
45
+ options = options.map do |k, v|
46
+ [k.to_s.underscore.to_sym, v]
47
+ end.to_h
48
+ options[:args] = args
49
+ new merge_mfa_variable(options)
50
+ end
51
+
52
+ def self.new_from_credential_provider_initialization(options)
53
+ logger.debug "new_from_credential_provider_initialization with #{options.to_h}"
54
+ new_from_credential_provider(options, credentials: nil, delete: [])
55
+ end
56
+
57
+ def self.new_from_credential_provider(options = {}, credentials: nil, delete: [])
58
+ option_hash = options.to_h
59
+ config = option_hash.fetch(:config, {}).to_h
60
+ hash_to_merge = option_hash.merge config
61
+ hash_to_merge.merge(credentials: credentials) if credentials
62
+ delete.each do |k|
63
+ hash_to_merge.delete k
64
+ end
65
+ hash = merge_mfa_variable(hash_to_merge)
66
+ logger.debug "new_from_credential_provider with #{hash}"
67
+ new hash
68
+ end
69
+
70
+ def to_h
71
+ to_hash
72
+ end
73
+ end
@@ -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