gratan 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.
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