You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ngcpcfg/sbin/ngcp-sync-db-grants

623 lines
18 KiB

#!/usr/bin/perl
#----------------------------------------------------------------------
# Synchronizes mysql grants from a schema template
#----------------------------------------------------------------------
use strict;
use warnings;
use English;
use DBI;
use Getopt::Long;
use Config::Tiny;
use YAML::XS;
use Readonly;
Readonly my $GRANTS_SCHEMA => '/etc/mysql/grants.yml';
Readonly my $DB_CFG => "/etc/default/ngcp-db";
Readonly my $DEFAULT_DBHOST => "127.0.0.1";
Readonly my $DEFAULT_DBPORT => "3306";
Readonly my $TEMP_GRANT_USER => 'ngcp-sync-db';
Readonly my $TEMP_GRANT_HOST => 'localhost';
my $grants = {};
my $dbh;
my $debug = 0;
my $log_offset = 0;
my $password_length = 20;
my $check_only = 0;
my $recreate_user = 0;
my $custom_db_host;
my $custom_db_port;
my $no_warnings = 0;
sub Usage {
print <<USAGE;
==
Synchronizes mysql grants from a schema template
==
$PROGRAM_NAME [options]
Options:
--help|-h|-? -- this help
--verbose|-v -- verbose mode
--check|-c -- check only
--quiet|-q -- quiet mode
--recreate-user -- recreate user
(useful when all user hosts need to be cleared)
--db-host -- use custom db host
--db-port -- use custom db port
--no-warnings -- suppress warning messages such as
"Cannot apply grant: Table 'x' doesn't exist
USAGE
exit 0;
}
GetOptions("h|?|help" => \&Usage,
"v|verbose" => \$debug,
"c|check" => \$check_only,
"q|quiet" => sub { $debug = -1 },
"recreate-user" => \$recreate_user,
"db-host=s" => \$custom_db_host,
"db-port=s" => \$custom_db_port,
"no-warnings" => \$no_warnings,
) or die Usage();
sub pwgen {
my @list = ("a".."z",0..9,"A".."Z");
my @randoms;
for (1..$password_length) {
push @randoms, $list[int(rand($#list))];
}
return join "", @randoms;
}
sub connect_db {
my ($dbhost, $dbport) = @_;
my $dbcredentials = "/etc/mysql/sipwise_extra.cnf";
$dbh = DBI->connect("DBI:mysql:database=mysql;host=$dbhost;port=$dbport;"
. "mysql_read_default_file=${dbcredentials}",
"", "",
{ PrintError => 0 })
or die "Can't connect to MySQL database 'mysql': $DBI::errstr\n";
log_debug("connected to $dbhost:$dbport using '${dbcredentials}'");
$dbh->do("SET sql_log_bin=0")
or die "Cannot set sql_log_bin=0: $DBI::errstr\n";
return;
}
sub logger {
my $str = shift || '';
my $mode = shift || 0;
return if $debug == -1;
return if $mode == 1 && $debug <= 0;
my $offset = $log_offset*2;
$offset -= 2 if $debug < 1;
$offset = 0 if $offset < 0;
my $prefix = $mode < 2 ? "-->" : "Warning:";
printf { $mode == 2 ? *STDERR : *STDOUT } "%s%s %s\n",
$prefix, " "x$offset, $str;
return;
}
sub log_info { logger(shift, 0); }
sub log_debug { logger(shift, 1); }
sub log_warn { $no_warnings || logger(shift, 2); }
sub get_hostname {
my $hostname = qx(ngcp-hostname);
chomp $hostname;
return $hostname;
}
sub apply_grants {
my ($ref, $ptr, $key, $idx, $data, $as) = @_;
$idx ||= 0;
$data ||= [];
if ($ref =~ s/^(.+\..+\..+)\s+as\s+(\S+)\s*$/$1/) {
$as = $2 || die "Missing as 'hostname'";
}
my $rc = 0;
unless ($key) {
my @path = split /\./, $ref;
die "Malformed grants ref $ref" unless $#path == 2;
die "Index $idx is out of allowed range" if $idx < 0 || $idx > 2;
$key ||= $path[$idx];
}
$ptr ||= $grants;
if ($key =~ s/\*/.+/g) {
foreach my $v (sort { $a cmp $b } keys %$ptr) {
if ($v =~ /^$key$/) {
$rc += apply_grants($ref, $ptr, $v, $idx, [ @$data ] , $as);
}
}
} else {
unless (defined $ptr->{$key}) {
unless ($as) {
die sprintf "Undefined key %s in %s with idx=%d in ref %s",
$key, join('.', @$data), $idx, $ref
} else {
return 0;
}
}
if (ref $ptr->{$key} eq 'HASH') {
if ($idx == 1 && $recreate_user && !$as) { # local user
if (!$check_only && apply_drop_users($key)) {
flush_privs();
}
}
$rc += apply_grants($ref, $ptr->{$key}, undef, $idx+1,
[ @$data, $key ], $as);
} elsif (ref $ptr->{$key} eq 'ARRAY') {
push @$data, $key;
my ($top, $user, $host) = @$data;
$host = $as if $as;
$log_offset = 1;
my $new_user = 0;
log_debug(sprintf "[%s]%s", join('.', @$data), $as ? " as $as" : '');
normalise_grants($ptr->{$key}, $user, $host);
return 0 unless check_grants($ptr->{$key}, $user, $host);
log_info(sprintf "[%s]%s", join('.', @$data), $as ? " as $as" : '') unless $debug;
$log_offset = 2;
log_info(sprintf "revoke all from: %s\@%s", $user, $host);
unless ($check_only) {
$dbh->do("REVOKE ALL PRIVILEGES, GRANT OPTION FROM '$user'\@'$host'");
if ($DBI::errstr) {
if ($DBI::errstr =~
/revoke all privileges for one or more of the requested users/) {
$new_user = 1;
} elsif ($DBI::errstr !~ /There is no such grant defined/) {
print "USER: $user HOST: $host\n";
die sprintf "Cannot revoke privileges from %s\@%s: %s\n",
$user, $host, $DBI::errstr;
}
}
}
$rc++;
for (my $i=0;$i<=$#{$ptr->{$key}};$i++) {
my $grant = $ptr->{$key}[$i];
$log_offset = 2;
my ($s_grant, $s_suffix) = split_grant_suffix($grant);
log_info(sprintf "grant %s to %s\@%s %s",
$s_grant, $user, $host, $s_suffix);
next if $check_only;
$dbh->do("GRANT $s_grant TO '$user'\@'$host' $s_suffix");
if ($DBI::errstr &&
$DBI::errstr !~ /Table\s+'\S+\.\S+'\s+doesn't\s+exist/ &&
$DBI::errstr !~ /FUNCTION or PROCEDURE\s+\S+\s+does not exist/) {
die "Cannot grant privileges: $DBI::errstr\n";
} elsif ($DBI::errstr) {
$log_offset = 0;
log_warn("Cannot apply grant: ".$DBI::errstr);
$log_offset = 2;
}
}
if ($new_user && !$check_only) {
create_protected_user($user, $host);
}
} else {
die "Unparsable grants structure element: $key";
}
}
return $rc;
}
sub apply_host_grants {
my $rc = 0;
foreach my $grant_host (sort { $a cmp $b } keys %{$grants->{hosts}}) {
if (my $ref = $grants->{hosts}{$grant_host}) {
(my $grant_host_rx = $grant_host) =~ s/\*/.+/g;
my $hostname = get_hostname();
$grant_host_rx = $hostname if $grant_host_rx eq "self";
if ($hostname =~ /^$grant_host_rx$/) {
log_debug("host: $hostname ($grant_host)");
$ref = ref $ref ? $ref : [ $ref ];
map { $rc += apply_grants($_) } @$ref;
$log_offset = 0;
}
}
}
return $rc;
}
sub apply_copy_grants {
my $rc = 0;
foreach my $grant_host (sort { $a cmp $b } keys %{$grants->{copy}}) {
if (my $ref = $grants->{copy}{$grant_host}) {
(my $grant_host_rx = $grant_host) =~ s/\*/.+/g;
my $hostname = get_hostname();
$grant_host_rx = $hostname if $grant_host_rx eq "self";
if ($hostname =~ /^$grant_host_rx$/) {
log_debug("copy: $hostname ($grant_host)");
$ref = ref $ref ? $ref : [ $ref ];
map { $rc += apply_grants($_) } @$ref;
$log_offset = 0;
}
}
}
return $rc;
}
sub apply_drop_users {
my $forced_user = shift; # to drop a specific user
my $rc = 0;
my $drops = $grants->{drop};
if ($forced_user) {
$drops = { $forced_user => '*' };
}
foreach my $user (sort { $a cmp $b } keys %$drops) {
my $ref = $drops->{$user};
$ref = ref $ref ? $ref : [ $ref ];
foreach my $host_rx (@$ref) {
my $usel_sql = 'SELECT Host FROM mysql.user WHERE User = ?';
my $is_not = 0;
my $is_rx = 0;
if ($host_rx =~ s/^\!//) {
$is_not = 1;
}
if ($host_rx =~ /\*/) {
$host_rx =~ s/\*/%/g;
$is_rx = 1;
}
$usel_sql .= $is_not
? $is_rx
? ' AND Host NOT LIKE ?'
: ' AND Host != ?'
: $is_rx
? ' AND Host LIKE ?'
: ' AND Host = ?';
my $users = $dbh->selectall_arrayref($usel_sql, undef, $user, $host_rx)
or die "Cannot select user $user -- $host_rx: $DBI::errstr\n";
foreach my $host_ref (@{$users}) {
my $host = $host_ref->[0];
$dbh->do("DROP USER '$user'\@'$host'")
or die "Cannot drop user $user -- $host: $DBI::errstr\n";
log_info(sprintf "drop: %s\@%s", $user, $host);
$rc++;
}
}
}
return $rc;
}
sub create_protected_user {
my ($user, $host) = @_;
my ($random_pass) = $dbh->selectrow_array("SELECT PASSWORD(?)",
undef, pwgen());
die "Cannot generate password: $DBI::errstr\n" if $DBI::err;
unless ($random_pass =~ /^\*(\S+)\s*$/) {
die "Cannot parse generated password: $random_pass";
}
$random_pass = '!'.$1;
my ($temp_user_count) = $dbh->selectrow_array(<<SQL, undef, $user, $host);
SELECT COUNT(User)
FROM user
WHERE User = ?
AND Host = ?
SQL
die "Cannot select grant temp user: $DBI::errstr\n" if $DBI::err;
unless ($temp_user_count) {
$dbh->do("CREATE USER '$user'\@'$host'");
die "Cannot create grant temp user: $DBI::errstr\n" if $DBI::err;
}
$dbh->do("SET PASSWORD FOR '$user'\@'$host' = PASSWORD(?)",
undef, $random_pass);
die sprintf "Cannot update %s@%s with generated password, %s\n",
$user, $host, $DBI::errstr if $DBI::err;
return;
}
sub split_grant_suffix {
my $grant = shift;
my $suffix = "";
if ($grant =~ s/^(.+on\s+\S+\.\S+)\s+(to\s+\S+\@\S+\s*.*?)*((with|require).+)$/$1/i) {
$suffix = $3 if $3;
}
return ($grant, $suffix);
}
sub grants_helper {
my $grants = shift;
my $user = $TEMP_GRANT_USER;
my $host = $TEMP_GRANT_HOST;
$dbh->do("REVOKE ALL PRIVILEGES, GRANT OPTION FROM '$user'\@'$host'");
foreach my $grant (@$grants) {
$grant = normalise_grant_str($grant);
my ($s_grant, $s_suffix) = split_grant_suffix($grant);
$dbh->do("GRANT $s_grant TO '$user'\@'$host' $s_suffix");
if ($DBI::errstr &&
$DBI::errstr !~ /Table\s+'\S+\.\S+'\s+doesn't\s+exist/ &&
$DBI::errstr !~ /FUNCTION or PROCEDURE\s+\S+\s+does not exist/) {
die "Cannot grant privileges: $DBI::errstr\n";
} elsif ($DBI::errstr) {
$log_offset = 0;
log_warn("Cannot apply grant: ".$DBI::errstr);
$log_offset = 2;
}
}
my $temp_grants = $dbh->selectall_arrayref(
"SHOW GRANTS FOR '$user'\@'$host'");
for (my $i=0;$i<=$#$temp_grants;$i++) {
if ($temp_grants->[$i][0] =~ /grant usage/i) {
splice(@$temp_grants, $i, 1);
last;
}
}
die "Error in checking grants" if $#$temp_grants < 0;
@$grants = ( map { $_->[0] } @$temp_grants );
return;
}
sub normalise_grant_str {
my $grant = shift;
my $suffix = "";
($grant, $suffix) = split_grant_suffix($grant);
$grant = lc $grant;
$grant =~ s/(^\s+|\s+$)//g;
$grant =~ s/^grant\s+//i;
$grant =~ s/^(.+)\s+to\s+.+$/$1/i;
$grant =~ s/`//g;
$grant =~ s/,\s+/,/g;
$grant =~ s/all\s+on/all privileges on/;
if ($suffix) {
$grant =~ s/\s+$//;
$suffix =~ s/\s+/ /;
$suffix = lc $suffix;
$grant = sprintf "%s %s", $grant, $suffix;
}
return $grant;
}
sub normalise_grants {
my ($grants, $user, $host) = @_;
grants_helper($grants);
foreach my $grant (@$grants) {
$grant = normalise_grant_str($grant);
}
return;
}
sub check_grants {
my ($grants, $user, $host) = @_;
my $current_grants = $dbh->selectall_arrayref(
"SHOW GRANTS FOR '$user'\@'$host'");
if ($DBI::errstr
&& ($DBI::errstr !~ /There is no such grant defined/ &&
$DBI::errstr !~ /fetch[()]+ without execute[()]+/)) {
die sprintf "Cannot select grants for %s\@%s: %s\n",
$user, $host, $DBI::errstr;
}
return 0 if not $current_grants and scalar @$grants == 0;
for (my $i=0;$i<=$#$current_grants;$i++) {
if ($current_grants->[$i][0] =~ /grant usage/i) {
splice(@$current_grants, $i, 1);
last;
}
}
return 1 if scalar @$current_grants != scalar @$grants;
foreach my $c_ref (@{$current_grants}) {
my $grant = $c_ref->[0];
next if $grant =~ /grant usage/i;
$grant = normalise_grant_str($grant);
my $rc = 1;
foreach my $check (@$grants) {
if ($check eq $grant) {
$rc = 0;
last;
}
}
return $rc if $rc;
}
return 0;
}
sub flush_privs {
log_debug("flush privileges");
$dbh->do("FLUSH PRIVILEGES")
or die "Cannot flush privileges: $DBI::errstr\n";
return;
}
sub main {
$grants = YAML::XS::LoadFile($GRANTS_SCHEMA);
my ($dbhost, $dbport);
if (my $db_cfg = Config::Tiny->read($DB_CFG)) {
($dbhost, $dbport) = @{$db_cfg->{_}}{qw(PAIR_DBHOST PAIR_DBPORT)};
} else {
log_warn(sprintf "Cannot open %s: %s, using host=%s port=%s",
$DB_CFG, $ERRNO,
$custom_db_host // $DEFAULT_DBHOST,
$custom_db_port // $DEFAULT_DBPORT);
$dbhost = $custom_db_host // $DEFAULT_DBHOST;
$dbport = $custom_db_port // $DEFAULT_DBPORT;
}
$dbhost = $custom_db_host // $dbhost;
$dbport = $custom_db_port // $dbport;
connect_db($dbhost, $dbport);
if ($check_only) {
log_info("** running in 'check only' mode, no changes are made to the users/grants **");
}
$dbh->begin_work or die "Cannot start transaction: $DBI::errstr\n";
eval {
my $rc = 0;
create_protected_user($TEMP_GRANT_USER, $TEMP_GRANT_HOST);
foreach my $proc (@{$grants->{order}}) {
SWITCH: for ($proc) {
/^drop$/ && do {
if (!$check_only && apply_drop_users()) {
flush_privs();
}
};
/^hosts$/ && do {
$rc += apply_host_grants();
};
/^copy$/ && do {
$rc += apply_copy_grants();
};
} # SWITCH
}
$dbh->do("DROP USER IF EXISTS '$TEMP_GRANT_USER'\@'$TEMP_GRANT_HOST'");
die "Cannot drop temp user: $DBI::errstr\n" if $DBI::err;
if ($rc) {
flush_privs();
}
};
if ($@) {
$dbh->rollback if $dbh;
$dbh->disconnect if $dbh;
die "Error: $@";
}
$dbh->commit or die "Cannot commit transaction: $DBI::errstr\n";
$dbh->disconnect;
if ($recreate_user) {
log_info(<<MSG);
Warning: Recreated users are without passwords,
please consider running 'ngcp-sync-db-creds' to update passwords for them.
MSG
}
return;
}
main();
exit 0;
# vim: ts=4 sw=4 et
__END__
=pod
=head1 NAME
ngcp-sync-db-grants - synchronizes mysql grants from a schema template
=head1 SYNOPSIS
B<ngcp-sync-db-grants> [I<options>...]
=head1 DESCRIPTION
B<This program> synchronizes mysql grants from a schema template.
=head1 OPTIONS
=over 8
=item B<--recreate-user>
Drop all appearances of a user before applying grants.
This option is useful to clean up the particular
user's hosts that are not covered by the schema.
=item B<--verbose>
Verbose mode
=back
=head1 EXIT STATUS
=over 8
=item B<exit code 0>
Everything is ok
=item B<exit code != 0>
Something is wrong, an error message raises
=back
=head1 DIAGNOSTICS
TODO
=head1 BUGS AND LIMITATIONS
Please report problems you notice to the Sipwise
Development Team <support@sipwise.com>.
=head1 AUTHOR
Kirill Solomko <ksolomko@sipwise.com>
=head1 LICENSE
Copyright (C) 2016 Sipwise GmbH, Austria
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
=cut