Skip to content

Commit

Permalink
NACaaS Remote Database (#8038)
Browse files Browse the repository at this point in the history
* add remote database configuration to configurator UI

* add remote database fields to assign database method

* remove dup declaration

* rename remote form fields, add remote_password. default remote_port

* use password input

* improve labels

* Test a remote connection

* do not perform secure installation with remotedb

* remove Data::Dumper

* Add support for MySQL

* Add back the username is secureDatabase

* Add back username

* flatten remote, add boolean in payload

* adjust payloads

* Use remote or local to connect to database

* rename root_username -> username, root_password -> password

* Add username to test

* Always send is_remote and remote

* Use the newer call the hanldes remote db setup

* Update version

* Update API

* Change for updated API

* fix namespace

* update pfconfig and database_proxy settings

* Don't delete the database

* Fix typo

* disable e2e unit until after next e2e PR is merged

---------

Co-authored-by: James Rouzier <jrouzier@inverse.ca>
  • Loading branch information
satkunas and jrouzierinverse committed Apr 25, 2024
1 parent 7a020d8 commit 6f36168
Show file tree
Hide file tree
Showing 11 changed files with 464 additions and 62 deletions.
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Depends: ${misc:Depends}, vlan,
# perl uncategorized modules
libapache-htpasswd-perl, libbit-vector-perl, libtext-csv-perl, libtext-csv-xs-perl,
libcgi-session-serialize-yaml-perl, libtimedate-perl, libapache-dbi-perl,
libdbd-mysql-perl, libfile-tail-perl, libnetwork-ipv4addr-perl,
libdbd-mysql-perl (>= 4.052), libfile-tail-perl, libnetwork-ipv4addr-perl,
libiptables-parse-perl, libiptables-chainmgr-perl, iptables (>= 1.4.0), iptables-netflow-dkms,
liblwp-useragent-determined-perl,
liblwp-protocol-connect-perl,
Expand Down
2 changes: 1 addition & 1 deletion html/pfappserver/lib/pfappserver/Form/Config/Pf.pm
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ sub field_list {
type => 'PathUpload',
accessor => $old_name,
config_prefix => $doc_section->{ext},
upload_namespace => $name,
upload_namespace => 'pf',
};
};
}
Expand Down
250 changes: 238 additions & 12 deletions html/pfappserver/lib/pfappserver/Model/DB.pm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use pf::file_paths qw($install_dir);
use pf::error;
use pf::util;
use File::Slurp qw(read_dir);
use File::Temp qw(tempfile);

extends 'Catalyst::Model';

Expand All @@ -37,56 +38,169 @@ our $dbHandler;

sub assign {
my ( $self, $db, $user, $password ) = @_;
return $self->_assign($dbHandler, $db, $user, $password);
}

sub get_db_type {
my ($dbh) = @_;
my $data = $dbh->selectrow_arrayref("SELECT VERSION()");
if (!defined $data || !@$data) {
return
}

my $version = $data->[0];
($version, my $type) = split('-', $version);
$type //= "MySQL";
return ($version, $type);
}

sub assign_database {
my ($self, $args) = @_;
my $logger = get_logger();
my $db = delete $args->{database};
$args->{database} = '';
my ($dbh, undef, $user) = connect_to_database($args);
if (!$dbh) {
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, [ "Error creating the user $user on database $db}"] );
}

my $status_msg;
return $self->_assign($dbh, $db, $args->{pf_username}, $args->{pf_password});
}

$db = $dbHandler->quote_identifier($db);
sub _assign {
my ($self, $dbh, $db, $user, $password ) = @_;
my $logger = get_logger();

my $status_msg;
my ($version, $type) = get_db_type($dbh);
$db = $dbh->quote_identifier($db);

# Create global PF user
foreach my $host ("'%'","localhost") {
my $sql_query = "GRANT SELECT,INSERT,UPDATE,DELETE,EXECUTE,LOCK TABLES,CREATE TEMPORARY TABLES ON $db.* TO ?\@${host} IDENTIFIED BY ?";
$dbHandler->do($sql_query, undef, $user, $password);
my $sql_query = "DROP USER IF EXISTS ?\@${host}";
$dbh->do($sql_query, undef, $user);
if ( $DBI::errstr ) {
$status_msg = "Error creating the user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
$sql_query = "GRANT DROP ON $db.radius_nas TO ?\@${host} IDENTIFIED BY ?";
$dbHandler->do($sql_query, undef, $user, $password);

$sql_query = "CREATE USER ?\@${host} IDENTIFIED BY ?";
$dbh->do($sql_query, undef, $user, $password);
if ( $DBI::errstr ) {
$status_msg = "Error creating the user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
$sql_query = "GRANT SELECT ON mysql.proc TO ?\@${host} IDENTIFIED BY ?";
$dbHandler->do($sql_query, undef, $user, $password);

$sql_query = "GRANT DROP,SELECT,INSERT,UPDATE,DELETE,EXECUTE,LOCK TABLES,CREATE TEMPORARY TABLES ON $db.* TO ?\@${host}";
$dbh->do($sql_query, undef, $user);
if ( $DBI::errstr ) {
$status_msg = "Error creating the user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
$sql_query = "GRANT BINLOG ADMIN ON *.* TO ?\@${host} IDENTIFIED BY ?";
$dbHandler->do($sql_query, undef, $user, $password);

$sql_query = "GRANT CREATE,DROP ON $db.radius_nas TO ?\@${host}";
$dbh->do($sql_query, undef, $user);
if ( $DBI::errstr ) {
$status_msg = "Error granting BINLOG ADMIN for user $user on database";
$status_msg = "Error creating the user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}

if ($type ne 'MySQL') {
$sql_query = "GRANT SELECT ON mysql.proc TO ?\@${host}";
$dbh->do($sql_query, undef, $user);
if ( $DBI::errstr ) {
$status_msg = "Error creating the user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
}


$sql_query = $type eq 'MySQL' ? "GRANT BINLOG_ADMIN ON *.* TO ?\@${host}" : "GRANT BINLOG ADMIN ON *.* TO ?\@${host}";
$dbh->do($sql_query, undef, $user);
if ( $DBI::errstr ) {
$status_msg = "Error granting BINLOG ADMIN for user $user on database $db";
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
}
# Apply the new privileges
$dbHandler->do("FLUSH PRIVILEGES");
$dbh->do("FLUSH PRIVILEGES");
if ( $DBI::errstr ) {
$status_msg = ["Error creating the user [_1] on database [_2]",$user,$db];
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}

$status_msg = ["Successfully created the user [_1] on database [_2]",$user,$db];

# return original status message
return ( $STATUS::OK, $status_msg );
}

sub connect_to_database {
my ($args) = @_;
my $fh;
$args->{database} //= "mysql";
$args->{hostname} ||= "localhost";
if ($args->{remote}{ca_cert}) {
($fh, my $filename) = tempfile();
print $fh $args->{remote}{ca_cert};
$fh->flush();
$fh->close();
$args->{remote}{ca_file} = $filename;
}

my ($connect_str, $user, $password) = make_connection_str($args);
my $dbh = DBI->connect($connect_str, $user, $password);
return $dbh, $args->{database}, $user;
}

sub test_connection {
my ($self, $args) = @_;
my $logger = get_logger();
my ($dbh, $db, $user) = connect_to_database($args);
if ( !$dbh ) {
my $status_msg = ["Error in connection to the database [_1] with user [_2]",$db,$user];
$logger->warn("$DBI::errstr");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}

my $status_msg = ["Successfully connected to the database [_1] with user [_2]",$db,$user];
return ( $STATUS::OK, $status_msg );
}

sub make_connection_str {
my ($args) = @_;
my $dsn = "DBI:mysql:dbname=$args->{database}";
if (!$args->{is_remote}) {
return (
"$dsn;mysql_socket=/var/lib/mysql/mysql.sock",
$args->{username},
$args->{password},
);
}

my $remote = $args->{remote};
$dsn .= ";host=$remote->{hostname}";
my $port = $remote->{port} // '3306';
$dsn .= ";port=$port";
if ($remote->{encryption} eq "tls") {
$dsn .= ";mysql_ssl=1;mysql_ssl_ca_file=$remote->{ca_file}";
}

return (
$dsn,
$remote->{username},
$remote->{password},
);
}

=head2 connect
=cut
Expand Down Expand Up @@ -132,6 +246,118 @@ sub create {
return ( $STATUS::OK, $status_msg );
}

sub make_mysql_command {
my ($args) = @_;
if ($args->{is_remote}) {
return make_remote_mysql_command($args);
}

my $user = quotemeta ($args->{username});
my $password = quotemeta ($args->{password});
my $db = quotemeta ($args->{database});
my $mysql_cmd = "/usr/bin/mysql --socket=/var/lib/mysql/mysql.sock -u $user -p$password $db";
return ($mysql_cmd, undef, "-p$password");
}

sub make_remote_mysql_command {
my ($args) = @_;
my $remote = $args->{remote};
my $mysql_cmd = "/usr/bin/mysql";
if ($remote->{encryption} eq 'tls') {
$mysql_cmd .= " --ssl";
}

if ($remote->{port}) {
$mysql_cmd .= " -P". quotemeta($remote->{port});
}

my $fh = make_file_for_cert($remote);
if ($remote->{ca_file}) {
$mysql_cmd .= " --ssl-ca=". $remote->{ca_file};
}

my $host = quotemeta ($remote->{hostname});
my $user = quotemeta ($remote->{username});
my $password = quotemeta ($remote->{password});
my $db = quotemeta ($args->{database});
$mysql_cmd .= " -h$host -u$user -p$password $db";
return ($mysql_cmd, $fh, "-p$password");
}

sub make_file_for_cert {
my ($remote_args) = @_;
if (!$remote_args->{ca_cert}) {
return undef;
}

my ($fh, $filename) = tempfile();
print $fh $remote_args->{ca_cert};
$fh->flush();
$fh->close();
$remote_args->{ca_file} = $filename;
return $fh;
}

sub apply_schema {
my ( $self, $args) = @_;
my $logger = get_logger();

my ( $status_msg, $result );
my ($mysql_cmd, $fh, $log_strip) = make_mysql_command($args);
my $cmd = "$mysql_cmd < $install_dir/db/pf-schema.sql";
my $db = $args->{database};
eval { $result = pf_run($cmd, (accepted_exit_status => [ 0 ]), log_strip => $log_strip) };
if ( $@ || !defined($result) ) {
$status_msg = ["Error applying the schema to the database [_1]", $db ];
$logger->warn("$@: $result");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
my @custom_schemas = read_dir( "$install_dir/db/custom", prefix => 1, err_mode => 'quiet' ) ;
@custom_schemas = sort @custom_schemas;
foreach my $custom_schema (@custom_schemas) {
my $cmd = "$mysql_cmd < $custom_schema";
eval { $result = pf_run($cmd, (accepted_exit_status => [ 0 ]), log_strip => $log_strip) };
if ( $@ || !defined($result) ) {
$status_msg = ["Error applying the custom schema $custom_schema to the database [_1]", $db ];
$logger->warn("$@: $result");
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}
}

$status_msg = ["Successfully applied the schema to the database [_1]", $db];
# return original status message
return ( $STATUS::OK, $status_msg );
}

=head2 create_database
=cut

sub create_database {
my ( $self, $args ) = @_;
my $db = $args->{database};
my $logger = get_logger();
my ( $status_msg, $result );
my ($dbh, undef, $user) = connect_to_database({%$args, database => ''});
if (!$dbh) {
$status_msg = ["Error in creating the database [_1]",$db];
$logger->warn($DBI::errstr);
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}

my $db_quoted = $dbh->quote_identifier($db);
$result = $dbh->do("CREATE DATABASE $db_quoted DEFAULT CHARACTER SET = 'utf8mb4'");
if ( !$result ) {
$status_msg = ["Error in creating the database [_1]", $db];
$logger->warn($DBI::errstr);
return ( $STATUS::INTERNAL_SERVER_ERROR, $status_msg );
}

$status_msg = ["Successfully created the database [_1]", $db];
# return original status message
return ( $STATUS::OK, $status_msg );
}

=head2 secureInstallation
Intended to integrate the "/usr/bin/mysql_secure_installation" steps
Expand Down
11 changes: 5 additions & 6 deletions html/pfappserver/root/src/utils/regex.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
export const reAscii = value => /^([\x20-\x7E]+)$/.test(value)

export const reAlphaNumeric = value => /^[a-zA-Z0-9]*$/.test(value)

export const reAlphaNumericHyphenUnderscoreDot = value => /^[a-zA-Z0-9-_.]*$/.test(value)

export const reCommonName = value => /^([A-Z]+|[A-Z]+[0-9A-Z_:]*[0-9A-Z]+)$/i.test(value)

export const reDomain = value => /^((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9*]+\.)+[a-zA-Z]{2,}))$/.test(value)

export const reEmail = value => /(^$|^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$)/.test(value)

export const reDomain = value => /^((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9*]+\.)+[a-zA-Z]{2,}))$/.test(value)
export const reFilename = value => /^[^\\/?%*:|"<>]+$/.test(value)

export const reIpv4 = value => /^(([0-9]{1,3}.){3,3}[0-9]{1,3})$/i.test(value)

export const reIpv6 = value => /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/i.test(value)

export const reFilename = value => /^[^\\/?%*:|"<>]+$/.test(value)

export const reMac = value => /^([0-9a-fA-F]{2}[-:]?){5,}([0-9a-fA-F]){2}$/.test(value)

export const reNumeric = value => /^-?[0-9]*$/.test(value)

// eslint-disable-next-line no-useless-escape
export const reStaticRoute = value => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}\/?(\d+)?\s+?(via\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}\s+?)?dev\s+[a-z,0-9\.]+$/i.test(value)


export const reAscii = value => /^([\x20-\x7E]+)$/.test(value)
10 changes: 9 additions & 1 deletion html/pfappserver/root/src/utils/yup.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,14 @@ yup.addMethod(yup.string, 'isFQDN', function (message) {
})
})

yup.addMethod(yup.string, 'isHostname', function (message) {
return this.test({
name: 'isFQDN',
message: message || i18n.t('Invalid hostname.'),
test: value => ['', null, undefined].includes(value) || reIpv4(value) || isFQDN(value)
})
})

yup.addMethod(yup.string, 'isIpv4', function (message) {
return this.test({
name: 'isIpv4',
Expand Down Expand Up @@ -412,7 +420,7 @@ yup.addMethod(yup.string, 'isPort', function (message) {
return this.test({
name: 'isPort',
message: message || i18n.t('Invalid port.'),
test: value => ['', null, undefined].includes(value) || (+value === parseFloat(value) && +value >= 1 && +value <= 65535)
test: value => ['', null, undefined].includes(value) || (+value === parseInt(value) && +value >= 1 && +value <= 65535)
})
})

Expand Down
Loading

0 comments on commit 6f36168

Please sign in to comment.