miam 0.1.0.beta

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
+ 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