diff --git a/Makefile b/Makefile index 95856164..92ba74ad 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ PERL_SCRIPTS = helper/sort-yml \ helper/tt2-daemon \ helper/validate-yml helper/fileformat_version \ sbin/ngcp-network \ - sbin/ngcp-sync-constants + sbin/ngcp-sync-constants \ + sbin/ngcp-sync-grants all: docs @@ -26,11 +27,12 @@ man: xsltproc --nonet /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl docs/ngcpcfg.xml pod2man --section=8 sbin/ngcp-network > ngcp-network.8 pod2man --section=8 sbin/ngcp-sync-constants > ngcp-sync-constants.8 + pod2man --section=8 sbin/ngcp-sync-grants > ngcp-sync-grants.8 clean: rm -f docs/ngcpcfg.xml docs/ngcpcfg.epub docs/ngcpcfg.html docs/ngcpcfg.pdf - rm -f ngcpcfg.8 ngcp-network.8 ngcp-sync-constants.8 + rm -f ngcpcfg.8 ngcp-network.8 ngcp-sync-constants.8 ngcp-sync-grants.8 dist-clean: rm -f docs/ngcpcfg.html docs/ngcpcfg.pdf diff --git a/debian/control b/debian/control index cbf9f892..84718581 100644 --- a/debian/control +++ b/debian/control @@ -6,6 +6,7 @@ Build-Depends: asciidoc, debhelper (>= 9~), docbook-xsl, libclone-perl, + libconfig-tiny-perl, libdata-validate-ip-perl, libdbd-mysql-perl, libdbi-perl, @@ -32,6 +33,7 @@ Depends: etckeeper, file, git (>= 1:1.7.2.5-3~), libclone-perl, + libconfig-tiny-perl, libdata-validate-ip-perl, libdbd-mysql-perl, libdbi-perl, diff --git a/debian/ngcp-ngcpcfg.install b/debian/ngcp-ngcpcfg.install index 2701eb6b..58e1051f 100644 --- a/debian/ngcp-ngcpcfg.install +++ b/debian/ngcp-ngcpcfg.install @@ -11,6 +11,7 @@ helper/validate-yml usr/share/ngcp-ngcpcfg/helper/ lib/* usr/lib/ngcp-ngcpcfg/ sbin/ngcp-network usr/sbin/ sbin/ngcp-sync-constants usr/sbin/ +sbin/ngcp-sync-grants usr/sbin/ sbin/ngcpcfg usr/sbin/ scripts/apply usr/share/ngcp-ngcpcfg/scripts/ scripts/build usr/share/ngcp-ngcpcfg/scripts/ diff --git a/debian/ngcp-ngcpcfg.manpages b/debian/ngcp-ngcpcfg.manpages index 1378aeb6..d3f8c0b9 100644 --- a/debian/ngcp-ngcpcfg.manpages +++ b/debian/ngcp-ngcpcfg.manpages @@ -1,3 +1,4 @@ ngcpcfg.8 ngcp-network.8 ngcp-sync-constants.8 +ngcp-sync-grants.8 diff --git a/sbin/ngcp-sync-grants b/sbin/ngcp-sync-grants new file mode 100755 index 00000000..9b980120 --- /dev/null +++ b/sbin/ngcp-sync-grants @@ -0,0 +1,493 @@ +#!/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"; + +my $grants = {}; +my $dbh; +my $debug = 0; +my $log_offset = 0; + +my $recreate_user = 0; + +sub Usage { + print < \&Usage, + "v|verbose" => \$debug, + "q|quiet" => sub { $debug = -1 }, + "recreate-user" => \$recreate_user, + ) or die Usage(); + +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; + printf "-->%s %s\n", " "x$offset, $str; + + return; +} + +sub log_debug { logger(shift, 1); } +sub log_info { logger(shift, 0); } + +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}) { + die sprintf "Unknown key %s in %s with idx=%d in ref %s", + $key, join('.', @$data), $idx, $ref + } + if (ref $ptr->{$key} eq 'HASH') { + $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; + log_debug(sprintf "[%s]%s", join('.', @$data), $as ? " as $as" : ''); + if (!$as && $recreate_user) { + if (apply_drop_users($user)) { + flush_privs(); + } + } else { + return 0 unless check_grants($ptr->{$key}, $user, $host); + unless ($debug > 0) { + log_info(sprintf "[%s]%s", join('.', @$data), $as ? " as $as" : ''); + } + $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 + && + ($DBI::errstr !~ /There is no such grant defined/ && + $DBI::errstr !~ + /revoke all privileges for one or more of the requested users/ + )) { + die sprintf "Cannot revoke privileges from %s\@%s: %s", + $user, $host, $DBI::errstr; + } + } + $rc++; + foreach my $grant (@{$ptr->{$key}}) { + $log_offset = 2; + log_info(sprintf "grant %s to %s\@%s", $grant, $user, $host); + $dbh->do("GRANT $grant TO $user\@$host") + or die "Cannot grant privileges: ".$DBI::errstr; + } + } 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 normalise_grant_str { + my $grant = shift; + $grant =~ s/^grant\s+//i; + $grant =~ s/^(.+)\s+TO.+$/$1/i; + $grant =~ s/`//g; + $grant =~ s/,\s+/,/g; + $grant =~ s/all\s+on/all privileges on/; + $grant = lc $grant; + if ($grant =~ /,/) { + $grant =~ /^(.+)\s+(on\s+.+)$/i; + my $allow = $1; + my $on = $2; + my %sorted; + my @order = qw(select insert update delete); + foreach (split /,/, $allow) { + for (my $i=0;$i<=$#order;$i++) { + if ($_ eq $order[$i]) { + $sorted{$i} = $_; + } + } + } + $grant = join ',', map { $sorted{$_} } sort { $a <=> $b } keys %sorted; + $grant .= ' '.$on; + } + + return $grant; +} + +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/) { + 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) { + $check = normalise_grant_str($check); + 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 $db_cfg = Config::Tiny->read($DB_CFG) + or die "Cannot open $DB_CFG: $ERRNO"; + + connect_db(@{$db_cfg->{_}}{qw(LOCAL_DBHOST LOCAL_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(< + +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 DESCRIPTION + +B synchronizes mysql grants from a schema template. + +=head1 EXIT STATUS + +=over 8 + +=item B +Everything is ok + +=item B +Something is wrong, an error message raises + +=back + +=head1 REQUIRED ARGUMENTS + +None + +=head1 CONFIGURATION + +None + +=head1 DEPENDENCIES + +ngcp-sync-grants relies on a bunch of Perl modules, all of them specified as +dependencies through the Debian package. + +=head1 DIAGNOSTICS + +TODO + +=head1 INCOMPATIBILITIES + +No known at this time. + +=head1 BUGS AND LIMITATIONS + +Please report problems you notice to the Sipwise +Development Team . + +=head1 AUTHOR + +Kirill Solomko + +=head1 LICENSE AND COPYRIGHT + +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 diff --git a/scripts/commit b/scripts/commit index 6eb3dda6..a0e3cbcb 100755 --- a/scripts/commit +++ b/scripts/commit @@ -74,7 +74,8 @@ log_debug "${SCRIPTS}/etckeeper" "${SCRIPTS}"/etckeeper >/dev/null if [ -z "${NO_DB_SYNC:-}" ] ; then - log_info "Synchronizing data from ${CONSTANTS_CONFIG}" + log_info "Synchronizing MySQL grants/credentials" + ngcp-sync-grants | sed "s/^/$timestamp_replacementchars/" ngcp-sync-constants >/dev/null | sed "s/^/$timestamp_replacementchars/" else log_debug "no-db-sync: skipping 'ngcp-sync-constants'"