mirror of https://github.com/sipwise/ngcpcfg.git
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.
623 lines
18 KiB
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
|