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.
@@ -0,0 +1,38 @@
1
+ module AwsMfaSecure
2
+ class CLI < Command
3
+ desc "session *ARGV", "Calls aws cli with a MFA secure session"
4
+ long_desc Help.text(:session)
5
+ def session(*argv)
6
+ Session.new(options, *argv).run
7
+ end
8
+
9
+ desc "exports", "Generate export statements that can be eval"
10
+ long_desc Help.text(:exports)
11
+ def exports
12
+ Exports.new(options).run
13
+ end
14
+
15
+ desc "unsets", "Generate unsets statements that can be eval"
16
+ long_desc Help.text(:unsets)
17
+ def unsets
18
+ Unsets.new(options).run
19
+ end
20
+
21
+ desc "completion *PARAMS", "Prints words for auto-completion."
22
+ long_desc Help.text("completion")
23
+ def completion(*params)
24
+ Completer.new(CLI, *params).run
25
+ end
26
+
27
+ desc "completion_script", "Generates a script that can be eval to setup auto-completion."
28
+ long_desc Help.text("completion_script")
29
+ def completion_script
30
+ Completer::Script.generate
31
+ end
32
+
33
+ desc "version", "prints version"
34
+ def version
35
+ puts VERSION
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ require "thor"
2
+
3
+ # Override thor's long_desc identation behavior
4
+ # https://github.com/erikhuda/thor/issues/398
5
+ class Thor
6
+ module Shell
7
+ class Basic
8
+ def print_wrapped(message, options = {})
9
+ message = "\n#{message}" unless message[0] == "\n"
10
+ stdout.puts message
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module AwsMfaSecure
17
+ class Command < Thor
18
+ class << self
19
+ def dispatch(m, args, options, config)
20
+ # Allow calling for help via:
21
+ # aws-mfa-secure command help
22
+ # aws-mfa-secure command -h
23
+ # aws-mfa-secure command --help
24
+ # aws-mfa-secure command -D
25
+ #
26
+ # as well thor's normal way:
27
+ #
28
+ # aws-mfa-secure help command
29
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
30
+ if args.length > 1 && !(args & help_flags).empty?
31
+ args -= help_flags
32
+ args.insert(-2, "help")
33
+ end
34
+
35
+ # aws-mfa-secure version
36
+ # aws-mfa-secure --version
37
+ # aws-mfa-secure -v
38
+ version_flags = ["--version", "-v"]
39
+ if args.length == 1 && !(args & version_flags).empty?
40
+ args = ["version"]
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ # Override command_help to include the description at the top of the
47
+ # long_description.
48
+ def command_help(shell, command_name)
49
+ meth = normalize_command_name(command_name)
50
+ command = all_commands[meth]
51
+ alter_command_description(command)
52
+ super
53
+ end
54
+
55
+ def alter_command_description(command)
56
+ return unless command
57
+
58
+ # Add description to beginning of long_description
59
+ long_desc = if command.long_description
60
+ "#{command.description}\n\n#{command.long_description}"
61
+ else
62
+ command.description
63
+ end
64
+
65
+ # add reference url to end of the long_description
66
+ unless website.empty?
67
+ full_command = [command.ancestor_name, command.name].compact.join('-')
68
+ url = "#{website}/reference/aws-mfa-secure-#{full_command}"
69
+ long_desc += "\n\nHelp also available at: #{url}"
70
+ end
71
+
72
+ command.long_description = long_desc
73
+ end
74
+ private :alter_command_description
75
+
76
+ # meant to be overriden
77
+ def website
78
+ ""
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,159 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ aws-mfa-secure completion
52
+ aws-mfa-secure completion hello
53
+ aws-mfa-secure completion hello name
54
+ aws-mfa-secure completion hello name --
55
+ aws-mfa-secure completion hello name --noop
56
+
57
+ aws-mfa-secure completion
58
+ aws-mfa-secure completion sub:goodbye
59
+ aws-mfa-secure completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module AwsMfaSecure
72
+ class Completer
73
+ def initialize(command_class, *params)
74
+ @params = params
75
+ @current_command = @params[0]
76
+ @command_class = command_class # CLI initiall
77
+ end
78
+
79
+ def run
80
+ if subcommand?(@current_command)
81
+ subcommand_class = @command_class.subcommand_classes[@current_command]
82
+ @params.shift # destructive
83
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
84
+ return
85
+ end
86
+
87
+ # full command has been found!
88
+ unless found?(@current_command)
89
+ puts all_commands
90
+ return
91
+ end
92
+
93
+ # will only get to here if command aws found (above)
94
+ arity = @command_class.instance_method(@current_command).arity.abs
95
+ if @params.size > arity or thor_group_command?
96
+ puts options_completion
97
+ else
98
+ puts params_completion
99
+ end
100
+ end
101
+
102
+ def subcommand?(command)
103
+ @command_class.subcommands.include?(command)
104
+ end
105
+
106
+ # hacky way to detect that command is a registered Thor::Group command
107
+ def thor_group_command?
108
+ command_params(raw=true) == [[:rest, :args]]
109
+ end
110
+
111
+ def found?(command)
112
+ public_methods = @command_class.public_instance_methods(false)
113
+ command && public_methods.include?(command.to_sym)
114
+ end
115
+
116
+ # all top-level commands
117
+ def all_commands
118
+ commands = @command_class.all_commands.reject do |k,v|
119
+ v.is_a?(Thor::HiddenCommand)
120
+ end
121
+ commands.keys
122
+ end
123
+
124
+ def command_params(raw=false)
125
+ params = @command_class.instance_method(@current_command).parameters
126
+ # Example:
127
+ # >> Sub.instance_method(:goodbye).parameters
128
+ # => [[:req, :name]]
129
+ # >>
130
+ raw ? params : params.map!(&:last)
131
+ end
132
+
133
+ def params_completion
134
+ offset = @params.size - 1
135
+ offset_params = command_params[offset..-1]
136
+ command_params[offset..-1].first
137
+ end
138
+
139
+ def options_completion
140
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
141
+
142
+ method_options = @command_class.all_commands[@current_command].options.keys
143
+ class_options = @command_class.class_options.keys
144
+
145
+ all_options = method_options + class_options + ['help']
146
+
147
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
148
+ filtered_options = all_options - used
149
+ filtered_options.uniq
150
+ end
151
+
152
+ # Useful for debugging. Using puts messes up completion.
153
+ def log(msg)
154
+ File.open("/tmp/complete.log", "a") do |file|
155
+ file.puts(msg)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,6 @@
1
+ class AwsMfaSecure::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _aws-mfa-secure() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(aws-mfa-secure completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _aws-mfa-secure aws-mfa-secure
@@ -0,0 +1,35 @@
1
+ # Useful for Ruby interfacing
2
+ module AwsMfaSecure
3
+ class Credentials < Base
4
+ # Using Singleton as caching mechanism for speed
5
+ # The fetch_creds? is slow from shelling out to python for aws configure get.
6
+ include Singleton
7
+
8
+ attr_reader :data
9
+ def initialize
10
+ @aws_profile = aws_profile
11
+ setup
12
+ end
13
+
14
+ def setup
15
+ return unless iam_mfa?
16
+
17
+ if fetch_creds?
18
+ resp = get_session_token(shell: true)
19
+ save_creds(resp.credentials.to_h)
20
+ end
21
+
22
+ @data = credentials
23
+ end
24
+
25
+ def set?
26
+ !!@data
27
+ end
28
+
29
+ %w[access_key_id secret_access_key session_token].each do |name|
30
+ define_method name do
31
+ @data[name]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ module AwsMfaSecure
2
+ class Exports < Base
3
+ def initialize(options={})
4
+ @options = options
5
+ @aws_profile = aws_profile
6
+ end
7
+
8
+ def run
9
+ unless iam_mfa?
10
+ $stderr.puts "WARN: mfa_serial is not configured for this AWS_PROFILE=#{@aws_profile}"
11
+ return
12
+ end
13
+
14
+ if fetch_creds?
15
+ resp = get_session_token
16
+ save_creds(resp.credentials.to_h)
17
+ end
18
+
19
+ puts script
20
+ end
21
+
22
+ def script
23
+ <<~EOL
24
+ export AWS_ACCESS_KEY_ID=#{credentials["access_key_id"]}
25
+ export AWS_SECRET_ACCESS_KEY=#{credentials["secret_access_key"]}
26
+ export AWS_SESSION_TOKEN=#{credentials["session_token"]}
27
+ EOL
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ require "aws-sdk-core"
2
+
3
+ module AwsMfaCredentials
4
+ def initialize(*)
5
+ credentials = AwsMfaSecure::Credentials.instance
6
+ if credentials.set?
7
+ @access_key_id = credentials.access_key_id
8
+ @secret_access_key = credentials.secret_access_key
9
+ @session_token = credentials.session_token
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+
16
+ module Aws
17
+ class Credentials
18
+ prepend AwsMfaCredentials
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module AwsMfaSecure::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ ## Examples
2
+
3
+ aws-mfa-secure completion
4
+
5
+ Prints words for TAB auto-completion.
6
+
7
+ aws-mfa-secure completion
8
+ aws-mfa-secure completion hello
9
+ aws-mfa-secure completion hello name
10
+
11
+ To enable, TAB auto-completion add the following to your profile:
12
+
13
+ eval $(aws-mfa-secure completion_script)
14
+
15
+ Auto-completion example usage:
16
+
17
+ aws-mfa-secure [TAB]
18
+ aws-mfa-secure hello [TAB]
19
+ aws-mfa-secure hello name [TAB]
20
+ aws-mfa-secure hello name --[TAB]
@@ -0,0 +1,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(aws-mfa-secure completion_script)
@@ -0,0 +1,29 @@
1
+ ## Example
2
+
3
+ aws-mfa-secure exports
4
+
5
+ ## Example with Output
6
+
7
+ $ aws-mfa-secure exports
8
+ Please provide your MFA code: 147280
9
+ export AWS_ACCESS_KEY_ID=ASIAXZ6ODJLBCEXAMPLE
10
+ export AWS_SECRET_ACCESS_KEY=HgYHvNxacSsFSwls1FO9RoF5+tvYCFIABEXAMPLE
11
+ 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
12
+ $
13
+
14
+ Eval example:
15
+
16
+ $ eval `aws-mfa-secure exports`
17
+
18
+ Note: Prompts write to stderr so eval-ing the exports works even if with the prompt.
19
+
20
+ ## Related
21
+
22
+ You may also be interested in the `aws-mfa-secure unset` command. It provides a quick way to unset the AWS_* environment variables. Example:
23
+
24
+ $ aws-mfa-secure unset # generates script
25
+ unset AWS_ACCESS_KEY_ID
26
+ unset AWS_SECRET_ACCESS_KEY
27
+ unset AWS_SESSION_TOKEN
28
+ $ eval `aws-mfa-secure unset` # to unset
29
+ $