#!/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::Tiny; 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, "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; 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_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/) { 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(<prepare(<{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(<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 ?\@?", undef, $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 ?\@?", undef, $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 { if (my $yml = YAML::Tiny->read($GRANTS_SCHEMA)) { $grants = $yml->[0]; } 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(< [I...] =head1 DESCRIPTION B 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 Everything is ok =item B 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 . =head1 AUTHOR Kirill Solomko =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 . =cut