Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ jobs:
ruby:
- 3.0.6
- 3.1.4
- 3.4.1
mysql:
- 5.6
- 5.7
# TODO: skip old env
# - 5.6
# - 5.7
- 8.0.28
needs:
- check_test_execution_conditions
services:
Expand All @@ -50,6 +53,29 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Setup MySQL variables
run: |
version=`echo ${{ matrix.mysql }} | awk -F. '{printf "%d%03d", $1, $2}'`
# 8.0 or later
if (( 8000 <= version )); then
# https://blog.n-z.jp/blog/2021-02-20-github-actions-services-mysqld-option.html
touch test.cnf
(
echo '[mysqld]';
echo 'default_authentication_plugin=mysql_native_password';
echo 'log_bin_trust_function_creators=ON';
) > test.cnf
docker cp ./test.cnf ${{ job.services.mysql.id }}:/etc/mysql/conf.d/
docker restart ${{ job.services.mysql.id }}
for sleep in 0 ${WAITS:- 1 2 4 8 15 25 100}; do
sleep "$sleep"
health_status=`docker inspect --format="{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}" ${{ job.services.mysql.id }}`
if [ 'starting' != "$health_status" ]; then
exit 0
fi
done
exit 1
fi
- name: Run RSpec for ${{ matrix.mysql }}
run: bundle exec rake spec`echo ${{ matrix.mysql }} | awk -F. '{print $1"_"$2}'`
env:
Expand Down
5 changes: 3 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new('spec')
task :default => :spec_all

suffixes = %w(5_6 5_7)
suffixes = %w(5_6 5_7 8_0)
task :spec_all => suffixes.map { |s| "spec#{s}" }

suffixes.each do |suffix|
Expand All @@ -13,7 +13,8 @@ suffixes.each do |suffix|
if overwrite_host
ENV['MYSQL_HOST'] = "mysql#{suffix}"
end
ENV['MYSQL5_7'] = (suffix == '5_7' ? 1 : 0).to_s
ENV['MYSQL5_7'] = (%w(5_7 8_0).include?(suffix) ? 1 : 0).to_s
ENV['MYSQL8_0'] = (%w(8_0).include?(suffix) ? 1 : 0).to_s
Rake::Task['spec'].execute
end
end
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ services:
timeout: 5s
interval: 5s
retries: 5
mysql8_0:
image: "mysql:8.0.28"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
command: --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=ON
healthcheck:
test: ["CMD", "mysql", "-h", "localhost", "-u", "root", "--execute", "SHOW DATABASES;"]
start_period: 30s
timeout: 5s
interval: 5s
retries: 5
rake:
build: .
depends_on:
mysql5_6:
condition: service_healthy
mysql5_7:
condition: service_healthy
mysql8_0:
condition: service_healthy
entrypoint: ["rake"]
volumes:
- .:/usr/src/app
Expand Down
3 changes: 2 additions & 1 deletion gratan.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Gem::Specification.new do |spec|
spec.add_dependency 'term-ansicolor'
spec.add_dependency 'deep_merge'
spec.add_dependency 'hashie'
spec.add_dependency 'csv'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rake', '~> 12.3.2'
spec.add_development_dependency 'rspec', '>= 3.0.0'
spec.add_development_dependency 'timecop'
end
2 changes: 1 addition & 1 deletion lib/gratan/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def normalize_privs(privs)
priv[0].upcase!

if priv[1]
priv[1] = priv[1].split(',').map {|i| i.gsub(')', '').strip }.sort.join(', ')
priv[1] = priv[1].split(',').map { |i| i.gsub(/[`')]/, '').strip }.sort.join(', ')
priv[1] << ')'
end

Expand Down
97 changes: 72 additions & 25 deletions lib/gratan/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,36 @@ def flush_privileges

def create_user(user, host, options = {})
objects = options[:objects]
identified = options[:options][:identified]
required = options[:required]
with_option = options[:with]
auth_plugin = options[:auth_plugin] || "mysql_native_password"
grant_options = options[:options]
granted = false
required ||= grant_options.delete(:required)
granted = objects.keys.any? do |object_or_regexp|
!expand_object(object_or_regexp).empty?
end

unless granted
log(:warn, "there was no privileges to grant to #{quote_user(user, host)}", :color => :yellow)
return
end


sql = "CREATE USER #{quote_user(user, host)}"
if identified
prepos, quoted_identifier = quote_identifier_with_preposition(identified, options)
sql << " IDENTIFIED WITH #{auth_plugin} #{prepos} #{quoted_identifier}"
end
sql << " REQUIRE #{required}" if required
sql << " WITH #{with_option}" if with_option
update(sql)

objects.each do |object_or_regexp, object_options|
expand_object(object_or_regexp).each do |object|
grant(user, host, object, grant_options.merge(object_options))
granted = true
end
end

unless granted
log(:warn, "there was no privileges to grant to #{quote_user(user, host)}", :color => :yellow)
end
end

def drop_user(user, host)
Expand All @@ -85,7 +102,6 @@ def drop_user(user, host)

def grant(user, host, object, options)
privs = options.fetch(:privs)
identified = options[:identified]
required = options[:required]
with_option = options[:with]

Expand All @@ -95,7 +111,6 @@ def grant(user, host, object, options)
quote_user(user, host),
]

sql << " IDENTIFIED BY #{quote_identifier(identified)}" if identified
sql << " REQUIRE #{required}" if required
sql << " WITH #{with_option}" if with_option

Expand All @@ -110,29 +125,27 @@ def grant(user, host, object, options)
end
end

def identify(user, host, identifier)
sql = 'GRANT USAGE ON *.* TO %s IDENTIFIED BY %s' % [
def identify(user, host, identifier, auth_plugin = "mysql_native_password")
prepos, quoted_identifier = quote_identifier_with_preposition(identifier)

sql = "ALTER USER %s IDENTIFIED WITH #{auth_plugin} #{prepos} %s" % [
quote_user(user, host),
quote_identifier(identifier),
quoted_identifier,
]

update(sql)

if (identifier || '').empty?
set_password(user, host, identifier)
set_password(user, host, identifier, auth_plugin)
end
end

def set_password(user, host, password, options = {})
password ||= ''

unless options[:hash]
password = "PASSWORD('#{escape(password)}')"
end
def set_password(user, host, password, auth_plugin = 'mysql_native_password', options = {})
prepos, quoted_identifier = quote_identifier_with_preposition(password, options)

sql = 'SET PASSWORD FOR %s = %s' % [
sql = "ALTER USER %s IDENTIFIED WITH #{auth_plugin} #{prepos} %s" % [
quote_user(user, host),
password,
quoted_identifier,
]

update(sql)
Expand All @@ -141,7 +154,7 @@ def set_password(user, host, password, options = {})
def set_require(user, host, required)
required ||= 'NONE'

sql = 'GRANT USAGE ON *.* TO %s REQUIRE %s' % [
sql = 'ALTER USER %s REQUIRE %s' % [
quote_user(user, host),
required
]
Expand All @@ -166,7 +179,7 @@ def revoke(user, host, object, options = {})

def revoke0(user, host, object, privs)
sql = 'REVOKE %s ON %s FROM %s' % [
privs.join(', '),
privs.join(', ').gsub('\'', '').strip,
quote_object(object),
quote_user(user, host),
]
Expand All @@ -175,13 +188,20 @@ def revoke0(user, host, object, privs)
end

def update_with_option(user, host, object, with_option)
options = []
update_with_grant_option(user, host, object, with_option)
update_with_resource_option(user, host, with_option)
end

def update_with_grant_option(user, host, object, with_option)
if with_option =~ /\bGRANT\s+OPTION\b/i
options << 'GRANT OPTION'
grant(user, host, object, :privs => ['USAGE'], :with => 'GRANT OPTION')
else
revoke(user, host, object, :privs => ['GRANT OPTION'])
end
end

def update_with_resource_option(user, host, with_option)
options = []

%w(
MAX_QUERIES_PER_HOUR
Expand All @@ -199,10 +219,16 @@ def update_with_option(user, host, object, with_option)
end

unless options.empty?
grant(user, host, object, :privs => ['USAGE'], :with => options.join(' '))
alter_user_with(user, host, with: options.join(' '))
end
end

def alter_user_with(user, host, with:)
sql = "ALTER USER #{quote_user(user, host)}"
sql << " WITH #{with}" if with
update(sql)
end

def disable_log_bin_local
unless @options[:skip_disable_log_bin]
query('SET SQL_LOG_BIN = 0')
Expand Down Expand Up @@ -257,6 +283,27 @@ def quote_object(object)
object_type + object.split('.', 2).map {|i| i == '*' ? i : "`#{i}`" }.join('.')
end

def quote_identifier_with_preposition(identifier, options = {})
identifier = if identifier
identifier.dup
else
''
end
prepos = 'BY'
quoted_identifier = quote_identifier(identifier)
if options[:hash]
prepos = 'AS'
password = query("SELECT CONCAT('*', UPPER(SHA1(UNHEX(SHA1(#{quoted_identifier}))))) AS PASSWORD").first.values.first
elsif identifier.slice!(/^\s*PASSWORD\s+/i)
prepos = 'AS'
quoted_identifier = identifier
else
quoted_identifier = quote_identifier(identifier)
end

[prepos, quoted_identifier]
end

def quote_identifier(identifier)
identifier ||= ''

Expand Down
4 changes: 3 additions & 1 deletion lib/gratan/exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def export
end

@driver.show_grants(user, host) do |stmt|
grants << Gratan::GrantParser.parse(stmt, create_user)
grant = Gratan::GrantParser.parse(stmt, create_user)
next if grant.fetch(:privs).empty?
grants << grant
end
end

Expand Down
Loading