miam 0.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 78a6357ad96eb46c37e14554170823849a1b7299
4
+ data.tar.gz: 5e8e8b62d90cfa1d313874b5d842a505bb07bcfe
5
+ SHA512:
6
+ metadata.gz: 2bbe33cd3fc4274a239bfde9bff8eee340af294178b873f9b3f1c533a5efd02f0a41a960ee8d752f54c252eadba21a3f2a1484f939a94e16c96690e173e8ee3f
7
+ data.tar.gz: aa09af7d99890cd20908d5f6991846978bc40fcdf007e2cc7b152e72f81f13e49b00b9120646e34524ec95e66bc863a12b5586c49d54518146e4593fa8d01382
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ test.rb
16
+ IAMfile
17
+ *.iam
18
+ account.csv
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in miam.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Genki Sugawara
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.
@@ -0,0 +1,112 @@
1
+ # Miam
2
+
3
+ Miam is a tool to manage IAM.
4
+
5
+ It defines the state of IAM using DSL, and updates IAM according to DSL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'miam'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install miam --pre
22
+
23
+ ## Usage
24
+
25
+ ```sh
26
+ export AWS_ACCESS_KEY_ID='...'
27
+ export AWS_SECRET_ACCESS_KEY='...'
28
+ export AWS_REGION='us-east-1'
29
+ miam -e -o IAMfile # export IAM
30
+ vi IAMfile
31
+ miam -a --dry-run
32
+ miam -a # apply `IAMfile`
33
+ ```
34
+
35
+ ## Help
36
+
37
+ ```
38
+ Usage: miam [options]
39
+ -p, --profile PROFILE_NAME
40
+ --credentials-path PATH
41
+ -k, --access-key ACCESS_KEY
42
+ -s, --secret-key SECRET_KEY
43
+ -r, --region REGION
44
+ -a, --apply
45
+ -f, --file FILE
46
+ --dry-run
47
+ --account-output FILE
48
+ -e, --export
49
+ -o, --output FILE
50
+ --split
51
+ --no-color
52
+ --no-progress
53
+ --debug
54
+ ```
55
+
56
+ ## IAMfile example
57
+
58
+ ```ruby
59
+ require 'other/iamfile'
60
+
61
+ user "bob", path: "/developer/" do
62
+ login_profile password_reset_required: true
63
+
64
+ groups(
65
+ "Admin"
66
+ )
67
+
68
+ policy "bob-policy" do
69
+ {"Version"=>"2012-10-17",
70
+ "Statement"=>
71
+ [{"Action"=>
72
+ ["s3:Get*",
73
+ "s3:List*"],
74
+ "Effect"=>"Allow",
75
+ "Resource"=>"*"}]}
76
+ end
77
+ end
78
+
79
+ user "mary", path: "/staff/" do
80
+ # login_profile password_reset_required: true
81
+
82
+ groups(
83
+ # no group
84
+ )
85
+
86
+ policy "s3-readonly" do
87
+ {"Version"=>"2012-10-17",
88
+ "Statement"=>
89
+ [{"Action"=>
90
+ ["s3:Get*",
91
+ "s3:List*"],
92
+ "Effect"=>"Allow",
93
+ "Resource"=>"*"}]}
94
+ end
95
+
96
+ policy "route53-readonly" do
97
+ {"Version"=>"2012-10-17",
98
+ "Statement"=>
99
+ [{"Action"=>
100
+ ["route53:Get*",
101
+ "route53:List*"],
102
+ "Effect"=>"Allow",
103
+ "Resource"=>"*"}]}
104
+ end
105
+ end
106
+
107
+ group "Admin", path: "/admin/" do
108
+ policy "Admin" do
109
+ {"Statement"=>[{"Effect"=>"Allow", "Action"=>"*", "Resource"=>"*"}]}
110
+ end
111
+ end
112
+ ```
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("#{File.dirname __FILE__}/../lib")
3
+ require 'rubygems'
4
+ require 'miam'
5
+ require 'optparse'
6
+
7
+ Version = Miam::VERSION
8
+ DEFAULT_FILENAME = 'IAMfile'
9
+
10
+ mode = nil
11
+ file = DEFAULT_FILENAME
12
+ output_file = '-'
13
+ account_output = 'account.csv'
14
+ split = false
15
+
16
+ options = {
17
+ :dry_run => false,
18
+ :color => true,
19
+ :debug => false,
20
+ }
21
+
22
+ options[:password_manager] = Miam::PasswordManager.new(account_output, options)
23
+
24
+ ARGV.options do |opt|
25
+ begin
26
+ access_key = nil
27
+ secret_key = nil
28
+ region = nil
29
+ profile_name = nil
30
+ credentials_path = nil
31
+
32
+ opt.on('-p', '--profile PROFILE_NAME') {|v| profile_name = v }
33
+ opt.on('' , '--credentials-path PATH') {|v| credentials_path = v }
34
+ opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
35
+ opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
36
+ opt.on('-r', '--region REGION') {|v| region = v }
37
+ opt.on('-a', '--apply') { mode = :apply }
38
+ opt.on('-f', '--file FILE') {|v| file = v }
39
+ opt.on('', '--dry-run') { options[:dry_run] = true }
40
+ opt.on('' , '--account-output FILE') {|v| options[:password_manager] = Miam::PasswordManager.new(v, options) }
41
+ opt.on('' , '--split') { split = true }
42
+ opt.on('-e', '--export') { mode = :export }
43
+ opt.on('-o', '--output FILE') {|v| output_file = v }
44
+ opt.on('' , '--split') { split = true }
45
+ opt.on('' , '--no-color') { options[:color] = false }
46
+ opt.on('' , '--no-progress') { options[:no_progress] = true }
47
+ opt.on('' , '--debug') { options[:debug] = true }
48
+ opt.parse!
49
+
50
+ aws_opts = {}
51
+
52
+ if access_key and secret_key
53
+ aws_opts.update(
54
+ :access_key_id => access_key,
55
+ :secret_access_key => secret_key
56
+ )
57
+ elsif profile_name or credentials_path
58
+ credentials_opts = {}
59
+ credentials_opts[:profile_name] = profile_name if profile_name
60
+ credentials_opts[:path] = credentials_path if credentials_path
61
+ credentials = Aws::SharedCredentials.new(credentials_opts)
62
+ aws_opts[:credentials] = credentials
63
+ elsif (access_key and !secret_key) or (!access_key and secret_key) or mode.nil?
64
+ puts opt.help
65
+ exit 1
66
+ end
67
+
68
+ aws_opts[:region] = region if region
69
+ Aws.config.update(aws_opts)
70
+ rescue => e
71
+ $stderr.puts("[ERROR] #{e.message}")
72
+ exit 1
73
+ end
74
+ end
75
+
76
+ String.colorize = options[:color]
77
+
78
+ if options[:debug]
79
+ Aws.config.update(
80
+ :http_wire_trace => true,
81
+ :logger => Miam::Logger.instance
82
+ )
83
+ end
84
+
85
+ begin
86
+ logger = Miam::Logger.instance
87
+ logger.set_debug(options[:debug])
88
+ client = Miam::Client.new(options)
89
+
90
+ case mode
91
+ when :export
92
+ if split
93
+ logger.info('Export IAM')
94
+ output_file = DEFAULT_FILENAME if output_file == '-'
95
+ requires = []
96
+
97
+ client.export do |users_or_groups, dsl|
98
+ iam_file = File.join(File.dirname(output_file), "#{users_or_groups}.iam")
99
+ requires << iam_file
100
+ logger.info(" write `#{iam_file}`")
101
+
102
+ open(iam_file, 'wb') do |f|
103
+ f.puts dsl
104
+ end
105
+ end
106
+
107
+ logger.info(" write `#{output_file}`")
108
+
109
+ open(output_file, 'wb') do |f|
110
+ requires.each do |iam_file|
111
+ f.puts "require '#{File.basename iam_file}'"
112
+ end
113
+ end
114
+ else
115
+ if output_file == '-'
116
+ logger.info('# Export IAM')
117
+ puts client.export
118
+ else
119
+ logger.info("Export IAM to `#{output_file}`")
120
+ open(output_file, 'wb') {|f| f.puts client.export }
121
+ end
122
+ end
123
+ when :apply
124
+ unless File.exist?(file)
125
+ raise "No IAMfile found (looking for: #{file})"
126
+ end
127
+
128
+ msg = "Apply `#{file}` to IAM"
129
+ msg << ' (dry-run)' if options[:dry_run]
130
+ logger.info(msg)
131
+
132
+ updated = client.apply(file)
133
+
134
+ logger.info('No change'.intense_blue) unless updated
135
+ end
136
+ rescue => e
137
+ if options[:debug]
138
+ raise e
139
+ else
140
+ $stderr.puts("[ERROR] #{e.message}".red)
141
+ exit 1
142
+ end
143
+ end
@@ -0,0 +1,24 @@
1
+ require 'cgi'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'pp'
5
+ require 'singleton'
6
+
7
+ require 'aws-sdk-core'
8
+ require 'ruby-progressbar'
9
+ require 'term/ansicolor'
10
+
11
+ module Miam; end
12
+ require 'miam/logger'
13
+ require 'miam/client'
14
+ require 'miam/driver'
15
+ require 'miam/dsl'
16
+ require 'miam/dsl/context'
17
+ require 'miam/dsl/context/group'
18
+ require 'miam/dsl/context/user'
19
+ require 'miam/dsl/converter'
20
+ require 'miam/exporter'
21
+ require 'miam/ext/string_ext'
22
+ require 'miam/password_manager'
23
+ require 'miam/utils'
24
+ require 'miam/version'
@@ -0,0 +1,268 @@
1
+ class Miam::Client
2
+ def initialize(options = {})
3
+ @options = options
4
+ aws_config = options.delete(:aws_config) || {}
5
+ @iam = Aws::IAM::Client.new(aws_config)
6
+ @driver = Miam::Driver.new(@iam, options)
7
+ @password_manager = options[:password_manager] || Miam::PasswordManager.new('-', options)
8
+ end
9
+
10
+ def export
11
+ exported, group_users = Miam::Exporter.export(@iam, @options) do |export_options|
12
+ progress(*export_options.values_at(:progress_total, :progress))
13
+ end
14
+
15
+ if block_given?
16
+ [:users, :groups].each do |users_or_groups|
17
+ splitted = {:users => {}, :groups => {}}
18
+ splitted[users_or_groups] = exported[users_or_groups]
19
+ yield(users_or_groups, Miam::DSL.convert(splitted, @options).strip)
20
+ end
21
+ else
22
+ Miam::DSL.convert(exported, @options)
23
+ end
24
+ end
25
+
26
+ def apply(file)
27
+ walk(file)
28
+ end
29
+
30
+ private
31
+
32
+ def walk(file)
33
+ expected = load_file(file)
34
+
35
+ actual, group_users = Miam::Exporter.export(@iam, @options) do |export_options|
36
+ progress(*export_options.values_at(:progress_total, :progress))
37
+ end
38
+
39
+ updated = walk_groups(expected[:groups], actual[:groups], actual[:users], group_users)
40
+ updated = walk_users(expected[:users], actual[:users], group_users) || updated
41
+
42
+ if @options[:dry_run]
43
+ false
44
+ else
45
+ updated
46
+ end
47
+ end
48
+
49
+ def walk_users(expected, actual, group_users)
50
+ updated = scan_rename(:user, expected, actual, group_users)
51
+
52
+ expected.each do |user_name, expected_attrs|
53
+ actual_attrs = actual.delete(user_name)
54
+
55
+ if actual_attrs
56
+ updated = walk_user(user_name, expected_attrs, actual_attrs) || updated
57
+ else
58
+ actual_attrs = @driver.create_user(user_name, expected_attrs)
59
+ access_key = @driver.create_access_key(user_name)
60
+
61
+ if access_key
62
+ @password_manager.puts_password(user_name, access_key[:access_key_id], access_key[:secret_access_key])
63
+ end
64
+
65
+ walk_user(user_name, expected_attrs, actual_attrs)
66
+ updated = true
67
+ end
68
+ end
69
+
70
+ actual.each do |user_name, attrs|
71
+ @driver.delete_user(user_name, attrs)
72
+
73
+ group_users.each do |group_name, users|
74
+ users.delete(user_name)
75
+ end
76
+
77
+ updated = true
78
+ end
79
+
80
+ updated
81
+ end
82
+
83
+ def walk_user(user_name, expected_attrs, actual_attrs)
84
+ updated = walk_login_profile(user_name, expected_attrs[:login_profile], actual_attrs[:login_profile])
85
+ updated = walk_user_groups(user_name, expected_attrs[:groups], actual_attrs[:groups]) || updated
86
+ walk_policies(:user, user_name, expected_attrs[:policies], actual_attrs[:policies])
87
+ end
88
+
89
+ def walk_login_profile(user_name, expected_login_profile, actual_login_profile)
90
+ updated = false
91
+
92
+ [expected_login_profile, actual_login_profile].each do |login_profile|
93
+ if login_profile and not login_profile.has_key?(:password_reset_required)
94
+ login_profile[:password_reset_required] = false
95
+ end
96
+ end
97
+
98
+ if expected_login_profile and not actual_login_profile
99
+ expected_login_profile[:password] ||= @password_manager.identify(user_name, :login_profile)
100
+ @driver.create_login_profile(user_name, expected_login_profile)
101
+ updated = true
102
+ elsif not expected_login_profile and actual_login_profile
103
+ @driver.delete_login_profile(user_name)
104
+ updated = true
105
+ elsif expected_login_profile != actual_login_profile
106
+ @driver.update_login_profile(user_name, expected_login_profile)
107
+ updated = true
108
+ end
109
+
110
+ updated
111
+ end
112
+
113
+ def walk_user_groups(user_name, expected_groups, actual_groups)
114
+ expected_groups = expected_groups.sort
115
+ actual_groups = actual_groups.sort
116
+ updated = false
117
+
118
+ if expected_groups != actual_groups
119
+ add_groups = expected_groups - actual_groups
120
+ remove_groups = actual_groups - expected_groups
121
+
122
+ unless add_groups.empty?
123
+ @driver.add_user_to_groups(user_name, add_groups)
124
+ end
125
+
126
+ unless remove_groups.empty?
127
+ @driver.remove_user_from_groups(user_name, remove_groups)
128
+ end
129
+
130
+ updated = true
131
+ end
132
+
133
+ updated
134
+ end
135
+
136
+ def walk_groups(expected, actual, actual_users, group_users)
137
+ updated = scan_rename(:group, expected, actual, group_users)
138
+
139
+ expected.each do |group_name, expected_attrs|
140
+ actual_attrs = actual.delete(group_name)
141
+
142
+ if actual_attrs
143
+ updated = walk_path(:group, group_name, expected_attrs[:path], actual_attrs[:path]) || updated
144
+ updated = walk_group(group_name, expected_attrs, actual_attrs) || updated
145
+ else
146
+ actual_attrs = @driver.create_group(group_name, expected_attrs)
147
+ walk_group(group_name, expected_attrs, actual_attrs)
148
+ updated = true
149
+ end
150
+ end
151
+
152
+ actual.each do |group_name, attrs|
153
+ users_in_group = group_users.delete(group_name) || []
154
+ @driver.delete_group(group_name, attrs, users_in_group)
155
+
156
+ actual_users.each do |user_name, user_attrs|
157
+ user_attrs[:groups].delete(group_name)
158
+ end
159
+
160
+ updated = true
161
+ end
162
+
163
+ updated
164
+ end
165
+
166
+ def walk_group(group_name, expected_attrs, actual_attrs)
167
+ walk_policies(:group, group_name, expected_attrs[:policies], actual_attrs[:policies])
168
+ end
169
+
170
+ def scan_rename(type, expected, actual, group_users)
171
+ updated = false
172
+
173
+ expected.each do |name, expected_attrs|
174
+ renamed_from = expected_attrs[:renamed_from]
175
+ next unless renamed_from
176
+
177
+ actual_attrs = actual.delete(renamed_from)
178
+ next unless actual_attrs
179
+
180
+ @driver.update_name(type, renamed_from, name)
181
+ actual[name] = actual_attrs
182
+
183
+ case type
184
+ when :user
185
+ group_users.each do |group_name, users|
186
+ users.each do |user_name|
187
+ if user_name == renamed_from
188
+ user_name.replace(name)
189
+ end
190
+ end
191
+ end
192
+ when :group
193
+ users = group_users.delete(renamed_from)
194
+ group_users[name] = users if users
195
+ end
196
+
197
+ updated = true
198
+ end
199
+
200
+ updated
201
+ end
202
+
203
+ def walk_path(type, user_or_group_name, expected_path, actual_path)
204
+ updated = false
205
+
206
+ if expected_path != actual_path
207
+ @driver.update_path(type, user_or_group_name, expected_path)
208
+ updated = true
209
+ end
210
+
211
+ updated
212
+ end
213
+
214
+ def walk_policies(type, user_or_group_name, expected_policies, actual_policies)
215
+ updated = false
216
+
217
+ expected_policies.each do |policy_name, expected_document|
218
+ actual_document = actual_policies.delete(policy_name)
219
+
220
+ if actual_document
221
+ updated = walk_policy(type, user_or_group_name, policy_name, expected_document, actual_document) || updated
222
+ else
223
+ @driver.create_policy(type, user_or_group_name, policy_name, expected_document)
224
+ updated = true
225
+ end
226
+ end
227
+
228
+ actual_policies.each do |policy_name, document|
229
+ @driver.delete_policy(type, user_or_group_name, policy_name)
230
+ updated = true
231
+ end
232
+
233
+ updated
234
+ end
235
+
236
+ def walk_policy(type, user_or_group_name, policy_name, expected_document, actual_document)
237
+ updated = false
238
+
239
+ if expected_document != actual_document
240
+ @driver.update_policy(type, user_or_group_name, policy_name, expected_document)
241
+ updated = true
242
+ end
243
+
244
+ updated
245
+ end
246
+
247
+ def load_file(file)
248
+ if file.kind_of?(String)
249
+ open(file) do |f|
250
+ Miam::DSL.parse(f.read, file)
251
+ end
252
+ elsif file.respond_to?(:read)
253
+ Miam::DSL.parse(file.read, file.path)
254
+ else
255
+ raise TypeError, "can't convert #{file} into File"
256
+ end
257
+ end
258
+
259
+ def progress(total, n)
260
+ return if @options[:no_progress]
261
+
262
+ unless @progressbar
263
+ @progressbar = ProgressBar.create(:title => "Loading", :total => total, :output => $stderr)
264
+ end
265
+
266
+ @progressbar.progress = n
267
+ end
268
+ end