sty 0.0.1

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: be460e9dd95543a4956eddfaa6e45b8f1d8f9f89d74560bc769e23ad8284fd71
4
+ data.tar.gz: 0f6383f3d4913d6997420b78b6a30a9c266d029a45b2539c2fee6fa0c33aba77
5
+ SHA512:
6
+ metadata.gz: 1a6cab7166156c2be6e4f4ca0e7c66fe34687ec30b738ad4b35cf229f04852540fcc29bd2488f583e09a666c3f837bad12c1eb01c7c12c065ce282dce53e4d90
7
+ data.tar.gz: 67468b0793e2073b989d91d20d4e0e81e067b3e7cc9429bed8cb67de829e2161ce9cc0c898a9c97141dee04d9200e910bda0e6f8aa0d27bb902e0d19e8ce4400
@@ -0,0 +1,2 @@
1
+ lib/*.rb
2
+ lib/**/*.rb
@@ -0,0 +1,5 @@
1
+ # Sty
2
+ [![Build Status](https://travis-ci.org/sozonnyk/sty.svg?branch=master)](https://travis-ci.org/sozonnyk/sty)
3
+
4
+ <a title="Jean-Pol GRANDMONT [CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)], via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Fourneau_St-Michel_-_Porcherie_(Forri%C3%A8res).JPG">
5
+ <img alt="Fourneau St-Michel - Porcherie (Forrières)" src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Fourneau_St-Michel_-_Porcherie_%28Forri%C3%A8res%29.JPG/1024px-Fourneau_St-Michel_-_Porcherie_%28Forri%C3%A8res%29.JPG"></a>
@@ -0,0 +1,8 @@
1
+ username:
2
+ ssh-keys: keys
3
+ accounts:
4
+ ga:
5
+ prod:
6
+ users:
7
+ key_id:
8
+ secret_key:
@@ -0,0 +1,19 @@
1
+ accounts:
2
+ ga:
3
+ prod:
4
+ users:
5
+ acc_id: 123
6
+ tools:
7
+ acc_id: 456
8
+ parent: ga/prod/users
9
+ role: abc
10
+ mgmt:
11
+ acc_id: 789
12
+ parent: ga/prod/users
13
+ role: cde
14
+ hub:
15
+ acc_id: 121
16
+ parent: ga/prod/users
17
+
18
+
19
+
data/bin/sty ADDED
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+
3
+ if [ -z "$ZSH_VERSION" ] && [ -z "$BASH_VERSION" ]; then
4
+ echo "Your shell is neither bash nor zsh. Sty will not work."
5
+ exit 1
6
+ fi
7
+
8
+ if [[ $0 == "$BASH_SOURCE" ]]; then
9
+ export STY_SOURCE_RUN=false
10
+ else
11
+ export STY_SOURCE_RUN=true
12
+ fi
13
+
14
+ result=`ruby -e "require 'sty'" $@`
15
+
16
+ marker="#EVAL#"
17
+ if [[ "$result" =~ $marker ]]; then
18
+ eval "$result"
19
+ else
20
+ printf "$result\n"
21
+ fi
@@ -0,0 +1,7 @@
1
+ ga:
2
+ prod:
3
+ mgmt:
4
+ linux: sshjumphost.internal
5
+ windows: rdp-proxy.internal
6
+ tools: ga/prod/mgmt
7
+ test: ga/prod/mgmt
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'fileutils'
3
+
4
+ task default: ['install_script']
5
+
6
+ task :install_script do
7
+
8
+ fail "Sty doesnt work on Windows" if Gem.win_platform?
9
+
10
+ # Create /usr/local/bin/sty
11
+ src = "#{Dir.pwd}/../../bin/sty"
12
+ dst = '/usr/local/bin/sty'
13
+
14
+ #TODO check if /usr/local/bin exists
15
+ begin
16
+ FileUtils.cp(src, dst)
17
+ rescue Errno::EPERM, Errno::EACCES => e
18
+ puts "No permission to create #{dst}, let's try with sudo."
19
+ `sudo cp #{src} #{dst}`
20
+ end
21
+
22
+ # Create ~/.sty
23
+ home = File.expand_path('~')
24
+ sty_home = "#{home}/.sty/"
25
+ FileUtils.mkdir_p(sty_home)
26
+ FileUtils.mkdir_p("#{sty_home}/auth-cache")
27
+
28
+ # Copy config file examples
29
+ yamls = Dir.glob("#{Dir.pwd}/../../*.yaml")
30
+ dest_yamls = Dir.glob("#{sty_home}/*.yaml").map{|f| File.basename(f)}
31
+
32
+ yamls.reject! do |y|
33
+ dest_yamls.include?(File.basename(y))
34
+ end
35
+
36
+ FileUtils.cp(yamls, sty_home)
37
+
38
+ puts "All done"
39
+ end
@@ -0,0 +1,7 @@
1
+ trap "INT" do
2
+ $stderr.reopen(IO::NULL)
3
+ $stdout.reopen(IO::NULL)
4
+ exit 1
5
+ end
6
+
7
+ require_relative 'sty/cli'
@@ -0,0 +1,18 @@
1
+ require_relative 'functions'
2
+
3
+ class Account
4
+
5
+ def self.from_fqn(fqn)
6
+
7
+ end
8
+
9
+ def self.from_path(path)
10
+
11
+ end
12
+
13
+ def initialize(path)
14
+
15
+ end
16
+
17
+
18
+ end
@@ -0,0 +1,69 @@
1
+ require_relative 'functions'
2
+
3
+ require 'aws-sdk-iam'
4
+
5
+ DEFAULT_KEY_AGE = 90
6
+ Aws.config.update(:http_proxy => ENV['https_proxy'])
7
+
8
+ class Rotator
9
+
10
+ def rotate
11
+ keys = yaml('auth-keys')
12
+ path = to_path(act_acc)
13
+ current_key = keys['accounts'].dig(*path)
14
+ puts "Current account #{white(act_acc)}"
15
+
16
+ unless current_key
17
+ puts red("You need to authenticate to userstore account to rotate keys.")
18
+ exit(1)
19
+ end
20
+ puts "Current key #{white(current_key['key_id'])}"
21
+
22
+ iam = Aws::IAM::Client.new(region: region)
23
+
24
+ account_keys = iam.list_access_keys.access_key_metadata
25
+
26
+ key_to_rotate = account_keys.select {|k| k.access_key_id == current_key['key_id']}.first
27
+
28
+ unless key_to_rotate
29
+ puts red("Key #{current_key['key_id']} for account #{ act_acc } doesn't exist in AWS.")
30
+ exit(1)
31
+ end
32
+
33
+ if account_keys.size > 1
34
+ puts "You have #{white(account_keys.size)} keys already. Remove other keys before trying to rotate."
35
+ account_keys.each do |k|
36
+ key = k.access_key_id == current_key['key_id'] ? "#{white(k.access_key_id)} <-- Keep" : "#{red(k.access_key_id)} <-- Remove"
37
+ puts key
38
+ end
39
+ exit(1)
40
+ end
41
+
42
+ key_age_days = ((Time.now - key_to_rotate.create_date)/3600/24).round
43
+ puts "The key is #{white(key_age_days)} days old."
44
+
45
+ if key_age_days < DEFAULT_KEY_AGE
46
+ puts green('All good.')
47
+ exit(0)
48
+ else
49
+ puts 'Key needs rotation.'
50
+ end
51
+
52
+ new_key = iam.create_access_key()
53
+ current_key['key_id'] = new_key.access_key.access_key_id
54
+ current_key['secret_key'] = new_key.access_key.secret_access_key
55
+
56
+ puts "New key #{white(current_key['key_id'])} was created"
57
+
58
+ dump(keys, 'auth-keys')
59
+
60
+ account_keys.each do |k|
61
+ puts "Removing old key #{white(k.access_key_id)}"
62
+ iam.delete_access_key(access_key_id: k.access_key_id)
63
+ end
64
+
65
+ puts green('Key was rotated successfully.')
66
+ end
67
+ end
68
+
69
+ Rotator.new.rotate
@@ -0,0 +1,188 @@
1
+ require_relative 'functions'
2
+ require_relative 'dig'
3
+
4
+ SESSION_DURATION_SECONDS = 43_200
5
+ DEFAULT_ROLE_NAME = 'ReadOnlyRole'
6
+ DEFAULT_REGION = 'ap-southeast-2'
7
+
8
+ class Auth
9
+
10
+ def logout
11
+ current = ENV['AWS_ACTIVE_ACCOUNT']
12
+ identity = ENV['AWS_ACTIVE_IDENTITY']
13
+ STDERR.puts "Logging off from: #{white(current)}"
14
+ if current
15
+ cache = cache_file(to_path(current),identity)
16
+ begin
17
+ File.delete(cache)
18
+ rescue Errno::ENOENT => e
19
+ end
20
+ end
21
+ puts "#EVAL#"
22
+ puts "unset AWS_ACTIVE_ACCOUNT AWS_SESSION_EXPIRY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_ACTIVE_IDENTITY"
23
+ end
24
+
25
+ def deep_merge(h1,h2)
26
+ h1.merge(h2){|k,v1,v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? deep_merge(v1,v2) : v2}
27
+ end
28
+
29
+ def initialize
30
+ #aws-sdk is slow, so load it only when needed
31
+ require 'aws-sdk-core'
32
+ Aws.config.update(:http_proxy => ENV['https_proxy'])
33
+ @config = deep_merge(yaml('auth'),yaml('auth-keys'))
34
+ end
35
+
36
+ def check_proxy
37
+ unless ENV.find { |k,v| k =~ /HTTPS_PROXY/i }
38
+ STDERR.puts red("WARNING! \"https_proxy\" env variable is not set.")
39
+ end
40
+ end
41
+
42
+ def user
43
+ @config['username']
44
+ end
45
+
46
+ def account(path)
47
+ acc = @config['accounts'].dig(*path)
48
+ unless acc
49
+ STDERR.puts red("ERROR! Account #{to_fqn(path)} not found in config")
50
+ exit 1
51
+ end
52
+
53
+ acc['path'] = path
54
+ acc
55
+ end
56
+
57
+ def parent(acc)
58
+ acc['parent']
59
+ end
60
+
61
+ def cached_creds(path, identity)
62
+ acc_fqn = to_fqn(path)
63
+ begin
64
+ cached_creds = Psych.load_file(cache_file(path, identity))
65
+ raise(RuntimeError) unless cached_creds
66
+ rescue Errno::ENOENT, RuntimeError
67
+ STDERR.puts "No cached creds for #{acc_fqn}"
68
+ return nil
69
+ end
70
+
71
+ remained_minutes = ((cached_creds['expiration'] - Time.now) / 60).to_i
72
+
73
+ if remained_minutes > 0
74
+ STDERR.puts "Loaded cached creds for #{acc_fqn}"
75
+ STDERR.puts "Credentials will stay active for the next #{remained_minutes} min"
76
+ return {creds: Aws::Credentials.new(cached_creds['access_key_id'],
77
+ cached_creds['secret_access_key'],
78
+ cached_creds['session_token']),
79
+ expiry: cached_creds['expiration']}
80
+ else
81
+ STDERR.puts "Cached creds for #{acc_fqn} expired"
82
+ end
83
+ end
84
+
85
+ def save_creds(acc, creds, expiration, identity)
86
+ creds_hash = {'access_key_id' => creds.access_key_id,
87
+ 'secret_access_key' => creds.secret_access_key,
88
+ 'session_token' => creds.session_token,
89
+ 'expiration' => expiration
90
+ }
91
+ File.open(cache_file(acc['path'], identity), 'w') do |file|
92
+ file.write(Psych.dump(creds_hash))
93
+ end
94
+ end
95
+
96
+ def login_bare(acc)
97
+
98
+ path = acc['path']
99
+ acc_fqn = to_fqn(path)
100
+
101
+ cached = cached_creds(path, user)
102
+ return { creds: cached[:creds], expiry: cached[:expiry], identity: user } if cached
103
+
104
+ STDERR.puts "Enter MFA for #{acc_fqn}"
105
+ token = STDIN.gets.chomp
106
+
107
+ mfa = "arn:aws:iam::#{acc['acc_id']}:mfa/#{user}"
108
+
109
+ bare_creds = Aws::Credentials.new(acc['key_id'], acc['secret_key'])
110
+
111
+ sts = Aws::STS::Client.new(credentials: bare_creds, region: region)
112
+
113
+ begin
114
+ session = sts.get_session_token(duration_seconds: SESSION_DURATION_SECONDS,
115
+ serial_number: mfa,
116
+ token_code: token)
117
+
118
+ creds = Aws::Credentials.new(session.credentials.access_key_id,
119
+ session.credentials.secret_access_key,
120
+ session.credentials.session_token)
121
+ rescue Exception => e
122
+ STDERR.puts red("ERROR! Unable to obtain credentials for #{acc_fqn}")
123
+ STDERR.puts white(e.message)
124
+ exit 1
125
+ end
126
+
127
+ STDERR.puts green("Successfully obtained creds for #{acc_fqn}")
128
+
129
+ save_creds(acc, creds, session.credentials.expiration, user)
130
+
131
+ {creds: creds, expiry: session.credentials.expiration, identity: user}
132
+ end
133
+
134
+ def login_role(acc, role)
135
+ path = acc['path']
136
+ active_role = role || acc['role'] || DEFAULT_ROLE_NAME
137
+ role_arn = "arn:aws:iam::#{acc['acc_id']}:role/#{active_role}"
138
+
139
+ cached = cached_creds(path, active_role)
140
+ return { creds: cached[:creds], expiry: cached[:expiry], identity: active_role } if cached
141
+
142
+ parent_path = to_path(parent(acc))
143
+ parent_acc = account(parent_path)
144
+ parent_creds = login_bare(parent_acc)[:creds]
145
+ sts = Aws::STS::Client.new(
146
+ credentials: parent_creds,
147
+ endpoint: 'https://sts.ap-southeast-2.amazonaws.com',
148
+ region: region
149
+ )
150
+ begin
151
+ creds = sts.assume_role(role_arn: role_arn,
152
+ role_session_name: "#{user}-#{parent_path.join('-')}",
153
+ duration_seconds: 3600).credentials
154
+ rescue Exception => e
155
+ STDERR.puts red("ERROR! Unable to obtain credentials for #{to_fqn(path)}")
156
+ STDERR.puts white(e.message)
157
+ exit 1
158
+ end
159
+ STDERR.puts green("Successfully obtained creds for #{to_fqn(path)}")
160
+ save_creds(acc, creds, creds.expiration, active_role)
161
+
162
+ {creds: creds, expiry: creds.expiration, identity: active_role}
163
+ end
164
+
165
+ def print_creds(acc, creds)
166
+ puts "#EVAL#"
167
+ puts "export AWS_ACTIVE_ACCOUNT=#{to_fqn(acc['path'])}"
168
+ puts "export AWS_ACTIVE_IDENTITY=#{creds[:identity]}"
169
+ puts "export AWS_SESSION_EXPIRY=\"#{creds[:expiry]}\""
170
+ puts "export AWS_ACCESS_KEY_ID=#{creds[:creds].access_key_id}"
171
+ puts "export AWS_SECRET_ACCESS_KEY=#{creds[:creds].secret_access_key}"
172
+ puts "export AWS_SESSION_TOKEN=#{creds[:creds].session_token}"
173
+ end
174
+
175
+ def login(fqn, role = nil)
176
+ check_proxy
177
+ acc = account(to_path(fqn))
178
+
179
+ if parent(acc)
180
+ creds = login_role(acc, role)
181
+ else
182
+ creds = login_bare(acc)
183
+ end
184
+
185
+ print_creds(acc, creds) if creds
186
+ end
187
+
188
+ end
@@ -0,0 +1,71 @@
1
+ require 'thor'
2
+ require_relative 'info'
3
+ require_relative 'auth'
4
+ require_relative 'console'
5
+ require_relative 'proxy'
6
+ require_relative 'ssh'
7
+
8
+ class Cli < Thor
9
+ map auth: :login, acct: :account, px: :proxy
10
+
11
+ def self.basename
12
+ 'sty'
13
+ end
14
+
15
+ desc "ssh [OPTIONS] <SEARCH_TERM...>","Creates ssh connection to desired ec2 instance through existing jumphost. SEARCH_TERM to search in EC2 instance ID, name or IP address"
16
+ method_option :no_jumphost, type: :boolean, default: false, aliases: "-n", desc: "Connect directly without jumphost"
17
+ method_option :select_jumphost, type: :boolean, aliases: "-s", desc: "Select jumphost instance"
18
+ method_option :use_key, type: :boolean, aliases: "-k", desc: "Use private key auth for target instance. Keys are searched recursively in ~/.sty/keys"
19
+ def ssh(*search_term)
20
+ Ssh.new.connect(search_term, options[:no_jumphost], options[:select_jumphost], options[:use_key])
21
+ end
22
+
23
+ desc "console", "Opens AWS console in browser for currently authenticated session"
24
+ method_option :browser, type: :string, aliases: "-b", enum: Console::BROWSERS, desc: "Use specific browser"
25
+ method_option :incognito, type: :boolean, aliases: "-i", desc: "Create new incognito window"
26
+ method_option :logout, type: :boolean, aliases: "-l", dssc: "Logout from current session"
27
+ def console
28
+ Console.new.action(options[:browser], options[:incognito], options[:logout])
29
+ end
30
+
31
+ desc "login ACCOUNT_PATH", "Authenticate to the account"
32
+ method_option :role, aliases: "-r", dssc: "Override role name"
33
+ def login(path)
34
+ source_run(__method__)
35
+ Auth.new.login(path, options[:browser])
36
+ end
37
+
38
+ desc "logout", "Forget current credentials and clear cache"
39
+ def logout
40
+ source_run(__method__)
41
+ Auth.new.logout
42
+ end
43
+
44
+ desc "info", "Get current session information"
45
+ def info
46
+ Info.new.session_info
47
+ end
48
+
49
+ desc "account ACCOUNT_ID", "Find account information"
50
+ def account(path)
51
+ Info.new.account_info(path)
52
+ end
53
+
54
+ desc "proxy [PROXY_ID]", "Switch session proxy (use 'off' to disable)"
55
+ def proxy(px = nil)
56
+ source_run(__method__)
57
+ Proxy.new.action(px)
58
+ end
59
+
60
+ no_tasks do
61
+ def source_run(method)
62
+ unless ENV['STY_SOURCE_RUN'] == 'true'
63
+ puts "When using '#{method.to_s}' command, you must source it, i.e.: '. sty #{method.to_s}'"
64
+ exit 128
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ Cli.start(ARGV)