diff --git a/bin/pt-show-grants b/bin/pt-show-grants index 1e5dc110..d73a6157 100755 --- a/bin/pt-show-grants +++ b/bin/pt-show-grants @@ -1636,10 +1636,16 @@ sub _d { # ########################################################################### package pt_show_grants; +use strict; +use warnings FATAL => 'all'; use English qw(-no_match_vars); - use constant PTDEBUG => $ENV{PTDEBUG} || 0; +use Data::Dumper; +$Data::Dumper::Quotekeys = 0; +$Data::Dumper::Indent = 1; +$Data::Dumper::Sortkeys = 1; + sub main { @ARGV = @_; # set global ARGV for this package @@ -1757,13 +1763,17 @@ sub main { PTDEBUG && _d($EVAL_ERROR); $exit_status = 1; } + PTDEBUG && _d('Grants:', Dumper(\@grants)); next unless @grants; - if ( $o->get('separate') ) { # List each grant separately. + if ( $o->get('separate') ) { + # List each grant separately. @grants = map { my ( $grants, $on_what ) = $_ =~ m/GRANT (.*?) ON (.*)$/; - map { "GRANT $_ ON $on_what" } split(', ', $grants); + map { "GRANT $_ ON $on_what" } split_grants($grants); } @grants; + PTDEBUG && _d('Grants separated:', Dumper(\@grants)); + my $count; # If the row with IDENTIFIED BY has multiple grants, this will # create many such rows; strip it from all but the first. @@ -1775,12 +1785,15 @@ sub main { } $_; } @grants; + PTDEBUG && _d('Grants separated:', Dumper(\@grants)); } - else { # Sort the actual grants lexically within each row for consistency. + else { + # Sort the actual grants lexically within each row for consistency. @grants = map { - $_ =~ s/GRANT (.*?) ON (`|\*)/"GRANT " . join(', ', sort(split(', ', $1))) . " ON $2"/e; + $_ =~ s/GRANT (.*?) ON (`|\*)/"GRANT " . join(', ', sort(split_grants($1))) . " ON $2"/e; $_; } @grants; + PTDEBUG && _d('Grants grouped:', Dumper(\@grants)); } # Sort the grant rows for consistency too, but the one with the password @@ -1788,6 +1801,7 @@ sub main { @grants = sort { $b =~ m/IDENTIFIED BY/ <=> $a =~ m/IDENTIFIED BY/ || $a cmp $b } @grants; + PTDEBUG && _d('Grants sorted:', Dumper(\@grants)); # Print REVOKE statements. if ( $o->get('revoke') ) { @@ -1802,7 +1816,7 @@ sub main { my @result; if ( $o->get('separate') ) { @result = map { "REVOKE $_ ON $on_what FROM $user" } - split(', ', $grants); + split_grants($grants); } else { @result = "REVOKE $grants ON $on_what FROM $user"; @@ -1867,6 +1881,31 @@ sub parse_user { return ( $user, $host ); } +sub split_grants { + my ($grants) = @_; + return unless $grants; + my @grants; + if ( $grants =~ m/(?:INSERT|SELECT|UPDATE) \(/ ) { + PTDEBUG && _d('Splitting grants on keywords:', $grants); + # TODO: the following .+? might break (e.g. on `annoying)column`). + # Remember to update this whenever we switch to using + # a common SQL regex module + @grants = $grants =~ m/ + ( + (?:INSERT|SELECT|UPDATE)\s\(.+?\) # a column grants + | [A-Z\s]+ + ) + (?:,\s)? # Separted from the next grant, if any, by a comma + /xg; + } + else { + PTDEBUG && _d('Splitting grants on comma:', $grants); + @grants = split(', ', $grants); + } + PTDEBUG && _d('Grants split:', Dumper(\@grants)); + return @grants; +} + sub _d { my ($package, undef, $line) = caller 0; @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } @@ -1894,7 +1933,7 @@ pt-show-grants - Canonicalize and print MySQL grants so you can effectively repl =head1 SYNOPSIS -Usage: pt-show-grants [OPTION...] [DSN] +Usage: pt-show-grants [OPTIONS] [DSN] pt-show-grants shows grants (user privileges) from a MySQL server. diff --git a/t/pt-show-grants/basics.t b/t/pt-show-grants/basics.t index 351a07ce..d526ffea 100644 --- a/t/pt-show-grants/basics.t +++ b/t/pt-show-grants/basics.t @@ -22,9 +22,6 @@ my $dbh = $sb->get_dbh_for('master'); if ( !$dbh ) { plan skip_all => 'Cannot connect to sandbox master'; } -else { - plan tests => 11; -} $sb->wipe_clean($dbh); @@ -94,8 +91,59 @@ like( 'No output when all users skipped' ); +# ############################################################################# +# pt-show-grant doesn't support column-level grants +# https://bugs.launchpad.net/percona-toolkit/+bug/866075 +# ############################################################################# +$sb->load_file('master', 't/pt-show-grants/samples/column-grants.sql'); +diag(`/tmp/12345/use -u root -e "GRANT SELECT(DateCreated, PckPrice, PaymentStat, SANumber) ON test.t TO 'sally'\@'%'"`); +diag(`/tmp/12345/use -u root -e "GRANT SELECT(city_id), INSERT(city) ON sakila.city TO 'sally'\@'%'"`); + +ok( + no_diff( + sub { pt_show_grants::main('-F', $cnf, qw(--only sally --no-header)) }, + "t/pt-show-grants/samples/column-grants.txt", + stderr => 1, + ), + "Column-level grants (bug 866075)" +); + +ok( + no_diff( + sub { pt_show_grants::main('-F', $cnf, qw(--only sally --no-header), + qw(--separate)) }, + "t/pt-show-grants/samples/column-grants-separate.txt", + stderr => 1, + ), + "Column-level grants --separate (bug 866075)" +); + +ok( + no_diff( + sub { pt_show_grants::main('-F', $cnf, qw(--only sally --no-header), + qw(--separate --revoke)) }, + "t/pt-show-grants/samples/column-grants-separate-revoke.txt", + stderr => 1, + ), + "Column-level grants --separate --revoke (bug 866075)" +); + +diag(`/tmp/12345/use -u root -e "GRANT SELECT ON sakila.city TO 'sally'\@'%'"`); + +ok( + no_diff( + sub { pt_show_grants::main('-F', $cnf, qw(--only sally --no-header)) }, + "t/pt-show-grants/samples/column-grants-combined.txt", + stderr => 1, + ), + "Column-level grants combined with table-level grants on the same table (bug 866075)" +); + +diag(`/tmp/12345/use -u root -e "DROP USER 'sally'\@'%'"`); + # ############################################################################# # Done. # ############################################################################# +$sb->wipe_clean($dbh); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); -exit; +done_testing; diff --git a/t/pt-show-grants/samples/column-grants-combined.txt b/t/pt-show-grants/samples/column-grants-combined.txt new file mode 100644 index 00000000..fd679980 --- /dev/null +++ b/t/pt-show-grants/samples/column-grants-combined.txt @@ -0,0 +1,4 @@ +-- Grants for 'sally'@'%' +GRANT INSERT (city), SELECT, SELECT (city_id) ON `sakila`.`city` TO 'sally'@'%'; +GRANT SELECT (SANumber, DateCreated, PaymentStat, PckPrice) ON `test`.`t` TO 'sally'@'%'; +GRANT USAGE ON *.* TO 'sally'@'%'; diff --git a/t/pt-show-grants/samples/column-grants-separate-revoke.txt b/t/pt-show-grants/samples/column-grants-separate-revoke.txt new file mode 100644 index 00000000..157350be --- /dev/null +++ b/t/pt-show-grants/samples/column-grants-separate-revoke.txt @@ -0,0 +1,10 @@ +-- Revoke statements for 'sally'@'%' +REVOKE INSERT (city) ON `sakila`.`city` FROM 'sally'@'%'; +REVOKE SELECT (SANumber, DateCreated, PaymentStat, PckPrice) ON `test`.`t` FROM 'sally'@'%'; +REVOKE SELECT (city_id) ON `sakila`.`city` FROM 'sally'@'%'; +REVOKE USAGE ON *.* FROM 'sally'@'%'; +-- Grants for 'sally'@'%' +GRANT INSERT (city) ON `sakila`.`city` TO 'sally'@'%'; +GRANT SELECT (SANumber, DateCreated, PaymentStat, PckPrice) ON `test`.`t` TO 'sally'@'%'; +GRANT SELECT (city_id) ON `sakila`.`city` TO 'sally'@'%'; +GRANT USAGE ON *.* TO 'sally'@'%'; diff --git a/t/pt-show-grants/samples/column-grants-separate.txt b/t/pt-show-grants/samples/column-grants-separate.txt new file mode 100644 index 00000000..9b6361d9 --- /dev/null +++ b/t/pt-show-grants/samples/column-grants-separate.txt @@ -0,0 +1,5 @@ +-- Grants for 'sally'@'%' +GRANT INSERT (city) ON `sakila`.`city` TO 'sally'@'%'; +GRANT SELECT (SANumber, DateCreated, PaymentStat, PckPrice) ON `test`.`t` TO 'sally'@'%'; +GRANT SELECT (city_id) ON `sakila`.`city` TO 'sally'@'%'; +GRANT USAGE ON *.* TO 'sally'@'%'; diff --git a/t/pt-show-grants/samples/column-grants.sql b/t/pt-show-grants/samples/column-grants.sql new file mode 100644 index 00000000..b805c298 --- /dev/null +++ b/t/pt-show-grants/samples/column-grants.sql @@ -0,0 +1,18 @@ +drop database if exists test; +create database test; +use test; + +CREATE TABLE t ( + `SOrNum` mediumint(9) unsigned NOT NULL auto_increment, + `SPNum` mediumint(9) unsigned NOT NULL, + `DateCreated` timestamp NOT NULL default CURRENT_TIMESTAMP, + `DateRelease` timestamp NOT NULL default '0000-00-00 00:00:00', + `ActualReleasedDate` timestamp NULL default NULL, + `PckPrice` decimal(10,2) NOT NULL default '0.00', + `Status` varchar(20) NOT NULL, + `PaymentStat` varchar(20) NOT NULL default 'Unpaid', + `CusCode` int(9) unsigned NOT NULL, + `SANumber` mediumint(9) unsigned NOT NULL default '0', + `SpecialInstruction` varchar(500) default NULL, + PRIMARY KEY (`SOrNum`) +) ENGINE=InnoDB; diff --git a/t/pt-show-grants/samples/column-grants.txt b/t/pt-show-grants/samples/column-grants.txt new file mode 100644 index 00000000..54feb4a5 --- /dev/null +++ b/t/pt-show-grants/samples/column-grants.txt @@ -0,0 +1,4 @@ +-- Grants for 'sally'@'%' +GRANT INSERT (city), SELECT (city_id) ON `sakila`.`city` TO 'sally'@'%'; +GRANT SELECT (SANumber, DateCreated, PaymentStat, PckPrice) ON `test`.`t` TO 'sally'@'%'; +GRANT USAGE ON *.* TO 'sally'@'%';