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

607 lines
16 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 $DEFAULT_MYSQL_USER => "sipwise";
Readonly my $MYSQL_CREDENTIALS => "/etc/mysql/sipwise.cnf";
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 $recreate_user = 0;
sub Usage {
print <<USAGE;
==
Synchronizes mysql grants from a schema template
==
$PROGRAM_NAME [options]
Options:
--help|-h|-? -- this help
--verbose|-v -- verbose mode
--quiet|-q -- quiet mode
--recreate-user -- recreate user
(useful when all user hosts need to be cleared)
USAGE
exit 0;
}
GetOptions("h|?|help" => \&Usage,
"v|verbose" => \$debug,
"q|quiet" => sub { $debug = -1 },
"recreate-user" => \$recreate_user,
) 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 ($mysql_user, $mysql_pass) = get_mysql_credentials();
$dbh = DBI->connect("DBI:mysql:database=mysql;host=$dbhost;port=$dbport",
$mysql_user, $mysql_pass,
{ PrintError => 0 })
or die "Can't connect to MySQL database 'mysql': ". $DBI::errstr;
log_debug("connected to $dbhost:$dbport as $mysql_user");
$dbh->do("SET sql_log_bin=0")
or die "Cannot set sql_log_bin=0: ".$DBI::errstr;
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 { logger(shift, 2); }
sub get_mysql_credentials {
my $mysql_user = $DEFAULT_MYSQL_USER;
my $mysql_pass;
my $mysql_creds = Config::Tiny->read($MYSQL_CREDENTIALS)
or die "Cannot open $MYSQL_CREDENTIALS: $ERRNO";
if ($mysql_pass = $mysql_creds->{_}{SIPWISE_DB_PASSWORD}) {
$mysql_pass =~ s/^['"]|['"]$//g;
} else {
die "Cannot parse mysql credentials file $MYSQL_CREDENTIALS";
}
return ($mysql_user, $mysql_pass);
}
sub get_hostname {
open(my $fh, "<", "/etc/hostname")
or die "Cannot open /etc/hostname: $ERRNO";
my $hostname = <$fh>;
chomp $hostname;
close $fh;
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 (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);
$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",
$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);
$dbh->do("GRANT $s_grant TO '$user'\@'$host' $s_suffix");
if ($DBI::errstr &&
$DBI::errstr !~ /Table\s+'\S+\.\S+'\s+doesn't\s+exist/) {
die "Cannot grant privileges: ".$DBI::errstr;
} elsif ($DBI::errstr) {
$log_offset = 0;
log_warn("Cannot apply grant: ".$DBI::errstr);
$log_offset = 2;
}
}
if ($new_user) {
set_user_protected_password($user, $host);
}
} else {
die "Unparsable grants structure elemenent: $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 $ch = $dbh->prepare(<<SQL)
DELETE FROM mysql.user WHERE User = ? AND Host LIKE ?
SQL
or die "Cannot prepare: ".$DBI::errstr;
my $ch_sel = $dbh->prepare(<<SQL)
SELECT Host FROM mysql.user WHERE User = ? AND Host LIKE ?
SQL
or die "Cannot prepare: ".$DBI::errstr;
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) {
$host_rx =~ s/\*/%/g;
$ch_sel->execute($user, $host_rx)
or die "Cannot select user $user -- $host_rx: ".$DBI::errstr;
while (my ($host) = $ch_sel->fetchrow_array) {
$ch->execute($user, $host_rx)
or die "Cannot drop user $user -- $host_rx: ".$DBI::errstr;
log_info(sprintf "drop: %s\@%s", $user, $host);
$rc++;
}
}
}
$ch->finish;
$ch_sel->finish;
return $rc;
}
sub set_user_protected_password {
my ($user, $host) = @_;
my ($random_pass) = $dbh->selectrow_array("SELECT PASSWORD(?)",
undef, pwgen());
die "Cannot generate password: ".$DBI::errstr 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 if $DBI::err;
unless ($temp_user_count) {
$dbh->do("CREATE USER '$user'\@'$host'");
die "Cannot create grant temp user: ".$DBI::errstr if $DBI::err;
}
$dbh->do("UPDATE user SET Password = ? WHERE User = ? AND Host = ?",
undef, $random_pass, $user, $host);
die sprintf "Cannot update %s@%s with generated password, %s",
$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;
set_user_protected_password($user, $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/) {
die "Cannot grant privileges: ".$DBI::errstr;
} 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;
$dbh->do("DROP USER '$user'\@'$host'");
die "Cannot drop grant temp user: ".$DBI::errstr if $DBI::err;
@$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",
$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_info("flush privileges");
$dbh->do("FLUSH PRIVILEGES")
or die "Cannot flush privileges: ".$DBI::errstr;
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, $DEFAULT_DBHOST, $DEFAULT_DBPORT);
($dbhost, $dbport) = ($DEFAULT_DBHOST, $DEFAULT_DBPORT);
}
connect_db($dbhost, $dbport);
$dbh->begin_work or die "Cannot start transaction: ".$DBI::errstr;
eval {
my $rc = 0;
foreach my $proc (@{$grants->{order}}) {
SWITCH: for ($proc) {
/^drop$/ && do {
if (apply_drop_users()) {
flush_privs();
}
};
/^hosts$/ && do {
$rc += apply_host_grants();
};
/^hosts$/ && do {
$rc += apply_copy_grants();
};
} # SWITCH
}
if ($rc) {
flush_privs();
}
};
if ($@) {
$dbh->rollback if $dbh;
$dbh->disconnect if $dbh;
die "Error: $@";
}
$dbh->commit or die "Cannot commit transaction: ".$DBI::errstr;
$dbh->disconnect;
if ($recreate_user) {
log_info(<<MSG);
Warning: Recreated users are without passwords,
please consider running 'ngcp-sync-constants' to update passwords for them.
MSG
}
return;
}
main();
exit 0;
# vim: ts=4 sw=4 et
__END__
=pod
=head1 NAME
ngcp-sync-grants - synchronizes mysql grants from a schema template
=head1 SYNOPSIS
B<ngcp-sync-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