aws-mfa-secure 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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +126 -0
- data/Rakefile +14 -0
- data/aws-mfa-secure.gemspec +33 -0
- data/docs/how-it-works.md +19 -0
- data/exe/aws-mfa-secure +14 -0
- data/lib/aws-mfa-secure.rb +1 -0
- data/lib/aws_mfa_secure.rb +12 -0
- data/lib/aws_mfa_secure/autoloader.rb +22 -0
- data/lib/aws_mfa_secure/base.rb +128 -0
- data/lib/aws_mfa_secure/cli.rb +38 -0
- data/lib/aws_mfa_secure/command.rb +82 -0
- data/lib/aws_mfa_secure/completer.rb +159 -0
- data/lib/aws_mfa_secure/completer/script.rb +6 -0
- data/lib/aws_mfa_secure/completer/script.sh +10 -0
- data/lib/aws_mfa_secure/credentials.rb +35 -0
- data/lib/aws_mfa_secure/exports.rb +30 -0
- data/lib/aws_mfa_secure/ext/aws.rb +20 -0
- data/lib/aws_mfa_secure/help.rb +9 -0
- data/lib/aws_mfa_secure/help/completion.md +20 -0
- data/lib/aws_mfa_secure/help/completion_script.md +3 -0
- data/lib/aws_mfa_secure/help/exports.md +29 -0
- data/lib/aws_mfa_secure/help/session.md +4 -0
- data/lib/aws_mfa_secure/help/unsets.md +17 -0
- data/lib/aws_mfa_secure/session.rb +26 -0
- data/lib/aws_mfa_secure/unsets.rb +19 -0
- data/lib/aws_mfa_secure/version.rb +3 -0
- data/spec/fixtures/aws-mfa-secure-sessions/fake_credentials +6 -0
- data/spec/lib/cli_spec.rb +18 -0
- data/spec/monkey_patches.rb +20 -0
- data/spec/spec_helper.rb +29 -0
- metadata +239 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 373259274cc623e8bb6b7ca2e044be59ea2abaa6c09f268bdf5376518b04baff
|
4
|
+
data.tar.gz: 8a59132aa173779f10f17d7e5d9ce4eedcb50ae304b6c8624f8db3e37dd79bbe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 890158626d55398170a6904f1c2e19ce863a52caee43dd458f7d17967c9e73d0039676fcc3c0f36cd05658aee6ac6152fd568c80edb0b65e048812b98e379104
|
7
|
+
data.tar.gz: 9e09d81ad7ec37a89e4e4e5c0305c0e9f4367286ccc7d5664c90aa7aa6a3e38921b2f3b850442dfa4dd023920ff0d8dd77f516f6231b63b9029de31c8914684c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
aws-mfa-secure (0.1.0)
|
5
|
+
activesupport
|
6
|
+
aws-sdk-core
|
7
|
+
memoist
|
8
|
+
rainbow
|
9
|
+
thor
|
10
|
+
zeitwerk
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
activesupport (6.0.1)
|
16
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
17
|
+
i18n (>= 0.7, < 2)
|
18
|
+
minitest (~> 5.1)
|
19
|
+
tzinfo (~> 1.1)
|
20
|
+
zeitwerk (~> 2.2)
|
21
|
+
aws-eventstream (1.0.3)
|
22
|
+
aws-partitions (1.237.0)
|
23
|
+
aws-sdk-core (3.76.0)
|
24
|
+
aws-eventstream (~> 1.0, >= 1.0.2)
|
25
|
+
aws-partitions (~> 1, >= 1.228.0)
|
26
|
+
aws-sigv4 (~> 1.1)
|
27
|
+
jmespath (~> 1.0)
|
28
|
+
aws-sigv4 (1.1.0)
|
29
|
+
aws-eventstream (~> 1.0, >= 1.0.2)
|
30
|
+
byebug (11.0.1)
|
31
|
+
cli_markdown (0.1.0)
|
32
|
+
codeclimate-test-reporter (1.0.9)
|
33
|
+
simplecov (<= 0.13)
|
34
|
+
concurrent-ruby (1.1.5)
|
35
|
+
diff-lcs (1.3)
|
36
|
+
docile (1.1.5)
|
37
|
+
i18n (1.7.0)
|
38
|
+
concurrent-ruby (~> 1.0)
|
39
|
+
jmespath (1.4.0)
|
40
|
+
json (2.2.0)
|
41
|
+
memoist (0.16.1)
|
42
|
+
minitest (5.13.0)
|
43
|
+
rainbow (3.0.0)
|
44
|
+
rake (13.0.0)
|
45
|
+
rspec (3.9.0)
|
46
|
+
rspec-core (~> 3.9.0)
|
47
|
+
rspec-expectations (~> 3.9.0)
|
48
|
+
rspec-mocks (~> 3.9.0)
|
49
|
+
rspec-core (3.9.0)
|
50
|
+
rspec-support (~> 3.9.0)
|
51
|
+
rspec-expectations (3.9.0)
|
52
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
+
rspec-support (~> 3.9.0)
|
54
|
+
rspec-mocks (3.9.0)
|
55
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
+
rspec-support (~> 3.9.0)
|
57
|
+
rspec-support (3.9.0)
|
58
|
+
simplecov (0.13.0)
|
59
|
+
docile (~> 1.1.0)
|
60
|
+
json (>= 1.8, < 3)
|
61
|
+
simplecov-html (~> 0.10.0)
|
62
|
+
simplecov-html (0.10.2)
|
63
|
+
thor (0.20.3)
|
64
|
+
thread_safe (0.3.6)
|
65
|
+
tzinfo (1.2.5)
|
66
|
+
thread_safe (~> 0.1)
|
67
|
+
zeitwerk (2.2.1)
|
68
|
+
|
69
|
+
PLATFORMS
|
70
|
+
ruby
|
71
|
+
|
72
|
+
DEPENDENCIES
|
73
|
+
aws-mfa-secure!
|
74
|
+
bundler
|
75
|
+
byebug
|
76
|
+
cli_markdown
|
77
|
+
codeclimate-test-reporter
|
78
|
+
rake
|
79
|
+
rspec
|
80
|
+
|
81
|
+
BUNDLED WITH
|
82
|
+
2.0.2
|
data/Guardfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
guard "bundler", cmd: "bundle" do
|
2
|
+
watch("Gemfile")
|
3
|
+
watch(/^.+\.gemspec/)
|
4
|
+
end
|
5
|
+
|
6
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
7
|
+
require "guard/rspec/dsl"
|
8
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
9
|
+
|
10
|
+
# RSpec files
|
11
|
+
rspec = dsl.rspec
|
12
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
13
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
14
|
+
watch(rspec.spec_files)
|
15
|
+
|
16
|
+
# Ruby files
|
17
|
+
ruby = dsl.ruby
|
18
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
19
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2019 Tung Nguyen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# AWS MFA Secure
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/aws-mfa-secure)
|
4
|
+
|
5
|
+
Surprisingly, the [aws cli](https://docs.aws.amazon.com/cli/latest/reference/) does not yet support MFA for normal IAM users. See: https://github.com/boto/botocore/pull/1399 The aws-mfa-secure tool decorates the AWS CLI or API to handle MFA authentication. The MFA prompt only activates if `mfa_serial` is configured.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
gem install aws-mfa-secure
|
10
|
+
|
11
|
+
Prerequisite: The [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/) is required. You can install the AWS CLI via pip.
|
12
|
+
|
13
|
+
pip install awscli --upgrade --user
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
**Summary**:
|
18
|
+
|
19
|
+
1. Configure `~/.aws/credentials` with `mfa_serial`
|
20
|
+
2. Set up bash alias
|
21
|
+
3. Use aws cli like normal
|
22
|
+
|
23
|
+
### Configure ~/.aws/credentials with mfa_serial
|
24
|
+
|
25
|
+
Set up `mfa_serial` in credentials file for the profile section that requires it. Example:
|
26
|
+
|
27
|
+
~/.aws/credentials:
|
28
|
+
|
29
|
+
[mfa]
|
30
|
+
aws_access_key_id = BKCAXZ6ODJLQ1EXAMPLE
|
31
|
+
aws_secret_access_key = ABCDl4hXikfOHTvNqFAnb2Ea62bUuu/eUEXAMPLE
|
32
|
+
mfa_serial = arn:aws:iam::112233445566:mfa/MFAUser
|
33
|
+
|
34
|
+
Note: AWS already supports `mfa_serial` assumed roles: [AWS Configuration and Credential File Settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). The aws-mfa-secure tool does not decorate for assumed roles and lets the AWS CLI or SDK handle it.
|
35
|
+
|
36
|
+
### Set up bash alias
|
37
|
+
|
38
|
+
alias aws="aws-mfa-secure session"
|
39
|
+
|
40
|
+
You may want to add the alias to your `~/.bash_profile`
|
41
|
+
|
42
|
+
Autocompletion still works with the alias.
|
43
|
+
|
44
|
+
### Use aws cli like normal
|
45
|
+
|
46
|
+
Call `aws` command like you usually would:
|
47
|
+
|
48
|
+
aws s3 ls
|
49
|
+
|
50
|
+
### Example with Output
|
51
|
+
|
52
|
+
$ export AWS_PROFILE=mfa
|
53
|
+
$ aws s3 ls
|
54
|
+
Please provide your MFA code: 751888
|
55
|
+
2019-09-21 15:53:34 my-example-test-bucket
|
56
|
+
$ aws s3 ls
|
57
|
+
2019-09-21 15:53:34 my-example-test-bucket
|
58
|
+
$
|
59
|
+
|
60
|
+
Expiration: You get prompted for the MFA token once, and the MFA secure session lasts for 12 hours. You can override the default expiration time with `AWS_MFA_TTL`. For example, `AWS_MFA_TTL=3600` means the session expires in 1 hour instead.
|
61
|
+
|
62
|
+
## Calling Directly
|
63
|
+
|
64
|
+
You can also call `aws-mfa-secure session` directly.
|
65
|
+
|
66
|
+
aws-mfa-secure session --version
|
67
|
+
aws-mfa-secure session s3 ls
|
68
|
+
|
69
|
+
The arguments of `aws-mfa-secure session` are delegated to the `aws` command. So:
|
70
|
+
|
71
|
+
aws-mfa-secure session s3 ls
|
72
|
+
|
73
|
+
Is the same as:
|
74
|
+
|
75
|
+
aws s3 ls
|
76
|
+
|
77
|
+
Except `aws-mfa-secure session` will use the temporary session environment `AWS_*` variables values.
|
78
|
+
|
79
|
+
## Exports
|
80
|
+
|
81
|
+
You can also generate the exports script.
|
82
|
+
|
83
|
+
$ aws-mfa-secure exports
|
84
|
+
Please provide your MFA code: 147280
|
85
|
+
export AWS_ACCESS_KEY_ID=ASIAXZ6ODJLBCEXAMPLE
|
86
|
+
export AWS_SECRET_ACCESS_KEY=HgYHvNxacSsFSwls1FO9RoF5+tvYCFIABEXAMPLE
|
87
|
+
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEJ3//////////wEaCXVzLWVhc3QtMSJGMEQCIGnuGzUr8aszNWMFlFXQvFVhIA6aGdx3DskqY1JaIZWVAiANfE3xA79vIMVTqLnds4F2LpDy/qUeNRr7e9g9VQoS9SqyAQi2//////////8BEAEaDDUzNjc2NjI3MDE3NyIMgDgauwgJ4FIOMRV+KoYBRKR/MnKFB9/Q0Isc6D8gpG404xGJWqStNfGS0sHNsB5vVP/ccaAj4MG54p0Pl+V0LuIMXy345ua/bxxQFDWqhG0ORsXFEOo3iD1IQ+YA/yougAUl/0hbyvK3Jnf3NEHDejdL95iFCluJhoR0zFlDv7GwwBSXLUxS9K96/vgA0MmgK9a7kaAwoYiZ7gU63wHVDNYa1myqIP16Mi6KZ2zm9inMofixNN1ea3JMyRW+chWW8kdjjW4R3MFecpwoIayE7g3QLanmjE3jzrlxjIJWnl8tiipV+jassiSdlxLL2j1IIFH2pNEqrn4hkHG5t7OG+qZCTl8AnQ4W5wusmBoSIavr5w0dOdyx2mdsBMFtO82ZXvHSryY1gbIM9JyUd7dJ9h/mkfGL2p0n0R/lya8s9j8P8/8if+2uQcF+/BGDxojJ67kYXgstgfLjM5j8pZgyYj6YUFyTpyiOkllbPk/AjyxJY1svxW25wbNO+c13
|
88
|
+
$
|
89
|
+
|
90
|
+
You can eval it to set the environment variables in one go. Note, the MFA code prompt is written to standard error so it won't affect the eval.
|
91
|
+
|
92
|
+
$ eval `aws-mfa-secure exports`
|
93
|
+
|
94
|
+
If you're using the `aws-mfa-secure exports` command, the `aws-mfa-secure unsets` command is useful to unset the `AWS_*` env variables quickly. For more info: `aws-mfa-secure unsets -h`.
|
95
|
+
|
96
|
+
## AWS Extension
|
97
|
+
|
98
|
+
You can also use `aws-mfa-secure` to add MFA support to Ruby libraries. Do so by requiring the `aws_mfa_secure/ext/aws`.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
require "aws_mfa_secure/ext/aws" # add MFA support
|
102
|
+
```
|
103
|
+
|
104
|
+
This patches the aws-sdk-ruby library and adds MFA support.
|
105
|
+
|
106
|
+
## Setting MFA Info with Env Variables
|
107
|
+
|
108
|
+
You can also set the MFA info with env variables. They take the highest precedence and override what's in `~/.aws/credentials`. Example:
|
109
|
+
|
110
|
+
AWS_MFA_TOKEN=112233 arn:aws:iam::112233445566:mfa/MFAUser aws s3 ls
|
111
|
+
|
112
|
+
## How It Works
|
113
|
+
|
114
|
+
docs: [How It Works](docs/how-it-works.md)
|
115
|
+
|
116
|
+
## Related
|
117
|
+
|
118
|
+
You may also be interested in [tongueroo/aws-rotate](https://github.com/tongueroo/aws-rotate). It's an easy way to rotate all your AWS keys in your `~/.aws/credentials`.
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
1. Fork it
|
123
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
124
|
+
3. Commit your changes (`git commit -am "Add some feature"`)
|
125
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
126
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
task default: :spec
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
|
8
|
+
require_relative "lib/aws-mfa-secure"
|
9
|
+
require "cli_markdown"
|
10
|
+
desc "Generates cli reference docs as markdown"
|
11
|
+
task :docs do
|
12
|
+
mkdir_p "docs/_includes"
|
13
|
+
CliMarkdown::Creator.create_all(cli_class: AwsMfaSecure::CLI, cli_name: "aws-mfa-secure")
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "aws_mfa_secure/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "aws-mfa-secure"
|
8
|
+
spec.version = AwsMfaSecure::VERSION
|
9
|
+
spec.authors = ["Tung Nguyen"]
|
10
|
+
spec.email = ["tongueroo@gmail.com"]
|
11
|
+
spec.summary = "Adds MFA Support to AWS CLI and Ruby SDKs for normal IAM user"
|
12
|
+
spec.homepage = "https://github.com/tongueroo/aws-mfa-secure"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_dependency "aws-sdk-core"
|
23
|
+
spec.add_dependency "memoist"
|
24
|
+
spec.add_dependency "rainbow"
|
25
|
+
spec.add_dependency "thor"
|
26
|
+
spec.add_dependency "zeitwerk"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "byebug"
|
30
|
+
spec.add_development_dependency "cli_markdown"
|
31
|
+
spec.add_development_dependency "rake"
|
32
|
+
spec.add_development_dependency "rspec"
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
## How It Works
|
2
|
+
|
3
|
+
The wrapper `aws-mfa-secure session` command uses [sts.get_session_token](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/STS/Client.html#get_session_token-instance_method) to fetch temporary session AWS access tokens.
|
4
|
+
|
5
|
+
The tokens get saved to `~/.aws/aws-mfa-secure-sessions/AWS_PROFILE` and are then used to set the environment variables, which take the [higher precedence](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#config-settings-and-precedence).
|
6
|
+
|
7
|
+
* AWS_ACCESS_KEY_ID
|
8
|
+
* AWS_SECRET_ACCESS_KEY
|
9
|
+
* AWS_SESSION_TOKEN
|
10
|
+
|
11
|
+
The arguments are delegate the to the `aws` command. So:
|
12
|
+
|
13
|
+
aws-mfa-secure session s3 ls
|
14
|
+
|
15
|
+
Is the same as:
|
16
|
+
|
17
|
+
aws s3 ls
|
18
|
+
|
19
|
+
Except using the session environment `AWS_*` variables values.
|
data/exe/aws-mfa-secure
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Trap ^C
|
4
|
+
Signal.trap("INT") {
|
5
|
+
puts "\nCtrl-C detected. Exiting..."
|
6
|
+
sleep 0.1
|
7
|
+
exit
|
8
|
+
}
|
9
|
+
|
10
|
+
$:.unshift(File.expand_path("../../lib", __FILE__))
|
11
|
+
require "aws-mfa-secure"
|
12
|
+
require "aws_mfa_secure/cli"
|
13
|
+
|
14
|
+
AwsMfaSecure::CLI.start(ARGV)
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "aws_mfa_secure"
|
@@ -0,0 +1,12 @@
|
|
1
|
+
$:.unshift(File.expand_path("../", __FILE__))
|
2
|
+
require "aws_mfa_secure/version"
|
3
|
+
require "rainbow/ext/string"
|
4
|
+
|
5
|
+
require "aws_mfa_secure/autoloader"
|
6
|
+
AwsMfaSecure::Autoloader.setup
|
7
|
+
|
8
|
+
module AwsMfaSecure
|
9
|
+
class Error < StandardError; end
|
10
|
+
end
|
11
|
+
|
12
|
+
require "#{Dir.pwd}/spec/monkey_patches" if ENV['AWS_MFA_SECURE_TEST']
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
|
3
|
+
module AwsMfaSecure
|
4
|
+
class Autoloader
|
5
|
+
class Inflector < Zeitwerk::Inflector
|
6
|
+
def camelize(basename, _abspath)
|
7
|
+
map = { cli: "CLI", version: "VERSION" }
|
8
|
+
map[basename.to_sym] || super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def setup
|
14
|
+
loader = Zeitwerk::Loader.new
|
15
|
+
loader.inflector = Inflector.new
|
16
|
+
loader.push_dir(File.dirname(__dir__)) # lib
|
17
|
+
loader.ignore("#{File.dirname(__dir__)}/aws-mfa-secure.rb")
|
18
|
+
loader.setup
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "aws-sdk-core"
|
2
|
+
require "fileutils"
|
3
|
+
require "json"
|
4
|
+
require "memoist"
|
5
|
+
require "time"
|
6
|
+
require "active_support/core_ext/string"
|
7
|
+
require "active_support/core_ext/hash"
|
8
|
+
|
9
|
+
module AwsMfaSecure
|
10
|
+
class MfaError < StandardError; end
|
11
|
+
|
12
|
+
class Base
|
13
|
+
extend Memoist
|
14
|
+
|
15
|
+
def iam_mfa?
|
16
|
+
return false unless mfa_serial
|
17
|
+
|
18
|
+
# The iam_mfa? check will only return true for the case when mfa_serial is set and access keys are used.
|
19
|
+
# This is because for assume role cases, the current aws cli tool supports mfa_serial already.
|
20
|
+
# Sending session AWS based access keys intefere with the current aws cli assume role mfa_serial support
|
21
|
+
aws_access_key_id = aws_configure_get(:aws_access_key_id)
|
22
|
+
aws_secret_access_key = aws_configure_get(:aws_secret_access_key)
|
23
|
+
source_profile = aws_configure_get(:source_profile)
|
24
|
+
|
25
|
+
aws_access_key_id && aws_secret_access_key && !source_profile
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch_creds?
|
29
|
+
!good_session_creds?
|
30
|
+
end
|
31
|
+
|
32
|
+
def good_session_creds?
|
33
|
+
return false unless File.exist?(session_creds_path)
|
34
|
+
|
35
|
+
expiration = Time.parse(credentials["expiration"])
|
36
|
+
Time.now.utc < expiration # not expired
|
37
|
+
end
|
38
|
+
|
39
|
+
def credentials
|
40
|
+
JSON.load(IO.read(session_creds_path))
|
41
|
+
end
|
42
|
+
memoize :credentials
|
43
|
+
|
44
|
+
def save_creds(credentials)
|
45
|
+
FileUtils.mkdir_p(File.dirname(session_creds_path))
|
46
|
+
IO.write(session_creds_path, JSON.pretty_generate(credentials))
|
47
|
+
end
|
48
|
+
|
49
|
+
def session_creds_path
|
50
|
+
"#{ENV['HOME']}/.aws/aws-mfa-secure-sessions/#{@aws_profile}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_session_token(shell: false)
|
54
|
+
retries = 0
|
55
|
+
begin
|
56
|
+
token_code = mfa_prompt
|
57
|
+
options = {
|
58
|
+
serial_number: mfa_serial,
|
59
|
+
token_code: token_code,
|
60
|
+
}
|
61
|
+
options[:duration_seconds] = ENV['AWS_MFA_TTL'] if ENV['AWS_MFA_TTL']
|
62
|
+
|
63
|
+
if shell
|
64
|
+
shell_get_session_token(options, token_code) # mimic ruby sdk
|
65
|
+
else # ruby sdk
|
66
|
+
sts.get_session_token(options)
|
67
|
+
end
|
68
|
+
rescue Aws::STS::Errors::ValidationError, Aws::STS::Errors::AccessDenied, MfaError => e
|
69
|
+
$stderr.puts "#{e.class}: #{e.message}"
|
70
|
+
$stderr.puts "Incorrect MFA code. Please try again."
|
71
|
+
retries += 1
|
72
|
+
if retries >= 3
|
73
|
+
$stderr.puts "Giving up after #{retries} retries."
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
retry
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def mfa_prompt
|
81
|
+
if ENV['AWS_MFA_TOKEN']
|
82
|
+
token_code = ENV.delete('AWS_MFA_TOKEN') # only use once, prompt afterwards if incorrect
|
83
|
+
return token_code
|
84
|
+
end
|
85
|
+
|
86
|
+
$stderr.print "Please provide your MFA code: "
|
87
|
+
$stdin.gets.strip
|
88
|
+
end
|
89
|
+
|
90
|
+
def shell_get_session_token(options, token_code)
|
91
|
+
args = options.map { |k,v| "--#{k.to_s.gsub('_','-')} #{v}" }.join(' ')
|
92
|
+
command = "aws sts get-session-token #{args} 2>&1"
|
93
|
+
# puts "=> #{command}" # uncomment for debugging
|
94
|
+
out = `#{command}`
|
95
|
+
|
96
|
+
unless out.include?("Credentials")
|
97
|
+
raise(MfaError, out.strip) # custom error
|
98
|
+
end
|
99
|
+
|
100
|
+
data = JSON.load(out)
|
101
|
+
resp = data.deep_transform_keys { |k| k.underscore }
|
102
|
+
# mimic ruby sdk resp
|
103
|
+
credentials = Aws::STS::Types::Credentials.new(resp["credentials"])
|
104
|
+
Aws::STS::Types::GetSessionTokenResponse.new(credentials: credentials)
|
105
|
+
end
|
106
|
+
|
107
|
+
def mfa_serial
|
108
|
+
ENV['AWS_MFA_SERIAL'] || aws_configure_get(:mfa_serial)
|
109
|
+
end
|
110
|
+
|
111
|
+
def sts
|
112
|
+
Aws::STS::Client.new
|
113
|
+
end
|
114
|
+
memoize :sts
|
115
|
+
|
116
|
+
# Note the strip
|
117
|
+
# Each aws configure get call has about a 300-400ms overhead so we memoize it.
|
118
|
+
def aws_configure_get(prop)
|
119
|
+
v = `aws configure get #{prop}`.strip
|
120
|
+
v unless v.empty?
|
121
|
+
end
|
122
|
+
memoize :aws_configure_get
|
123
|
+
|
124
|
+
def aws_profile
|
125
|
+
ENV['AWS_PROFILE'] || 'default'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|