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