gratan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +86 -0
  8. data/Rakefile +5 -0
  9. data/bin/gratan +132 -0
  10. data/gratan.gemspec +28 -0
  11. data/lib/gratan/client.rb +211 -0
  12. data/lib/gratan/driver.rb +166 -0
  13. data/lib/gratan/dsl/context/on.rb +19 -0
  14. data/lib/gratan/dsl/context/user.rb +25 -0
  15. data/lib/gratan/dsl/context.rb +57 -0
  16. data/lib/gratan/dsl/converter.rb +74 -0
  17. data/lib/gratan/dsl/validator.rb +13 -0
  18. data/lib/gratan/dsl.rb +9 -0
  19. data/lib/gratan/exporter.rb +49 -0
  20. data/lib/gratan/ext/string_ext.rb +25 -0
  21. data/lib/gratan/grant_parser.rb +68 -0
  22. data/lib/gratan/identifier/auto.rb +28 -0
  23. data/lib/gratan/identifier/csv.rb +25 -0
  24. data/lib/gratan/identifier/null.rb +5 -0
  25. data/lib/gratan/identifier.rb +2 -0
  26. data/lib/gratan/logger.rb +28 -0
  27. data/lib/gratan/version.rb +3 -0
  28. data/lib/gratan.rb +24 -0
  29. data/spec/change/change_grants_2_spec.rb +154 -0
  30. data/spec/change/change_grants_3_spec.rb +164 -0
  31. data/spec/change/change_grants_4_spec.rb +37 -0
  32. data/spec/change/change_grants_spec.rb +209 -0
  33. data/spec/create/create_user_2_spec.rb +139 -0
  34. data/spec/create/create_user_3_spec.rb +115 -0
  35. data/spec/create/create_user_spec.rb +194 -0
  36. data/spec/drop/drop_user_2_spec.rb +77 -0
  37. data/spec/drop/drop_user_spec.rb +67 -0
  38. data/spec/drop/expire_user_spec.rb +179 -0
  39. data/spec/export/export_spec.rb +119 -0
  40. data/spec/misc/misc_spec.rb +74 -0
  41. data/spec/misc/require_spec.rb +77 -0
  42. data/spec/spec_helper.rb +118 -0
  43. metadata +198 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a970e74897bbcae2cf33d36cecf6b94950b7a55
4
+ data.tar.gz: fd04cfd92a0f5fa94c98aca24fc592dc53fc38ac
5
+ SHA512:
6
+ metadata.gz: 9b5b1868f6322b38a90bc7fb771f947e090c60d9a63c6519ec5a9e66e4e07f832b495200b00091ef366949524e16bbf207251c9a21794dde729ca1104d5816fc
7
+ data.tar.gz: 621bf66ba1e3709a247b23bc648029b858f8be33c5e278763c91dda34274c68fe4959638e3cfdcb85c5625a378a4530d3bcc7ec2b2a8e4f97a807f835e3c83f6
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
+ Grantfile
17
+ *.grant
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ script:
5
+ - bundle install
6
+ - bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gratan.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -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.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Gratan
2
+
3
+ Gratan is a tool to manage MySQL permissions.
4
+
5
+ It defines the state of MySQL permissions using Ruby DSL, and updates permissions according to DSL.
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/gratan.png)](http://badge.fury.io/rb/gratan)
8
+ [![Build Status](https://travis-ci.org/winebarrel/gratan.svg?branch=master)](https://travis-ci.org/winebarrel/gratan)
9
+ [![Coverage Status](https://coveralls.io/repos/winebarrel/gratan/badge.png?branch=master)](https://coveralls.io/r/winebarrel/gratan?branch=master)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'gratan'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install gratan
26
+
27
+ ## Usage
28
+
29
+ ```sh
30
+ gratan -e -o Grantfile
31
+ vi Grantfile
32
+ gratan -a --dry-run
33
+ gratan -a
34
+ ```
35
+
36
+ ## Help
37
+
38
+ ```sh
39
+ Usage: gratan [options]
40
+ --host HOST
41
+ --port PORT
42
+ --socket SOCKET
43
+ --username USERNAME
44
+ --password PASSWORD
45
+ --database DATABASE
46
+ -a, --apply
47
+ -f, --file FILE
48
+ --dry-run
49
+ -e, --export
50
+ --with-identifier
51
+ --enable-expired
52
+ --split
53
+ -o, --output FILE
54
+ --ignore-user REGEXP
55
+ --no-color
56
+ --debug
57
+ --auto-identify OUTPUT
58
+ --csv-identify CSV
59
+ -h, --help
60
+ ```
61
+
62
+ ## Grantfile example
63
+
64
+ ```ruby
65
+ require 'other/grantfile'
66
+
67
+ user "scott", "%" do
68
+ on "*.*" do
69
+ grant "USAGE"
70
+ end
71
+
72
+ on "test.*" do
73
+ grant "SELECT"
74
+ end
75
+ end
76
+
77
+ user "scott", "localhost", expired: '2014/10/10' do
78
+ on "*.*" do
79
+ grant "USAGE"
80
+ end
81
+
82
+ on "test.*" do
83
+ grant "SELECT"
84
+ end
85
+ end
86
+ ```
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :default => :spec
data/bin/gratan ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("#{File.dirname __FILE__}/../lib")
3
+ require 'rubygems'
4
+ require 'gratan'
5
+ require 'optparse'
6
+ require 'json'
7
+
8
+ Version = Gratan::VERSION
9
+ DEFAULT_FILENAME = 'Grantfile'
10
+
11
+ mode = nil
12
+ file = DEFAULT_FILENAME
13
+ output_file = '-'
14
+ split = false
15
+
16
+ mysql_options = {
17
+ :host => 'localhost',
18
+ :username => 'root',
19
+ }
20
+
21
+ options = {
22
+ :dry_run => false,
23
+ :color => true,
24
+ :debug => false,
25
+ }
26
+
27
+ ARGV.options do |opt|
28
+ begin
29
+ opt.on('' , '--host HOST') {|v| mysql_options[:host] = v }
30
+ opt.on('' , '--port PORT', Integer) {|v| mysql_options[:port] = v }
31
+ opt.on('' , '--socket SOCKET') {|v| mysql_options[:socket] = v }
32
+ opt.on('' , '--username USERNAME') {|v| mysql_options[:username] = v }
33
+ opt.on('' , '--password PASSWORD') {|v| mysql_options[:password] = v }
34
+ opt.on('' , '--database DATABASE') {|v| mysql_options[:database] = v }
35
+ opt.on('-a', '--apply') { mode = :apply }
36
+ opt.on('-f', '--file FILE') {|v| file = v }
37
+ opt.on('' , '--dry-run') { options[:dry_run] = true }
38
+ opt.on('-e', '--export') { mode = :export }
39
+ opt.on('' , '--with-identifier') { options[:with_identifier] = true }
40
+ opt.on('' , '--enable-expired') { options[:enable_expired] = true }
41
+ opt.on('' , '--split') { split = true }
42
+ opt.on('-o', '--output FILE') {|v| output_file = v }
43
+ opt.on('' , '--ignore-user REGEXP') {|v| options[:ignore_user] = Regexp.new(v) }
44
+ opt.on('' , '--no-color') { options[:color] = false }
45
+ opt.on('' , '--debug') { options[:debug] = true }
46
+
47
+ opt.on('' , '--auto-identify OUTPUT') {|v| options[:identifier] = Gratan::Identifier::Auto.new(v, options) }
48
+ opt.on('' , '--csv-identify CSV') {|v| options[:identifier] = Gratan::Identifier::CSV.new(v, options) }
49
+
50
+ opt.on('-h', '--help') do
51
+ puts opt.help
52
+ exit 1
53
+ end
54
+
55
+ opt.parse!
56
+
57
+ unless mode
58
+ puts opt.help
59
+ exit 1
60
+ end
61
+ rescue => e
62
+ $stderr.puts("[ERROR] #{e.message}")
63
+ exit 1
64
+ end
65
+ end
66
+
67
+ options.update(mysql_options)
68
+ String.colorize = options[:color]
69
+
70
+ begin
71
+ logger = Gratan::Logger.instance
72
+ logger.set_debug(options[:debug])
73
+ client = Gratan::Client.new(options)
74
+
75
+ case mode
76
+ when :export
77
+ if split
78
+ logger.info('Export Grants')
79
+ output_file = DEFAULT_FILENAME if output_file == '-'
80
+ requires = []
81
+
82
+ client.export do |user, dsl|
83
+ grant_file = File.join(File.dirname(output_file), "#{user}.grant")
84
+ requires << grant_file
85
+ logger.info(" write `#{grant_file}`")
86
+
87
+ open(grant_file, 'wb') do |f|
88
+ f.puts dsl
89
+ end
90
+ end
91
+
92
+ logger.info(" write `#{output_file}`")
93
+
94
+ open(output_file, 'wb') do |f|
95
+ requires.each do |grant_file|
96
+ f.puts "require '#{File.basename grant_file}'"
97
+ end
98
+ end
99
+ else
100
+ if output_file == '-'
101
+ logger.info('# Export Grants')
102
+ puts client.export(options)
103
+ else
104
+ logger.info("Export Grants to `#{output_file}`")
105
+ open(output_file, 'wb') {|f| f.puts client.export(options) }
106
+ end
107
+ end
108
+ when :apply
109
+ unless File.exist?(file)
110
+ raise "No Grantfile found (looking for: #{file})"
111
+ end
112
+
113
+ mysql_info = mysql_options.dup
114
+ mysql_info.delete(:password)
115
+ mysql_info = JSON.dump(mysql_info)
116
+
117
+ msg = "Apply `#{file}` to #{mysql_info}"
118
+ msg << ' (dry-run)' if options[:dry_run]
119
+ logger.info(msg)
120
+
121
+ updated = client.apply(file)
122
+
123
+ logger.info('No change'.intense_blue) unless updated
124
+ end
125
+ rescue => e
126
+ if options[:debug]
127
+ raise e
128
+ else
129
+ $stderr.puts("[ERROR] #{e.message}".red)
130
+ exit 1
131
+ end
132
+ end
data/gratan.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gratan/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'gratan'
8
+ spec.version = Gratan::VERSION
9
+ spec.authors = ['Genki Sugawara']
10
+ spec.email = ['sgwr_dts@yahoo.co.jp']
11
+ spec.summary = %q{Gratan is a tool to manage MySQL permissions using Ruby DSL.}
12
+ spec.description = %q{Gratan is a tool to manage MySQL permissions using Ruby DSL.}
13
+ spec.homepage = 'https://github.com/winebarrel/gratan'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |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 'mysql2'
22
+ spec.add_dependency "term-ansicolor"
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '>= 3.0.0'
26
+ spec.add_development_dependency 'timecop'
27
+ spec.add_development_dependency 'coveralls'
28
+ end
@@ -0,0 +1,211 @@
1
+ class Gratan::Client
2
+ def initialize(options = {})
3
+ @options = options
4
+ @options[:identifier] ||= Gratan::Identifier::Null.new
5
+ client = Mysql2::Client.new(options)
6
+ @driver = Gratan::Driver.new(client, options)
7
+ end
8
+
9
+ def export(options = {})
10
+ options = @options.merge(options)
11
+ exported = Gratan::Exporter.export(@driver, options)
12
+
13
+ if block_given?
14
+ exported.sort_by {|user_host, attrs|
15
+ user_host
16
+ }.chunk {|user_host, attrs|
17
+ user_host[0].empty? ? 'root' : user_host[0]
18
+ }.each {|user, grants|
19
+ h = {}
20
+ grants.each {|k, v| h[k] = v }
21
+ dsl = Gratan::DSL.convert(h, options)
22
+ yield(user, dsl)
23
+ }
24
+ else
25
+ exported = Gratan::Exporter.export(@driver, options)
26
+ Gratan::DSL.convert(exported, options)
27
+ end
28
+ end
29
+
30
+ def apply(file, options = {})
31
+ options = @options.merge(options)
32
+
33
+ in_progress do
34
+ walk(file, options)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def walk(file, options)
41
+ expected = load_file(file, options)
42
+ actual = Gratan::Exporter.export(@driver, options.merge(:with_identifier => true))
43
+
44
+ expected.each do |user_host, expected_attrs|
45
+ next if user_host[0] =~ options[:ignore_user]
46
+ actual_attrs = actual.delete(user_host)
47
+
48
+ if actual_attrs
49
+ walk_user(*user_host, expected_attrs, actual_attrs)
50
+ else
51
+ create_user(*user_host, expected_attrs)
52
+ end
53
+ end
54
+
55
+ actual.each do |user_host, attrs|
56
+ next if user_host[0] =~ options[:ignore_user]
57
+ drop_user(*user_host)
58
+ end
59
+ end
60
+
61
+ def create_user(user, host, attrs)
62
+ attrs[:options] ||= {}
63
+
64
+ unless attrs.has_key?(:identified)
65
+ identified = @options[:identifier].identify(user, host)
66
+ attrs[:options][:identified] = identified if identified
67
+ end
68
+
69
+ @driver.create_user(user, host, attrs)
70
+ update!
71
+ end
72
+
73
+ def drop_user(user, host)
74
+ @driver.drop_user(user, host)
75
+ update!
76
+ end
77
+
78
+ def walk_user(user, host, expected_attrs, actual_attrs)
79
+ walk_options(user, host, expected_attrs[:options], actual_attrs[:options])
80
+ walk_objects(user, host, expected_attrs[:objects], actual_attrs[:objects])
81
+ end
82
+
83
+ def walk_options(user, host, expected_options, actual_options)
84
+ if expected_options.has_key?(:identified)
85
+ walk_identified(user, host, expected_options[:identified], actual_options[:identified])
86
+ end
87
+
88
+ walk_required(user, host, expected_options[:required], actual_options[:required])
89
+ end
90
+
91
+ def walk_identified(user, host, expected_identified, actual_identified)
92
+ if expected_identified != actual_identified
93
+ @driver.identify(user, host, expected_identified)
94
+ update!
95
+ end
96
+ end
97
+
98
+ def walk_required(user, host, expected_required, actual_required)
99
+ if expected_required != actual_required
100
+ @driver.set_require(user, host, expected_required)
101
+ update!
102
+ end
103
+ end
104
+
105
+ def walk_objects(user, host, expected_objects, actual_objects)
106
+ expected_objects.each do |object, expected_options|
107
+ expected_options ||= {}
108
+ actual_options = actual_objects.delete(object)
109
+
110
+ if actual_options
111
+ walk_object(user, host, object, expected_options, actual_options)
112
+ else
113
+ @driver.grant(user, host, object, expected_options)
114
+ update!
115
+ end
116
+ end
117
+
118
+ actual_objects.each do |object, options|
119
+ options ||= {}
120
+ @driver.revoke(user, host, object, options)
121
+ update!
122
+ end
123
+ end
124
+
125
+ def walk_object(user, host, object, expected_options, actual_options)
126
+ walk_with_option(user, host, object, expected_options[:with], actual_options[:with])
127
+ walk_privs(user, host, object, expected_options[:privs], actual_options[:privs])
128
+ end
129
+
130
+ def walk_with_option(user, host, object, expected_with_option, actual_with_option)
131
+ expected_with_option = (expected_with_option || '').upcase
132
+ actual_with_option = (actual_with_option || '').upcase
133
+
134
+ if expected_with_option != actual_with_option
135
+ @driver.update_with_option(user, host, object, expected_with_option)
136
+ update!
137
+ end
138
+ end
139
+
140
+ def walk_privs(user, host, object, expected_privs, actual_privs)
141
+ expected_privs = normalize_privs(expected_privs)
142
+ actual_privs = normalize_privs(actual_privs)
143
+
144
+ revoke_privs = actual_privs - expected_privs
145
+ grant_privs = expected_privs - actual_privs
146
+
147
+ unless revoke_privs.empty?
148
+ if revoke_privs.length == 1 and revoke_privs[0] == 'USAGE' and not grant_privs.empty?
149
+ # nothing to do
150
+ else
151
+ @driver.revoke(user, host, object, :privs => revoke_privs)
152
+ update!
153
+ end
154
+ end
155
+
156
+ unless grant_privs.empty?
157
+ @driver.grant(user, host, object, :privs => grant_privs)
158
+ update!
159
+ end
160
+ end
161
+
162
+ def normalize_privs(privs)
163
+ privs.map do |priv|
164
+ priv = priv.split('(', 2)
165
+ priv[0].upcase!
166
+
167
+ if priv[1]
168
+ priv[1] = priv[1].split(',').map {|i| i.gsub(')', '').strip }.sort.join(', ')
169
+ priv[1] << ')'
170
+ end
171
+
172
+ priv = priv.join('(')
173
+
174
+ if priv == 'ALL'
175
+ priv = 'ALL PRIVILEGES'
176
+ end
177
+
178
+ priv
179
+ end
180
+ end
181
+
182
+ def load_file(file, options)
183
+ if file.kind_of?(String)
184
+ open(file) do |f|
185
+ Gratan::DSL.parse(f.read, file, options)
186
+ end
187
+ elsif file.respond_to?(:read)
188
+ Gratan::DSL.parse(file.read, file.path, options)
189
+ else
190
+ raise TypeError, "can't convert #{file} into File"
191
+ end
192
+ end
193
+
194
+ def in_progress
195
+ updated = false
196
+
197
+ begin
198
+ @updated = false
199
+ yield
200
+ updated = @updated
201
+ ensure
202
+ @updated = nil
203
+ end
204
+
205
+ updated
206
+ end
207
+
208
+ def update!
209
+ @updated = true unless @options[:dry_run]
210
+ end
211
+ end
@@ -0,0 +1,166 @@
1
+ class Gratan::Driver
2
+ include Gratan::Logger::Helper
3
+
4
+ def initialize(client, options = {})
5
+ @client = client
6
+ @options = options
7
+ end
8
+
9
+ def each_user
10
+ read('SELECT user, host FROM mysql.user').each do |row|
11
+ yield(row['user'], row['host'])
12
+ end
13
+ end
14
+
15
+ def show_grants(user, host)
16
+ read("SHOW GRANTS FOR #{quote_user(user, host)}").each do |row|
17
+ yield(row.values.first)
18
+ end
19
+ end
20
+
21
+ def create_user(user, host, options = {})
22
+ objects = options[:objects]
23
+ grant_options = options[:options]
24
+
25
+ objects.each do |object, object_options|
26
+ grant(user, host, object, grant_options.merge(object_options))
27
+ end
28
+ end
29
+
30
+ def drop_user(user, host)
31
+ sql = "DROP USER #{quote_user(user, host)}"
32
+ delete(sql)
33
+ end
34
+
35
+ def grant(user, host, object, options)
36
+ privs = options.fetch(:privs)
37
+ identified = options[:identified]
38
+ required = options[:required]
39
+ with_option = options[:with]
40
+
41
+ sql = 'GRANT %s ON %s TO %s' % [
42
+ privs.join(', '),
43
+ quote_object(object),
44
+ quote_user(user, host),
45
+ ]
46
+
47
+ sql << " IDENTIFIED BY #{quote_identifier(identified)}" if identified
48
+ sql << " REQUIRE #{required}" if required
49
+ sql << " WITH #{with_option}" if with_option
50
+
51
+ update(sql)
52
+ end
53
+
54
+ def identify(user, host, identifier)
55
+ sql = 'GRANT USAGE ON *.* TO %s IDENTIFIED BY %s' % [
56
+ quote_user(user, host),
57
+ quote_identifier(identifier),
58
+ ]
59
+
60
+ update(sql)
61
+ end
62
+
63
+ def set_require(user, host, required)
64
+ required ||= 'NONE'
65
+
66
+ sql = 'GRANT USAGE ON *.* TO %s REQUIRE %s' % [
67
+ quote_user(user, host),
68
+ required
69
+ ]
70
+
71
+ update(sql)
72
+ end
73
+
74
+ def revoke(user, host, object, options = {})
75
+ privs = options.fetch(:privs)
76
+ with_option = options[:with]
77
+
78
+ if with_option =~ /\bGRANT\s+OPTION\b/i
79
+ revoke0(user, host, object, ['GRANT OPTION'])
80
+
81
+ if privs.length == 1 and privs[0] =~ /\AUSAGE\z/i
82
+ return
83
+ end
84
+ end
85
+
86
+ revoke0(user, host, object, privs)
87
+ end
88
+
89
+ def revoke0(user, host, object, privs)
90
+ sql = 'REVOKE %s ON %s FROM %s' % [
91
+ privs.join(', '),
92
+ quote_object(object),
93
+ quote_user(user, host),
94
+ ]
95
+
96
+ delete(sql)
97
+ end
98
+
99
+ def update_with_option(user, host, object, with_option)
100
+ options = []
101
+
102
+ if with_option =~ /\bGRANT\s+OPTION\b/i
103
+ options << 'GRANT OPTION'
104
+ else
105
+ revoke(user, host, object, :privs => ['GRANT OPTION'])
106
+ end
107
+
108
+ %w(
109
+ MAX_QUERIES_PER_HOUR
110
+ MAX_UPDATES_PER_HOUR
111
+ MAX_CONNECTIONS_PER_HOUR
112
+ MAX_USER_CONNECTIONS
113
+ ).each do |name|
114
+ count = 0
115
+
116
+ if with_option =~ /\b#{name}\s+(\d+)\b/i
117
+ count = $1
118
+ end
119
+
120
+ options << [name, count].join(' ')
121
+ end
122
+
123
+ unless options.empty?
124
+ grant(user, host, object, :privs => ['USAGE'], :with => options.join(' '))
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def read(sql)
131
+ log(:debug, sql)
132
+ @client.query(sql)
133
+ end
134
+
135
+ def update(sql)
136
+ log(:info, sql, :green)
137
+ @client.query(sql) unless @options[:dry_run]
138
+ end
139
+
140
+ def delete(sql)
141
+ log(:info, sql, :red)
142
+ @client.query(sql) unless @options[:dry_run]
143
+ end
144
+
145
+ def escape(str)
146
+ @client.escape(str)
147
+ end
148
+
149
+ def quote_user(user, host)
150
+ "'%s'@'%s'" % [escape(user), escape(host)]
151
+ end
152
+
153
+ def quote_object(object)
154
+ object.split('.', 2).map {|i| i == '*' ? i : "`#{i}`" }.join('.')
155
+ end
156
+
157
+ def quote_identifier(identifier)
158
+ identifier ||= ''
159
+
160
+ unless identifier =~ /\APASSWORD\s+'.+'\z/
161
+ identifier = "'#{escape(identifier)}'"
162
+ end
163
+
164
+ identifier
165
+ end
166
+ end