gratan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +5 -0
- data/bin/gratan +132 -0
- data/gratan.gemspec +28 -0
- data/lib/gratan/client.rb +211 -0
- data/lib/gratan/driver.rb +166 -0
- data/lib/gratan/dsl/context/on.rb +19 -0
- data/lib/gratan/dsl/context/user.rb +25 -0
- data/lib/gratan/dsl/context.rb +57 -0
- data/lib/gratan/dsl/converter.rb +74 -0
- data/lib/gratan/dsl/validator.rb +13 -0
- data/lib/gratan/dsl.rb +9 -0
- data/lib/gratan/exporter.rb +49 -0
- data/lib/gratan/ext/string_ext.rb +25 -0
- data/lib/gratan/grant_parser.rb +68 -0
- data/lib/gratan/identifier/auto.rb +28 -0
- data/lib/gratan/identifier/csv.rb +25 -0
- data/lib/gratan/identifier/null.rb +5 -0
- data/lib/gratan/identifier.rb +2 -0
- data/lib/gratan/logger.rb +28 -0
- data/lib/gratan/version.rb +3 -0
- data/lib/gratan.rb +24 -0
- data/spec/change/change_grants_2_spec.rb +154 -0
- data/spec/change/change_grants_3_spec.rb +164 -0
- data/spec/change/change_grants_4_spec.rb +37 -0
- data/spec/change/change_grants_spec.rb +209 -0
- data/spec/create/create_user_2_spec.rb +139 -0
- data/spec/create/create_user_3_spec.rb +115 -0
- data/spec/create/create_user_spec.rb +194 -0
- data/spec/drop/drop_user_2_spec.rb +77 -0
- data/spec/drop/drop_user_spec.rb +67 -0
- data/spec/drop/expire_user_spec.rb +179 -0
- data/spec/export/export_spec.rb +119 -0
- data/spec/misc/misc_spec.rb +74 -0
- data/spec/misc/require_spec.rb +77 -0
- data/spec/spec_helper.rb +118 -0
- 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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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
|