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
@@ -0,0 +1,19 @@
1
+ class Gratan::DSL::Context::On
2
+ include Gratan::DSL::Validator
3
+
4
+ attr_reader :result
5
+
6
+ def initialize(user, host, object, &block)
7
+ @error_identifier = "User `#{user}@#{host}` on `#{object}`"
8
+ @result = []
9
+ instance_eval(&block)
10
+ end
11
+
12
+ def grant(name, options = {})
13
+ __validate("Grant `#{name}` is already defined") do
14
+ not @result.include?(name)
15
+ end
16
+
17
+ @result << name
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ class Gratan::DSL::Context::User
2
+ include Gratan::DSL::Validator
3
+
4
+ attr_reader :result
5
+
6
+ def initialize(user, host, &block)
7
+ @error_identifier = "User `#{user}@#{host}`"
8
+ @user = user
9
+ @host = host
10
+ @result = {}
11
+ instance_eval(&block)
12
+ end
13
+
14
+ def on(name, options = {}, &block)
15
+ name = name.to_s
16
+
17
+ __validate("Object `#{name}` is already defined") do
18
+ not @result.has_key?(name)
19
+ end
20
+
21
+ grant = {:privs => Gratan::DSL::Context::On.new(@user, @host, name, &block).result}
22
+ grant[:with] = options[:with] if options[:with]
23
+ @result[name] = grant
24
+ end
25
+ end
@@ -0,0 +1,57 @@
1
+ class Gratan::DSL::Context
2
+ include Gratan::DSL::Validator
3
+ include Gratan::Logger::Helper
4
+
5
+ def self.eval(dsl, path, options = {})
6
+ self.new(path, options) do
7
+ eval(dsl, binding, path)
8
+ end
9
+ end
10
+
11
+ attr_reader :result
12
+
13
+ def initialize(path, options = {}, &block)
14
+ @path = path
15
+ @options = options
16
+ @result = {}
17
+ instance_eval(&block)
18
+ end
19
+
20
+ private
21
+
22
+ def require(file)
23
+ grantfile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file))
24
+
25
+ if File.exist?(grantfile)
26
+ instance_eval(File.read(grantfile), grantfile)
27
+ elsif File.exist?(grantfile + '.rb')
28
+ instance_eval(File.read(grantfile + '.rb'), grantfile + '.rb')
29
+ else
30
+ Kernel.require(file)
31
+ end
32
+ end
33
+
34
+ def user(name, host, options = {}, &block)
35
+ name = name.to_s
36
+ host = host.to_s
37
+ options ||= {}
38
+
39
+ __validate("User `#{name}@#{host}` is already defined") do
40
+ not @result.has_key?([name, host])
41
+ end
42
+
43
+ if @options[:enable_expired] and (expired = options.delete(:expired))
44
+ expired = Time.parse(expired)
45
+
46
+ if Time.new >= expired
47
+ log(:warn, "User `#{name}@#{host}` has expired", :yellow)
48
+ return
49
+ end
50
+ end
51
+
52
+ @result[[name, host]] = {
53
+ :objects => Gratan::DSL::Context::User.new(name, host, &block).result,
54
+ :options => options,
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,74 @@
1
+ class Gratan::DSL::Converter
2
+ def self.convert(exported, options = {})
3
+ self.new(exported, options).convert
4
+ end
5
+
6
+ def initialize(exported, options = {})
7
+ @exported = exported
8
+ @options = options
9
+ end
10
+
11
+ def convert
12
+ @exported.map {|user_host, attrs|
13
+ output_user(user_host, attrs)
14
+ }.join("\n")
15
+ end
16
+
17
+ private
18
+
19
+ def output_user(user_host, attrs)
20
+ user, host = user_host
21
+ objects, options = attrs.values_at(:objects, :options)
22
+ options = output_user_options(options)
23
+
24
+ <<-EOS
25
+ user #{user.inspect}, #{host.inspect}#{options}do
26
+ #{output_objects(objects)}
27
+ end
28
+ EOS
29
+ end
30
+
31
+ def output_user_options(options)
32
+ if options.empty?
33
+ ' '
34
+ else
35
+ options = strip_hash_brace(options.inspect)
36
+ ", #{options} "
37
+ end
38
+ end
39
+
40
+ def output_objects(objects)
41
+ objects.map {|object, grant|
42
+ options = output_object_options(grant)
43
+
44
+ <<-EOS
45
+ on #{object.inspect}#{options}do
46
+ #{output_grant(grant)}
47
+ end
48
+ EOS
49
+ }.join("\n").strip
50
+ end
51
+
52
+ def output_object_options(grant)
53
+ with_option = grant.delete(:with)
54
+
55
+ if with_option
56
+ options = strip_hash_brace({:with => with_option}.inspect)
57
+ ", #{options} "
58
+ else
59
+ ' '
60
+ end
61
+ end
62
+
63
+ def output_grant(grant)
64
+ grant[:privs].map {|priv|
65
+ <<-EOS
66
+ grant #{priv.inspect}
67
+ EOS
68
+ }.join.strip
69
+ end
70
+
71
+ def strip_hash_brace(hash_str)
72
+ hash_str.sub(/\A\{/, '').sub(/\}\z/, '')
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ module Gratan::DSL::Validator
2
+ def __validate(errmsg)
3
+ raise __identify(errmsg) unless yield
4
+ end
5
+
6
+ def __identify(errmsg)
7
+ if @error_identifier
8
+ errmsg = "#{@error_identifier}: #{errmsg}"
9
+ end
10
+
11
+ return errmsg
12
+ end
13
+ end
data/lib/gratan/dsl.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Gratan::DSL
2
+ def self.convert(exported, options = {})
3
+ Gratan::DSL::Converter.convert(exported, options)
4
+ end
5
+
6
+ def self.parse(dsl, path, options = {})
7
+ Gratan::DSL::Context.eval(dsl, path, options).result
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ class Gratan::Exporter
2
+ def self.export(driver, options = {}, &block)
3
+ self.new(driver, options).export(&block)
4
+ end
5
+
6
+ def initialize(driver, options = {})
7
+ @driver = driver
8
+ @options = options
9
+ end
10
+
11
+ def export
12
+ grants = []
13
+
14
+ @driver.each_user do |user, host|
15
+ next if user =~ @options[:ignore_user]
16
+
17
+ @driver.show_grants(user, host) do |stmt|
18
+ grants << Gratan::GrantParser.parse(stmt)
19
+ end
20
+ end
21
+
22
+ pack(grants)
23
+ end
24
+
25
+ private
26
+
27
+ def pack(grants)
28
+ packed = {}
29
+
30
+ grants.each do |grant|
31
+ user = grant.delete(:user)
32
+ host = grant.delete(:host)
33
+ user_host = [user, host]
34
+ object = grant.delete(:object)
35
+ identified = grant.delete(:identified)
36
+ required = grant.delete(:require)
37
+
38
+ packed[user_host] ||= {:objects => {}, :options => {}}
39
+ packed[user_host][:objects][object] = grant
40
+ packed[user_host][:options][:required] = required if required
41
+
42
+ if @options[:with_identifier] and identified
43
+ packed[user_host][:options][:identified] = identified
44
+ end
45
+ end
46
+
47
+ packed
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ class String
2
+ @@colorize = false
3
+
4
+ class << self
5
+ def colorize=(value)
6
+ @@colorize = value
7
+ end
8
+
9
+ def colorize
10
+ @@colorize
11
+ end
12
+ end # of class methods
13
+
14
+ Term::ANSIColor::Attribute.named_attributes.map do |attribute|
15
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
16
+ def #{attribute.name}
17
+ if @@colorize
18
+ Term::ANSIColor.send(#{attribute.name.inspect}, self)
19
+ else
20
+ self
21
+ end
22
+ end
23
+ EOS
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ class Gratan::GrantParser
2
+ def initialize(stmt)
3
+ @stmt = stmt.strip
4
+ @parsed = {}
5
+ end
6
+
7
+ def self.parse(stmt)
8
+ parser = self.new(stmt)
9
+ parser.parse!
10
+ end
11
+
12
+ def parse!
13
+ parse_grant
14
+ parse_require
15
+ parse_identified
16
+ parse_main
17
+ @parsed
18
+ end
19
+
20
+ private
21
+
22
+ def parse_grant
23
+ @stmt.slice!(/\s+WITH\s+(.+?)\z/)
24
+ with_option = $1
25
+
26
+ if with_option
27
+ @parsed[:with] = with_option.strip
28
+ end
29
+ end
30
+
31
+ def parse_require
32
+ @stmt.slice!(/\s+REQUIRE\s+(.+?)\z/)
33
+ required = $1
34
+
35
+ if required
36
+ @parsed[:require] = required.strip
37
+ end
38
+ end
39
+
40
+ def parse_identified
41
+ @stmt.slice!(/\s+IDENTIFIED BY\s+(.+?)\z/)
42
+ identified = $1
43
+
44
+ if identified
45
+ @parsed[:identified] = identified.strip
46
+ end
47
+ end
48
+
49
+ def parse_main
50
+ md = /\AGRANT\s+(.+?)\s+ON\s+(.+?)\s+TO\s+'(.*)'@'(.+)'\z/.match(@stmt)
51
+ privs, object, user, host = md.captures
52
+ @parsed[:privs] = parse_privs(privs.strip)
53
+ @parsed[:object] = object.gsub('`', '').strip
54
+ @parsed[:user] = user
55
+ @parsed[:host] = host
56
+ end
57
+
58
+ def parse_privs(privs)
59
+ privs << ','
60
+ priv_list = []
61
+
62
+ while priv = privs.slice!(/\A[^,(]+(?:\([^)]+\))?\s*,\s*/)
63
+ priv_list << priv.strip.sub(/,\z/, '').strip
64
+ end
65
+
66
+ priv_list
67
+ end
68
+ end
@@ -0,0 +1,28 @@
1
+ class Gratan::Identifier::Auto
2
+ def initialize(output, options = {})
3
+ @options = options
4
+
5
+ if output == '-'
6
+ @output = $stdout
7
+ else
8
+ @output = open(output, 'w')
9
+ end
10
+ end
11
+
12
+ def identify(user, host)
13
+ password = mkpasswd
14
+ puts_password(user, host, password)
15
+ password
16
+ end
17
+
18
+ private
19
+
20
+ def mkpasswd(len = 8)
21
+ [*1..9, *'A'..'Z', *'a'..'z'].shuffle.slice(0, len).join
22
+ end
23
+
24
+ def puts_password(user, host, password)
25
+ @output.puts("#{user}@#{host},#{password}")
26
+ @output.flush
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'csv'
2
+
3
+ class Gratan::Identifier::CSV
4
+ include Gratan::Logger::Helper
5
+
6
+ def initialize(path, options = {})
7
+ @options = options
8
+ @passwords = {}
9
+
10
+ CSV.foreach(path) do |row|
11
+ @passwords[row[0]] = row[1]
12
+ end
13
+ end
14
+
15
+ def identify(user, host)
16
+ user_host = "#{user}@#{host}"
17
+ password = @passwords[user_host]
18
+
19
+ unless password
20
+ log(:warn, "password for `#{user_host}` can not be found", :yellow)
21
+ end
22
+
23
+ password
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ class Gratan::Identifier::Null
2
+ def identify(user, host)
3
+ nil
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ class Gratan::Identifier
2
+ end
@@ -0,0 +1,28 @@
1
+ class Gratan::Logger < ::Logger
2
+ include Singleton
3
+
4
+ def initialize
5
+ super($stdout)
6
+
7
+ self.formatter = proc do |severity, datetime, progname, msg|
8
+ "#{msg}\n"
9
+ end
10
+
11
+ self.level = Logger::INFO
12
+ end
13
+
14
+ def set_debug(value)
15
+ self.level = value ? Logger::DEBUG : Logger::INFO
16
+ end
17
+
18
+ module Helper
19
+ def log(level, message, color = nil)
20
+ options = @options || {}
21
+ message = "[#{level.to_s.upcase}] #{message}" unless level == :info
22
+ message << ' (dry-run)' if options[:dry_run]
23
+ message = message.send(color) if color
24
+ logger = options[:logger] || Gratan::Logger.instance
25
+ logger.send(level, message)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Gratan
2
+ VERSION = '0.1.0'
3
+ end
data/lib/gratan.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'logger'
2
+ require 'mysql2'
3
+ require 'singleton'
4
+ require 'term/ansicolor'
5
+ require 'time'
6
+
7
+ module Gratan; end
8
+ require 'gratan/logger'
9
+ require 'gratan/client'
10
+ require 'gratan/driver'
11
+ require 'gratan/dsl'
12
+ require 'gratan/dsl/validator'
13
+ require 'gratan/dsl/context'
14
+ require 'gratan/dsl/context/user'
15
+ require 'gratan/dsl/context/on'
16
+ require 'gratan/dsl/converter'
17
+ require 'gratan/exporter'
18
+ require 'gratan/ext/string_ext'
19
+ require 'gratan/grant_parser'
20
+ require 'gratan/identifier'
21
+ require 'gratan/identifier/auto'
22
+ require 'gratan/identifier/csv'
23
+ require 'gratan/identifier/null'
24
+ require 'gratan/version'
@@ -0,0 +1,154 @@
1
+ describe 'Gratan::Client#apply' do
2
+ before(:each) do
3
+ apply {
4
+ <<-RUBY
5
+ user 'scott', 'localhost', identified: 'tiger', required: 'SSL' do
6
+ on '*.*' do
7
+ grant 'USAGE'
8
+ end
9
+ end
10
+
11
+ user 'bob', 'localhost' do
12
+ on '*.*', with: 'GRANT OPTION' do
13
+ grant 'ALL PRIVILEGES'
14
+ end
15
+ end
16
+ RUBY
17
+ }
18
+ end
19
+
20
+ context 'when update password' do
21
+ subject { client }
22
+
23
+ it do
24
+ apply(subject) {
25
+ <<-RUBY
26
+ user 'scott', 'localhost', identified: '123', required: 'SSL' do
27
+ on '*.*' do
28
+ grant 'USAGE'
29
+ end
30
+ end
31
+
32
+ user 'bob', 'localhost', identified: '456' do
33
+ on '*.*', with: 'GRANT OPTION' do
34
+ grant 'ALL PRIVILEGES'
35
+ end
36
+ end
37
+ RUBY
38
+ }
39
+
40
+ expect(show_grants).to match_array [
41
+ "GRANT ALL PRIVILEGES ON *.* TO 'bob'@'localhost' IDENTIFIED BY PASSWORD '*531E182E2F72080AB0740FE2F2D689DBE0146E04' WITH GRANT OPTION",
42
+ "GRANT USAGE ON *.* TO 'scott'@'localhost' IDENTIFIED BY PASSWORD '*23AE809DDACAF96AF0FD78ED04B6A265E05AA257' REQUIRE SSL",
43
+ ]
44
+ end
45
+ end
46
+
47
+ context 'when remove password' do
48
+ subject { client }
49
+
50
+ it do
51
+ apply(subject) {
52
+ <<-RUBY
53
+ user 'scott', 'localhost', identified: nil, required: 'SSL' do
54
+ on '*.*' do
55
+ grant 'USAGE'
56
+ end
57
+ end
58
+
59
+ user 'bob', 'localhost' do
60
+ on '*.*', with: 'GRANT OPTION' do
61
+ grant 'ALL PRIVILEGES'
62
+ end
63
+ end
64
+ RUBY
65
+ }
66
+
67
+ expect(show_grants).to match_array [
68
+ "GRANT ALL PRIVILEGES ON *.* TO 'bob'@'localhost' WITH GRANT OPTION",
69
+ "GRANT USAGE ON *.* TO 'scott'@'localhost' REQUIRE SSL",
70
+ ]
71
+ end
72
+ end
73
+
74
+ context 'when skip update password' do
75
+ subject { client }
76
+
77
+ it do
78
+ apply(subject) {
79
+ <<-RUBY
80
+ user 'scott', 'localhost', required: 'SSL' do
81
+ on '*.*' do
82
+ grant 'USAGE'
83
+ end
84
+ end
85
+
86
+ user 'bob', 'localhost' do
87
+ on '*.*', with: 'GRANT OPTION' do
88
+ grant 'ALL PRIVILEGES'
89
+ end
90
+ end
91
+ RUBY
92
+ }
93
+
94
+ expect(show_grants).to match_array [
95
+ "GRANT ALL PRIVILEGES ON *.* TO 'bob'@'localhost' WITH GRANT OPTION",
96
+ "GRANT USAGE ON *.* TO 'scott'@'localhost' IDENTIFIED BY PASSWORD '*F2F68D0BB27A773C1D944270E5FAFED515A3FA40' REQUIRE SSL",
97
+ ]
98
+ end
99
+ end
100
+
101
+ context 'when update require' do
102
+ subject { client }
103
+
104
+ it do
105
+ apply(subject) {
106
+ <<-RUBY
107
+ user 'scott', 'localhost', required: 'X509' do
108
+ on '*.*' do
109
+ grant 'USAGE'
110
+ end
111
+ end
112
+
113
+ user 'bob', 'localhost', required: 'SSL' do
114
+ on '*.*', with: 'GRANT OPTION' do
115
+ grant 'ALL PRIVILEGES'
116
+ end
117
+ end
118
+ RUBY
119
+ }
120
+
121
+ expect(show_grants).to match_array [
122
+ "GRANT ALL PRIVILEGES ON *.* TO 'bob'@'localhost' REQUIRE SSL WITH GRANT OPTION",
123
+ "GRANT USAGE ON *.* TO 'scott'@'localhost' IDENTIFIED BY PASSWORD '*F2F68D0BB27A773C1D944270E5FAFED515A3FA40' REQUIRE X509",
124
+ ]
125
+ end
126
+ end
127
+
128
+ context 'when update with option' do
129
+ subject { client }
130
+
131
+ it do
132
+ apply(subject) {
133
+ <<-RUBY
134
+ user 'scott', 'localhost', identified: 'tiger', required: 'SSL' do
135
+ on '*.*', with: 'GRANT OPTION MAX_QUERIES_PER_HOUR 1 MAX_UPDATES_PER_HOUR 2 MAX_CONNECTIONS_PER_HOUR 3 MAX_USER_CONNECTIONS 4' do
136
+ grant 'USAGE'
137
+ end
138
+ end
139
+
140
+ user 'bob', 'localhost' do
141
+ on '*.*' do
142
+ grant 'ALL PRIVILEGES'
143
+ end
144
+ end
145
+ RUBY
146
+ }
147
+
148
+ expect(show_grants).to match_array [
149
+ "GRANT ALL PRIVILEGES ON *.* TO 'bob'@'localhost'",
150
+ "GRANT USAGE ON *.* TO 'scott'@'localhost' IDENTIFIED BY PASSWORD '*F2F68D0BB27A773C1D944270E5FAFED515A3FA40' REQUIRE SSL WITH GRANT OPTION MAX_QUERIES_PER_HOUR 1 MAX_UPDATES_PER_HOUR 2 MAX_CONNECTIONS_PER_HOUR 3 MAX_USER_CONNECTIONS 4",
151
+ ]
152
+ end
153
+ end
154
+ end