From e133340459b089021afe61042f59c58a2bd71937 Mon Sep 17 00:00:00 2001 From: Jake Deery Date: Tue, 8 Oct 2024 17:14:52 +0000 Subject: [PATCH] Bug 35761: Introduce central FTP/SFTP management This patch adds a central FTP/SFTP management UI, with an associated database object and class, plus the beginnings of an API. We can use this code as a building block for adding FTP/SFTP connections to other parts of the system, as well as to improve the usability of the EDI transports. It is also now possible to test these connections from within the Koha UI. To test:- You will need access to an FTP or SFTP server Contact me if you need a disposable FTP/SFTP server a) Go to Koha Administration b) Notice no FTP/SFTP referenced on this page c) APPLY PATCH, reset_all d) Create new user, let's say sftpuser e) Set permissions for new user to include: 1) catalogue 2) parameters -> manage_sftp_servers f) Log in with this new user, go back to Koha Administration 1) Notice the FTP/SFTP servers option g) Undo step e2) for the sftpuser via the koha admin user 1) Refresh the Koha Administration page as the sftpuser 2) Notice how FTP/SFTP servers has disappeared h) Back to koha admin, go to Koha Administration -> FTP/SFTP Servers -> New i) Complete form depending on the FTP or SFTP server you have access to, Submit 1) Notice you are prompted to confirm before saving j) Select Edit, observe your details are the same, and Submit again k) Repeat step J one additional time l) Select Test, observe all tests passing m) Select Delete, confirm deletion, and observe server disappear n) Optionally, run prove on new tests 1) t/Koha/Auth/Permissions.t 2) t/db_dependent/Koha/SFTP/Server.t 3) t/db_dependent/api/v1/sftp_servers.t o) SIGN-OFF Signed-off-by: Andrew Fuerste Henry Signed-off-by: Martin Renvoize --- Koha/REST/V1/Config/SFTP/Servers.pm | 184 ++++ Koha/REST/V1/SFTPServer.pm | 55 + Koha/SFTP/Server.pm | 446 ++++++++ Koha/SFTP/Servers.pm | 55 + admin/sftp_servers.pl | 234 +++++ api/v1/swagger/definitions/sftp_server.yaml | 69 ++ api/v1/swagger/paths/config_sftp_servers.yaml | 236 +++++ api/v1/swagger/paths/test_sftp_servers.yaml | 48 + api/v1/swagger/swagger.yaml | 17 + .../prog/en/includes/admin-menu.inc | 5 +- .../prog/en/includes/permissions.inc | 3 + .../prog/en/modules/admin/admin-home.tt | 6 +- .../prog/en/modules/admin/sftp_servers.tt | 963 ++++++++++++++++++ t/Koha/Auth/Permissions.t | 1 + t/db_dependent/Koha/SFTP/Server.t | 167 +++ t/db_dependent/api/v1/sftp_servers.t | 443 ++++++++ 16 files changed, 2930 insertions(+), 2 deletions(-) create mode 100644 Koha/REST/V1/Config/SFTP/Servers.pm create mode 100644 Koha/REST/V1/SFTPServer.pm create mode 100644 Koha/SFTP/Server.pm create mode 100644 Koha/SFTP/Servers.pm create mode 100755 admin/sftp_servers.pl create mode 100644 api/v1/swagger/definitions/sftp_server.yaml create mode 100644 api/v1/swagger/paths/config_sftp_servers.yaml create mode 100644 api/v1/swagger/paths/test_sftp_servers.yaml create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/sftp_servers.tt create mode 100755 t/db_dependent/Koha/SFTP/Server.t create mode 100755 t/db_dependent/api/v1/sftp_servers.t diff --git a/Koha/REST/V1/Config/SFTP/Servers.pm b/Koha/REST/V1/Config/SFTP/Servers.pm new file mode 100644 index 00000000000..f6f40668b1e --- /dev/null +++ b/Koha/REST/V1/Config/SFTP/Servers.pm @@ -0,0 +1,184 @@ +package Koha::REST::V1::Config::SFTP::Servers; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::SFTP::Servers; + +use Try::Tiny qw( catch try ); + +=head1 API + +=head2 Methods + +=head3 list + +Controller method that handles listing Koha::SFTP::Server objects + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $sftp_servers_set = Koha::SFTP::Servers->new; + my $sftp_servers = $c->objects->search($sftp_servers_set); + return $c->render( + status => 200, + openapi => $sftp_servers + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +Controller method that handles retrieving a single Koha::SFTP::Server object + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $sftp_server = Koha::SFTP::Servers->find( $c->param('sftp_server_id') ); + + return $c->render_resource_not_found("FTP/SFTP server") + unless $sftp_server; + + return $c->render( + status => 200, + openapi => $c->objects->to_api($sftp_server), + ); + } catch { + $c->unhandled_exception($_); + } +} + +=head3 add + +Controller method that handles adding a new Koha::SFTP::Server object + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + + my $sftp_server = Koha::SFTP::Server->new_from_api( $c->req->json ); + $sftp_server->store->discard_changes; + + $c->res->headers->location( $c->req->url->to_string . '/' . $sftp_server->id ); + + return $c->render( + status => 201, + openapi => $c->objects->to_api($sftp_server), + ); + } catch { + if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { + return $c->render( + status => 409, + openapi => { + error => $_->error, + conflict => $_->duplicate_id + } + ); + } + + $c->unhandled_exception($_); + }; +} + +=head3 update + +Controller method that handles updating a Koha::SFTP::Server object + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $sftp_server = Koha::SFTP::Servers->find( $c->param('sftp_server_id') ); + + return $c->render_resource_not_found("FTP/SFTP server") + unless $sftp_server; + + return try { + $sftp_server->set_from_api( $c->req->json ); + $sftp_server->store->discard_changes; + + return $c->render( + status => 200, + openapi => $c->objects->to_api($sftp_server), + ); + } catch { + if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { + return $c->render( + status => 409, + openapi => { + error => $_->error, + conflict => $_->duplicate_id + } + ); + } + + $c->unhandled_exception($_); + }; +} + +=head3 delete + +Controller method that handles deleting a Koha::SFTP::Server object + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $sftp_server = Koha::SFTP::Servers->find( $c->param('sftp_server_id') ); + + return $c->render_resource_not_found("FTP/SFTP server") + unless $sftp_server; + + return try { + $sftp_server->delete; + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 test + +Controller method that invokes Koha::SFTP::Server->test_conn + +=cut + +sub test { + my $c = shift->openapi->valid_input or return; + + my $sftp_server = Koha::SFTP::Servers->find( $c->param('sftp_server_id') ); + + return $c->render_resource_not_found("FTP/SFTP server") + unless $sftp_server; +} + +1; diff --git a/Koha/REST/V1/SFTPServer.pm b/Koha/REST/V1/SFTPServer.pm new file mode 100644 index 00000000000..d0a5641e1eb --- /dev/null +++ b/Koha/REST/V1/SFTPServer.pm @@ -0,0 +1,55 @@ +package Koha::REST::V1::SFTPServer; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::SFTP::Servers; + +use Try::Tiny qw( catch try ); + +=head1 API + +=head2 Methods + +=head3 test + +Controller method that invokes Koha::SFTP::Server->test_conn + +=cut + +sub test { + my $c = shift->openapi->valid_input or return; + + return try { + my $sftp_server = Koha::SFTP::Servers->find( $c->param('sftp_server_id') ); + return $c->render_resource_not_found("FTP/SFTP Server") + unless $sftp_server; + + my $sftp_server_test_conn = $sftp_server->test_conn; + + return $c->render( + status => 200, + openapi => $sftp_server_test_conn, + ); + } catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/Koha/SFTP/Server.pm b/Koha/SFTP/Server.pm new file mode 100644 index 00000000000..0db73011c1b --- /dev/null +++ b/Koha/SFTP/Server.pm @@ -0,0 +1,446 @@ +package Koha::SFTP::Server; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::Exceptions::Object; +use Koha::Encryption; +use Koha::Logger; +use Koha::SFTP::Servers; + +use Try::Tiny qw( catch try ); +use Mojo::JSON; +use Net::SFTP::Foreign; +use Net::FTP; +use File::Spec; +use IO::File; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::SFTP::Server - Koha SFTP Server Object class + +=head1 API + +=head2 Class methods + +=head3 store + + $server->store; + +Overloaded store method that ensures directory paths end with a forward slash. + +=cut + +sub store { + my ($self) = @_; + + for my $dir_field (qw( download_directory upload_directory )) { + my $dir = $self->$dir_field; + next unless $dir && $dir ne ''; + + $self->$dir_field( $dir . '/' ) unless substr( $dir, -1 ) eq '/'; + } + + return $self->SUPER::store; +} + +=head3 to_api + + my $json = $sftp_server->to_api; + +Returns a JSON representation of the object suitable for API output, +excluding sensitive data. + +=cut + +sub to_api { + my ( $self, $params ) = @_; + + my $json_sftp = $self->SUPER::to_api($params) or return; + delete $json_sftp->{password}; + delete $json_sftp->{key_file}; + + return $json_sftp; +} + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::SFTP::Server object +on the API. + +=cut + +sub to_api_mapping { + return { id => 'sftp_server_id' }; +} + +=head3 plain_text_password + + my $password = $server->plain_text_password; + +Returns the decrypted plaintext password. + +=cut + +sub plain_text_password { + my ($self) = @_; + return unless $self->password; + return Koha::Encryption->new->decrypt_hex( $self->password ); +} + +=head3 plain_text_key + + my $key = $server->plain_text_key; + +Returns the decrypted plaintext key file. + +=cut + +sub plain_text_key { + my ($self) = @_; + return unless $self->key_file; + return Koha::Encryption->new->decrypt_hex( $self->key_file ) . "\n"; +} + +=head3 write_key_file + + my $success = $server->write_key_file; + +Writes the keyfile from the db into a file. + +Returns 1 on success, undef on failure. + +=cut + +sub write_key_file { + my ($self) = @_; + + my $upload_path = C4::Context->config('upload_path') or return; + my $logger = Koha::Logger->get; + my $key_path = File::Spec->catdir( $upload_path, 'ssh_keys' ); + my $key_file = File::Spec->catfile( $key_path, 'id_ssh_' . $self->id ); + + mkdir $key_path unless -d $key_path; + unlink $key_file if -f $key_file; + + my $fh = IO::File->new( $key_file, 'w' ) or return; + + try { + chmod 0600, $key_file if -f $key_file; + print $fh $self->plain_text_key; + close $fh or $logger->warn("Failed to close key file: $!"); + return 1; + } catch { + $logger->warn("Error writing key file: $_"); + close $fh; + return; + }; +} + +=head3 locate_key_file + + my $path = $server->locate_key_file; + +Returns the keyfile's path if it exists, undef otherwise. + +=cut + +sub locate_key_file { + my ($self) = @_; + + my $upload_path = C4::Context->config('upload_path') or return; + my $key_file = File::Spec->catfile( + $upload_path, + 'ssh_keys', + 'id_ssh_' . $self->id + ); + + return ( -f $key_file ) ? $key_file : undef; +} + +=head3 update_password + + my $success = $server->update_password; + +Update the server's encrypted password. + +=cut + +sub update_password { + my ( $self, $value ) = @_; + return unless defined $value; + + $self->password( Koha::Encryption->new->encrypt_hex($value) ); + return $self->SUPER::store; +} + +=head3 update_key_file + + my $success = $server->update_key_file($new_key_file); + +Update the server's encrypted key file. + +=cut + +sub update_key_file { + my ( $self, $value ) = @_; + return unless defined $value; + + $self->key_file( Koha::Encryption->new->encrypt_hex( _dos2unix($value) ) ); + return $self->SUPER::store; +} + +=head3 update_status + + my $success = $server->update_status($new_status); + +Update the server's status + +=cut + +sub update_status { + my ( $self, $value ) = @_; + return unless defined $value; + + return $self->set( { status => $value } )->store; +} + +=head3 test_conn + + my ($success, $results) = $server->test_conn; + +Tests connection to the server. Returns success flag and detailed test results. + +=cut + +sub test_conn { + my ($self) = @_; + my $default_result = { + passed => Mojo::JSON->false, + err => undef, + msg => undef, + }; + my $sftp_test_results; + + if ( $self->transport eq 'sftp' ) { + $sftp_test_results->{'1_sftp_conn'} = {%$default_result}; + my $sftp = Net::SFTP::Foreign->new( + host => $self->host, + user => $self->user_name, + password => $self->plain_text_password, + key_path => $self->locate_key_file, + port => $self->port, + timeout => 10, + more => [ + qw(-vv), + qw(-o StrictHostKeyChecking=no), + ], + ); + unless ( $sftp->error ) { + $sftp_test_results->{'1_sftp_conn'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'1_sftp_conn'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'1_sftp_conn'}->{'err'} = $sftp->error; + } + + unless ( $sftp->error ) { + unless ( $self->download_directory eq '' ) { + $sftp_test_results->{'2a_sftp_cwd_dl'} = {%$default_result}; + $sftp->setcwd( $self->download_directory ); + unless ( $sftp->error ) { + $sftp_test_results->{'2a_sftp_cwd_dl'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'2a_sftp_cwd_dl'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'2a_sftp_cwd_dl'}->{'err'} = $sftp->error; + } + } + + $sftp_test_results->{'2b_sftp_ls_dl'} = {%$default_result}; + $sftp->ls(); + unless ( $sftp->error ) { + $sftp_test_results->{'2b_sftp_ls_dl'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'2b_sftp_ls_dl'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'2b_sftp_ls_dl'}->{'err'} = $sftp->error; + } + + unless ( $self->upload_directory eq '' ) { + $sftp_test_results->{'2c_sftp_cwd_ul'} = {%$default_result}; + $sftp->setcwd( $self->upload_directory ); + unless ( $sftp->error ) { + $sftp_test_results->{'2c_sftp_cwd_ul'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'2c_sftp_cwd_ul'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'2c_sftp_cwd_ul'}->{'err'} = $sftp->error; + } + } + + $sftp_test_results->{'2d_sftp_ls_ul'} = {%$default_result}; + $sftp->ls( $self->upload_directory ); + unless ( $sftp->error ) { + $sftp_test_results->{'2d_sftp_ls_ul'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'2d_sftp_ls_ul'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'2d_sftp_ls_ul'}->{'err'} = $sftp->error; + } + + $sftp_test_results->{'3_sftp_write'} = {%$default_result}; + open my $fh, '<', \"Hello, world!\n"; + close $fh if ( $sftp->put( $fh, $self->upload_directory . '.koha_test_file' ) ); + unless ( $sftp->error ) { + $sftp_test_results->{'3_sftp_write'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'3_sftp_write'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'3_sftp_write'}->{'err'} = $sftp->error; + } + + $sftp_test_results->{'4_sftp_del'} = {%$default_result}; + $sftp->remove( $self->upload_directory . '.koha_test_file' ); + unless ( $sftp->error ) { + $sftp_test_results->{'4_sftp_del'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'4_sftp_del'}->{'msg'} = $sftp->status; + } else { + $sftp_test_results->{'4_sftp_del'}->{'err'} = $sftp->error; + } + } + } elsif ( $self->transport eq 'ftp' ) { + $sftp_test_results->{'1_ftp_conn'} = {%$default_result}; + my $ftp = Net::FTP->new( + $self->host, + Port => $self->port, + Timeout => 10, + Passive => ( scalar $self->passive ) ? 1 : 0, + ); + if ($ftp) { + $sftp_test_results->{'1_ftp_conn'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'1_ftp_conn'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'1_ftp_conn'}->{'err'} = 'cannot connect to ' . $self->host . ': ' . $@; + } + + if ($ftp) { + $sftp_test_results->{'2_ftp_login'} = {%$default_result}; + my $login = $ftp->login( + $self->user_name, + $self->plain_text_password, + ); + if ($login) { + $sftp_test_results->{'2_ftp_login'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'2_ftp_login'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'2_ftp_login'}->{'err'} = $ftp->message; + } + + unless ( $self->download_directory eq '' ) { + $sftp_test_results->{'3a_ftp_cwd_dl'} = {%$default_result}; + my $cwd_dl = $ftp->cwd( $self->download_directory ); + if ($cwd_dl) { + $sftp_test_results->{'3a_ftp_cwd_dl'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'3a_ftp_cwd_dl'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'3a_ftp_cwd_dl'}->{'err'} = $ftp->message; + } + } + + $sftp_test_results->{'3b_ftp_ls_dl'} = {%$default_result}; + my $ls_dl = $ftp->ls(); + if ($ls_dl) { + $sftp_test_results->{'3b_ftp_ls_dl'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'3b_ftp_ls_dl'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'3b_ftp_ls_dl'}->{'err'} = $ftp->message; + } + + unless ( $self->upload_directory eq '' ) { + $sftp_test_results->{'3c_ftp_cwd_ul'} = {%$default_result}; + my $cwd_ul = $ftp->cwd( $self->upload_directory ); + if ($cwd_ul) { + $sftp_test_results->{'3c_ftp_cwd_ul'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'3c_ftp_cwd_ul'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'3c_ftp_cwd_ul'}->{'err'} = $ftp->message; + } + } + + $sftp_test_results->{'3d_ftp_ls_ul'} = {%$default_result}; + my $ls_ul = $ftp->ls(); + if ($ls_ul) { + $sftp_test_results->{'3d_ftp_ls_ul'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'3d_ftp_ls_ul'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'3d_ftp_ls_ul'}->{'err'} = $ftp->message; + } + + $sftp_test_results->{'4_ftp_write'} = {%$default_result}; + open my $fh, '<', \"Hello, world!\n"; + close $fh if ( my $put = $ftp->put( $fh, $self->upload_directory . '.koha_test_file' ) ); + if ($put) { + $sftp_test_results->{'4_ftp_write'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'4_ftp_write'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'4_ftp_write'}->{'err'} = $ftp->message; + } + + $sftp_test_results->{'5_ftp_del'} = {%$default_result}; + my $delete = $ftp->delete( $self->upload_directory . '.koha_test_file' ); + if ($delete) { + $sftp_test_results->{'5_ftp_del'}->{'passed'} = Mojo::JSON->true; + $sftp_test_results->{'5_ftp_del'}->{'msg'} = $ftp->message; + } else { + $sftp_test_results->{'5_ftp_del'}->{'err'} = $ftp->message; + } + } + } + + $self->update_status('tests_ok'); + foreach my $val ( values %$sftp_test_results ) { + if ( defined $val->{'err'} ) { + $self->update_status('tests_failed'); + } + } + + return ( 1, $sftp_test_results ); +} + +=head2 Internal methods + +=head3 _dos2unix + +Return a CR-free string from an input + +=cut + +sub _dos2unix { + my $dosStr = shift; + + return $dosStr =~ s/\015\012/\012/gr; +} + +=head3 _type + +Return type of Object relating to Schema ResultSet + +=cut + +sub _type { + return 'SftpServer'; +} + +1; diff --git a/Koha/SFTP/Servers.pm b/Koha/SFTP/Servers.pm new file mode 100644 index 00000000000..6859bbe2e45 --- /dev/null +++ b/Koha/SFTP/Servers.pm @@ -0,0 +1,55 @@ +package Koha::SFTP::Servers; + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::Exceptions; + +use Koha::SFTP::Server; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::SFTP::Servers - Koha SFTP Server Object set class + +=head1 API + +=head2 Internal methods + +=head3 _type + +Return type of object, relating to Schema ResultSet + +=cut + +sub _type { + return 'SftpServer'; +} + +=head3 object_class + +Return object class + +=cut + +sub object_class { + return 'Koha::SFTP::Server'; +} + +1; diff --git a/admin/sftp_servers.pl b/admin/sftp_servers.pl new file mode 100755 index 00000000000..94be222ac5c --- /dev/null +++ b/admin/sftp_servers.pl @@ -0,0 +1,234 @@ +#!/usr/bin/perl + +# Copyright 2024 PTFS Europe Ltd +# +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use CGI qw ( -utf8 ); +use Scalar::Util qw( blessed ); +use Try::Tiny qw( catch try ); + +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); + +use Koha::SFTP::Servers; + +my $input = CGI->new; +my $op = $input->param('op') || 'list'; + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { + template_name => "admin/sftp_servers.tt", + query => $input, + type => "intranet", + flagsrequired => { parameters => 'manage_sftp_servers' }, + } +); + +my @messages; + +my $sftp_servers = Koha::SFTP::Servers->search; + +if ( $op eq 'cud-add' ) { + my $name = $input->param('sftp_name'); + my $host = $input->param('sftp_host') || 'localhost'; + my $port = $input->param('sftp_port') || 22; + my $transport = $input->param('sftp_transport') || 'sftp'; + my $passive = ( scalar $input->param('sftp_passiv') ) ? 1 : 0; + my $auth_mode = $input->param('sftp_auth_mode') || 'password'; + my $user_name = $input->param('sftp_user_name') || undef; + my $password = $input->param('sftp_password') || undef; + my $key_file = $input->param('sftp_key_file') || undef; + my $download_directory = $input->param('sftp_download_directory') || undef; + my $upload_directory = $input->param('sftp_upload_directory') || undef; + my $status = $input->param('sftp_status') || ''; + my $debug = ( scalar $input->param('sftp_debug_mode') ) ? 1 : 0; + + try { + my $sftp_server = Koha::SFTP::Server->new( + { + name => $name, + host => $host, + port => $port, + transport => $transport, + passive => $passive, + auth_mode => $auth_mode, + user_name => $user_name, + download_directory => $download_directory, + upload_directory => $upload_directory, + status => $status, + debug => $debug, + } + )->store; + + $sftp_server->update_password($password) + if ($password); + + $sftp_server->update_key_file($key_file) + if ($key_file); + + push @messages, { + type => 'message', + code => 'success_on_insert' + }; + } catch { + if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { + push @messages, { + type => 'alert', + code => 'error_on_insert', + reason => 'duplicate_id' + }; + } + }; + + # list servers after adding + $op = 'list'; + +} elsif ( $op eq 'edit_form' ) { + my $sftp_server_id = $input->param('sftp_server_id'); + my $sftp_server; + my $sftp_server_plain_text_password; + my $sftp_server_plain_text_key; + + $sftp_server = Koha::SFTP::Servers->find($sftp_server_id) + unless !$sftp_server_id; + + unless ( !$sftp_server ) { + $sftp_server_plain_text_password = $sftp_server->plain_text_password || ''; + $sftp_server_plain_text_key = $sftp_server->plain_text_key || ''; + } + + if ($sftp_server) { + $template->param( + sftp_server => $sftp_server, + sftp_server_plain_text_password => $sftp_server_plain_text_password, + sftp_server_plain_text_key => $sftp_server_plain_text_key, + ); + } else { + push @messages, { + type => 'alert', + code => 'error_on_edit', + reason => 'invalid_id' + }; + } + +} elsif ( $op eq 'cud-edit_save' ) { + my $sftp_server_id = $input->param('sftp_server_id'); + my $sftp_server_plain_text_password; + my $sftp_server; + + $sftp_server = Koha::SFTP::Servers->find($sftp_server_id) + unless !$sftp_server_id; + + $sftp_server_plain_text_password = $sftp_server->plain_text_password + unless !$sftp_server_id; + + if ($sftp_server) { + my $name = $input->param('sftp_name'); + my $host = $input->param('sftp_host') || 'localhost'; + my $port = $input->param('sftp_port') || 22; + my $transport = $input->param('sftp_transport') || 'sftp'; + my $passive = ( scalar $input->param('sftp_passiv') ) ? 1 : 0; + my $auth_mode = $input->param('sftp_auth_mode') || 'password'; + my $user_name = $input->param('sftp_user_name') || undef; + my $password = $input->param('sftp_password') || undef; + my $key_file = $input->param('sftp_key_file') || undef; + my $download_directory = $input->param('sftp_download_directory') || undef; + my $upload_directory = $input->param('sftp_upload_directory') || undef; + my $status = $input->param('sftp_status') || ''; + my $debug = ( scalar $input->param('sftp_debug_mode') ) ? 1 : 0; + + try { + $sftp_server->set( + { + name => $name, + host => $host, + port => $port, + transport => $transport, + passive => $passive, + auth_mode => $auth_mode, + user_name => $user_name, + download_directory => $download_directory, + upload_directory => $upload_directory, + status => $status, + debug => $debug, + } + )->store; + + $sftp_server->update_password($password) + if ($password); + + $sftp_server->update_key_file($key_file) + if ($key_file); + + push @messages, { + type => 'message', + code => 'success_on_update' + }; + + } catch { + + push @messages, { + type => 'alert', + code => 'error_on_update' + }; + + }; + + # list servers after adding + $op = 'list'; + } else { + push @messages, { + type => 'alert', + code => 'error_on_update', + reason => 'invalid_id' + }; + } + +} elsif ( $op eq 'test_form' ) { + my $sftp_server_id = $input->param('sftp_server_id'); + my $sftp_server; + + $sftp_server = Koha::SFTP::Servers->find($sftp_server_id) + unless !$sftp_server_id; + + if ($sftp_server) { + $template->param( + sftp_server => $sftp_server, + ); + } else { + push @messages, { + type => 'alert', + code => 'error_on_test', + reason => 'invalid_id', + }; + } +} + +if ( $op eq 'list' ) { + $template->param( + servers_count => $sftp_servers->count, + ); +} + +$template->param( + op => $op, + messages => \@messages, +); + +output_html_with_http_headers $input, $cookie, $template->output; diff --git a/api/v1/swagger/definitions/sftp_server.yaml b/api/v1/swagger/definitions/sftp_server.yaml new file mode 100644 index 00000000000..cbc92605882 --- /dev/null +++ b/api/v1/swagger/definitions/sftp_server.yaml @@ -0,0 +1,69 @@ +--- +type: object +properties: + sftp_server_id: + type: integer + description: Internal FTP/SFTP server identifier + readOnly: true + name: + type: string + description: Name of the FTP/SFTP server + host: + type: string + description: FTP/SFTP host name (optional) + port: + type: integer + description: TCP port number (optional) + transport: + type: string + enum: + - ftp + - sftp + - file + description: Transport of FTP/SFTP server (optional) + auth_mode: + type: string + enum: + - password + - key_file + - noauth + description: Authentication mode to use (optional) + passive: + type: boolean + description: Whether or not to use passive mode (optional) + user_name: + type: + - string + - "null" + description: The user name to use for authentication (optional) + password: + type: + - string + - "null" + description: The password to use for authentication (optional) + key_file: + type: + - string + - "null" + description: The key file to use for authentication (optional) + download_directory: + type: + - string + - "null" + description: Path on the remote server we download from (optional) + upload_directory: + type: + - string + - "null" + description: Path on the remote server we upload to (optional) + status: + type: + - string + - "null" + description: Most recent status of the FTP/SFTP server (optional) + debug: + type: boolean + description: If the SMTP connection is set to debug mode (optional) +additionalProperties: false +required: + - name diff --git a/api/v1/swagger/paths/config_sftp_servers.yaml b/api/v1/swagger/paths/config_sftp_servers.yaml new file mode 100644 index 00000000000..e634760a637 --- /dev/null +++ b/api/v1/swagger/paths/config_sftp_servers.yaml @@ -0,0 +1,236 @@ +--- +/config/sftp_servers: + get: + x-mojo-to: Config::SFTP::Servers#list + operationId: listSFTPServers + tags: + - sftp_servers + summary: List FTP/SFTP servers + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + responses: + "200": + description: A list of FTP/SFTP servers + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/sftp_server" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers + post: + x-mojo-to: Config::SFTP::Servers#add + operationId: addSFTPServer + tags: + - sftp_servers + summary: Add FTP/SFTP server + parameters: + - name: body + in: body + description: A JSON object representing a new FTP/SFTP server configuration + required: true + schema: + $ref: "../swagger.yaml#/definitions/sftp_server" + produces: + - application/json + responses: + "201": + description: An FTP/SFTP server object + schema: + $ref: "../swagger.yaml#/definitions/sftp_server" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating resource + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers +"/config/sftp_servers/{sftp_server_id}": + get: + x-mojo-to: Config::SFTP::Servers#get + operationId: getSFTPServer + tags: + - sftp_servers + summary: Get FTP/SFTP server + parameters: + - $ref: "../swagger.yaml#/parameters/sftp_server_id_pp" + produces: + - application/json + responses: + "200": + description: An FTP/SFTP server object + schema: + $ref: "../swagger.yaml#/definitions/sftp_server" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Object not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict updating resource + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers + put: + x-mojo-to: Config::SFTP::Servers#update + operationId: updateSFTPServer + tags: + - sftp_servers + summary: Update FTP/SFTP server + parameters: + - $ref: "../swagger.yaml#/parameters/sftp_server_id_pp" + - name: body + in: body + description: An FTP/SFTP server object + required: true + schema: + $ref: "../swagger.yaml#/definitions/sftp_server" + produces: + - application/json + responses: + "200": + description: An FTP/SFTP server object + schema: + $ref: "../swagger.yaml#/definitions/sftp_server" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Object not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers + delete: + x-mojo-to: Config::SFTP::Servers#delete + operationId: deleteSFTPServer + tags: + - sftp_servers + summary: Delete FTP/SFTP server + parameters: + - $ref: "../swagger.yaml#/parameters/sftp_server_id_pp" + produces: + - application/json + responses: + "204": + description: FTP/SFTP server deleted + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Object not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers \ No newline at end of file diff --git a/api/v1/swagger/paths/test_sftp_servers.yaml b/api/v1/swagger/paths/test_sftp_servers.yaml new file mode 100644 index 00000000000..3fab229fc28 --- /dev/null +++ b/api/v1/swagger/paths/test_sftp_servers.yaml @@ -0,0 +1,48 @@ +--- +"/sftp_server/{sftp_server_id}/test_connection": + get: + x-mojo-to: SFTPServer#test + operationId: testSFTPServer + tags: + - sftp_servers + summary: Test FTP/SFTP server + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/sftp_server_id_pp" + responses: + "200": + description: Results of FTP/SFTP server test + schema: + type: object + items: + $ref: "../swagger.yaml#/definitions/sftp_server" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not Found + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_sftp_servers \ No newline at end of file diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 398b192a76c..03c1d51ad0b 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -176,6 +176,8 @@ definitions: $ref: ./definitions/search_filter.yaml smtp_server: $ref: ./definitions/smtp_server.yaml + sftp_server: + $ref: ./definitions/sftp_server.yaml suggestion: $ref: ./definitions/suggestion.yaml ticket: @@ -307,6 +309,10 @@ paths: $ref: ./paths/config_smtp_servers.yaml#/~1config~1smtp_servers "/config/smtp_servers/{smtp_server_id}": $ref: "./paths/config_smtp_servers.yaml#/~1config~1smtp_servers~1{smtp_server_id}" + /config/sftp_servers: + $ref: ./paths/config_sftp_servers.yaml#/~1config~1sftp_servers + "/config/sftp_servers/{sftp_server_id}": + $ref: "./paths/config_sftp_servers.yaml#/~1config~1sftp_servers~1{sftp_server_id}" "/deleted/biblios": $ref: "./paths/deleted_biblios.yaml#/~1deleted~1biblios" "/deleted/biblios/{biblio_id}": @@ -553,6 +559,8 @@ paths: $ref: "./paths/rotas.yaml#/~1rotas~1{rota_id}" "/rotas/{rota_id}/stages/{stage_id}/position": $ref: "./paths/rotas.yaml#/~1rotas~1{rota_id}~1stages~1{stage_id}~1position" + "/sftp_server/{sftp_server_id}/test_connection": + $ref: "./paths/test_sftp_servers.yaml#/~1sftp_server~1{sftp_server_id}~1test_connection" /suggestions: $ref: ./paths/suggestions.yaml#/~1suggestions "/suggestions/{suggestion_id}": @@ -933,6 +941,12 @@ parameters: name: smtp_server_id required: true type: integer + sftp_server_id_pp: + description: FTP/SFTP server internal identifier + in: path + name: sftp_server_id + required: true + type: integer suggestion_id_pp: description: Internal suggestion identifier in: path @@ -1280,6 +1294,9 @@ tags: - description: "Manage SMTP servers configurations\n" name: smtp_servers x-displayName: SMTP servers + - description: "Manage FTP/SFTP servers configurations\n" + name: sftp_servers + x-displayName: FTP/SFTP servers - description: "Manage tickets\n" name: tickets x-displayName: Tickets diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc index 464dbf7a568..8e00a787130 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -145,7 +145,7 @@ [% END %] - [% IF ( CAN_user_parameters_manage_identity_providers || CAN_user_parameters_manage_smtp_servers || CAN_user_parameters_manage_search_targets || CAN_user_parameters_manage_didyoumean || CAN_user_parameters_manage_column_config || CAN_user_parameters_manage_audio_alerts || ( CAN_user_parameters_manage_sms_providers && Koha.Preference('SMSSendDriver') == 'Email' ) || CAN_user_parameters_manage_usage_stats || CAN_user_parameters_manage_additional_fields || ( Koha.Preference('EnableAdvancedCatalogingEditor') && CAN_user_parameters_manage_keyboard_shortcuts ) ) %] + [% IF ( CAN_user_parameters_manage_identity_providers || CAN_user_parameters_manage_smtp_servers ||CAN_user_parameters_manage_sftp_servers || CAN_user_parameters_manage_search_targets || CAN_user_parameters_manage_didyoumean || CAN_user_parameters_manage_column_config || CAN_user_parameters_manage_audio_alerts || ( CAN_user_parameters_manage_sms_providers && Koha.Preference('SMSSendDriver') == 'Email' ) || CAN_user_parameters_manage_usage_stats || CAN_user_parameters_manage_additional_fields || ( Koha.Preference('EnableAdvancedCatalogingEditor') && CAN_user_parameters_manage_keyboard_shortcuts ) ) %]
Additional parameters
    [% IF ( CAN_user_parameters_manage_identity_providers) %] @@ -158,6 +158,9 @@ [% IF ( CAN_user_parameters_manage_smtp_servers ) %]
  • SMTP servers
  • [% END %] + [% IF ( CAN_user_parameters_manage_sftp_servers ) %] +
  • FTP/SFTP servers
  • + [% END %] [% IF ( CAN_user_parameters_manage_didyoumean ) %]
  • Did you mean?
  • [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc index 4ca590e4b8d..5c42d2806bc 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc @@ -193,6 +193,9 @@ [%- CASE 'manage_smtp_servers' -%] Manage SMTP servers ([% name | html %]) + [%- CASE 'manage_sftp_servers' -%] + Manage FTP/SFTP servers + ([% name | html %]) [%- CASE 'manage_column_config' -%] Manage column configuration ([% name | html %]) diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index a8813575fa2..b72160396f9 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -254,7 +254,7 @@ [% END %] - [% IF ( ( CAN_user_parameters_manage_identity_providers || CAN_user_parameters_manage_smtp_servers || CAN_user_parameters_manage_search_targets || CAN_user_parameters_manage_didyoumean || CAN_user_parameters_manage_column_config || CAN_user_parameters_manage_audio_alerts || CAN_user_parameters_manage_sms_providers && Koha.Preference('SMSSendDriver') == 'Email' ) || CAN_user_parameters_manage_usage_stats || CAN_user_parameters_manage_additional_fields || CAN_user_parameters_manage_mana || (Koha.Preference('EnableAdvancedCatalogingEditor') && CAN_user_parameters_manage_keyboard_shortcuts) ) %] + [% IF ( ( CAN_user_parameters_manage_identity_providers || CAN_user_parameters_manage_smtp_servers || CAN_user_parameters_manage_sftp_servers || CAN_user_parameters_manage_search_targets || CAN_user_parameters_manage_didyoumean || CAN_user_parameters_manage_column_config || CAN_user_parameters_manage_audio_alerts || CAN_user_parameters_manage_sms_providers && Koha.Preference('SMSSendDriver') == 'Email' ) || CAN_user_parameters_manage_usage_stats || CAN_user_parameters_manage_additional_fields || CAN_user_parameters_manage_mana || (Koha.Preference('EnableAdvancedCatalogingEditor') && CAN_user_parameters_manage_keyboard_shortcuts) ) %]

    Additional parameters

    + + + +

    New FTP/SFTP server

    + +
    + [% INCLUDE 'csrf-token.inc' %] + +
    +
      +
    1. + + + Required +
    2. +
    +
    + +
    +
      +
    1. + + + Required +
    2. +
    3. + + + Required +
    4. +
    5. + + + Required +
    6. +
    7. + + + Only applies to FTP connections +
    8. +
    9. + + +
    10. +
    11. + + + Required +
    12. +
    13. + + +
    14. +
    15. + + + Only applies to SFTP connections +
    16. +
    17. + +
      + The path on the remote server where we will download from +
    18. +
    19. + +
      + The path on the remote server where we will upload to +
    20. + +
    21. + + + Enables additional debug output in the logs +
    22. +
    +
    +
    + Submit + Cancel +
    +
    +[% END %] + +[% IF op == 'edit_form' && !messages %] + + + + +

    [% tx("Modify FTP/SFTP server '{sftp_server}'", { sftp_server = sftp_server.name }) | html %]

    + +
    + [% INCLUDE 'csrf-token.inc' %] + + +
    +
      +
    1. + + + Required +
    2. +
    +
    + +
    +
      +
    1. + + + Required +
    2. +
    3. + + + Required +
    4. +
    5. + + + Required +
    6. +
    7. + + + Only applies to FTP connections +
    8. +
    9. + + +
    10. +
    11. + + + Required +
    12. +
    13. + + +
    14. +
    15. + + + Only applies to SFTP connections +
    16. +
    17. + +
      + The path on the remote server where we will download from +
    18. +
    19. + +
      + The path on the remote server where we will upload to +
    20. + +
    21. + + + Enables additional debug output in the logs +
    22. +
    +
    +
    + Submit + Cancel +
    +
    +[% END %] + +[% IF op == 'test_form' %] + +

    [% tx("Test FTP/SFTP server '{sftp_server}'", { sftp_server = sftp_server.name }) | html %]

    + [% IF sftp_server.id %] +
    + [% IF sftp_server.transport == 'sftp' %] +
    + For your information: As the transport in use for this server is SFTP, please be aware that running a test attempt will also accept the current host key fingerprint of the destination server. If you have doubts about the authenticity of this server, remove its host key from your authorized_keys file immediately, and contact your System Administrator. +
    + [% END %] +
    +
    +

    Test results

    +
    + +
    +
    +
    +

    Test details

    +

    Connection details are as follows:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Host[% sftp_server.host | html %]
    Port[% sftp_server.port | html %]
    Transport[% sftp_server.transport FILTER upper | html %]
    Username[% sftp_server.user_name | html %]
    Authentication mode + [% IF sftp_server.auth_mode == 'password' %] + Password-based + [% ELSE %] + Key file-based + [% END %] +
    Status + [% SWITCH sftp_server.status %] + [% CASE 'tests_ok' %] + Tests passing + [% CASE 'tests_failed' %] + Tests failing + [% CASE %] + Never used + [% END %] +
    +
    +
    +
    +
    + Edit + Reset +
    + [% ELSE %] +
    +

    Oops – Not Found

    +

    An FTP/SFTP server with that ID was not found. Please go back and try again.

    +
    + [% END %] +[% END %] + +[% IF op == 'list' %] + + + +

    FTP/SFTP servers

    + + [% IF servers_count < 1 %] +
    +

    + There are no FTP/SFTP servers defined.
    + To create one, use the new FTP/SFTP server button above. +

    +
    + [% ELSE %] +
    + + + + + + + + + + + + + + + + +
    NameHostPortTransportAuthentication modeUsernameDownload directoryUpload directoryStatusDebugActions
    +
    + [% END %] +[% END %] + + + + + + +
    + +
    + + + +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/admin-menu.js") | $raw %] + [% INCLUDE 'datatables.inc' %] + + + [% IF op == 'test_form' %] + + [% END %] +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/t/Koha/Auth/Permissions.t b/t/Koha/Auth/Permissions.t index 108373cfb66..b7a4ac74f33 100755 --- a/t/Koha/Auth/Permissions.t +++ b/t/Koha/Auth/Permissions.t @@ -207,6 +207,7 @@ subtest 'superlibrarian tests' => sub { 'CAN_user_parameters_manage_search_targets' => 1, 'CAN_user_parameters_manage_sms_providers' => 1, 'CAN_user_parameters_manage_smtp_servers' => 1, + 'CAN_user_parameters_manage_sftp_servers' => 1, 'CAN_user_parameters_manage_sysprefs' => 1, 'CAN_user_parameters_manage_transfers' => 1, 'CAN_user_parameters_manage_usage_stats' => 1, diff --git a/t/db_dependent/Koha/SFTP/Server.t b/t/db_dependent/Koha/SFTP/Server.t new file mode 100755 index 00000000000..ee53f38e98d --- /dev/null +++ b/t/db_dependent/Koha/SFTP/Server.t @@ -0,0 +1,167 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 7; +use Test::Exception; +use Test::Warn; + +use Koha::SFTP::Servers; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'to_api() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + + ok( !exists $sftp_server->to_api->{password}, 'Password is not part of the API representation' ); + ok( !exists $sftp_server->to_api->{key_file}, 'Key file is not part of the API representation' ); + + $schema->storage->txn_rollback; +}; + +subtest 'update_password() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + $sftp_server->update_password('test123'); + + ok( $sftp_server->password ne 'test123', 'Password should not be in plain text' ); + is( length( $sftp_server->password ), 64, 'Password has should be 64 characters long' ); + + $schema->storage->txn_rollback; +}; + +subtest 'update_key_file() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + $sftp_server->update_key_file('321tset'); + + ok( $sftp_server->key_file ne 'test123', 'Password should not be in plain text' ); + is( length( $sftp_server->key_file ), 64, 'Password has should be 64 characters long' ); + + $schema->storage->txn_rollback; +}; + +subtest 'update_status() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + my $sftp_server_status = 'tests_ok'; + + $sftp_server->set( + { + password => 'somepass', + key_file => 'somekey', + } + )->store; + + isnt( $sftp_server_status, $sftp_server->status, 'Status should not by tests_ok' ); + + $sftp_server->update_status($sftp_server_status); + + is( $sftp_server_status, $sftp_server->status, 'Status should be tests_ok' ); + + $schema->storage->txn_rollback; + +}; + +subtest 'plain_text_password() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + + $sftp_server->update_password('test123'); + + my $sftp_server_plain_text_password = $sftp_server->plain_text_password; + + isnt( $sftp_server_plain_text_password, $sftp_server->password, 'Password and password hash shouldn\'t match' ); + is( $sftp_server_plain_text_password, 'test123', 'Password should be in plain text' ); + + $schema->storage->txn_rollback; +}; + +subtest 'plain_text_key() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + + $sftp_server->update_key_file('321tset'); + + my $sftp_server_plain_text_key = $sftp_server->plain_text_key; + + isnt( $sftp_server_plain_text_key, $sftp_server->key_file, 'Key file and key file hash shouldn\'t match' ); + is( $sftp_server_plain_text_key, "321tset\n", 'Key file should be in plain text' ); + + $schema->storage->txn_rollback; +}; + +subtest 'write_key_file() tests' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( { class => 'Koha::SFTP::Servers' } ); + + $sftp_server->update_key_file('321tset'); + + my $path = '/tmp/kohadev_test'; + t::lib::Mocks::mock_config( 'upload_path', $path ); + mkdir $path if !-d $path; + + my $first_test = $sftp_server->write_key_file; + + my $file = $sftp_server->locate_key_file; + my $second_test = ( -f $file ); + + open( my $fh, '<', $sftp_server->locate_key_file ); + my $third_test = <$fh>; + + is( $first_test, 1, 'Writing key file should return 1' ); + is( $second_test, 1, 'Written key file should exist' ); + is( $third_test, "321tset\n", 'The contents of the key file should be 321tset\n' ); + + unlink $file; + + $schema->storage->txn_rollback; +}; diff --git a/t/db_dependent/api/v1/sftp_servers.t b/t/db_dependent/api/v1/sftp_servers.t new file mode 100755 index 00000000000..af26c9ca2fc --- /dev/null +++ b/t/db_dependent/api/v1/sftp_servers.t @@ -0,0 +1,443 @@ +#!/usr/bin/env perl + +# This file is part of Koha. +# +# Koha 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. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 6; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::SFTP::Servers; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +my $t = Test::Mojo->new('Koha::REST::V1'); +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +subtest 'list() tests' => sub { + + plan tests => 11; + + $schema->storage->txn_begin; + + Koha::SFTP::Servers->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + ## Authorized user tests + # No FTP/SFTP servers, so empty array should be returned + $t->get_ok("//$userid:$password@/api/v1/config/sftp_servers")->status_is(200)->json_is( [] ); + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + + # One sftp server created, should get returned + $t->get_ok("//$userid:$password@/api/v1/config/sftp_servers")->status_is(200)->json_is( [ $sftp_server->to_api ] ); + + my $another_sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + + # Two FTP/SFTP servers created, they should both be returned + $t->get_ok("//$userid:$password@/api/v1/config/sftp_servers")->status_is(200) + ->json_is( [ $sftp_server->to_api, $another_sftp_server->to_api, ] ); + + # Unauthorized access + $t->get_ok("//$unauth_userid:$password@/api/v1/config/sftp_servers")->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/config/sftp_servers/" . $sftp_server->id )->status_is(200) + ->json_is( $sftp_server->to_api ); + + $t->get_ok( "//$unauth_userid:$password@/api/v1/config/sftp_servers/" . $sftp_server->id )->status_is(403); + + my $sftp_server_to_delete = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + my $non_existent_id = $sftp_server_to_delete->id; + $sftp_server_to_delete->delete; + + $t->get_ok("//$userid:$password@/api/v1/config/sftp_servers/$non_existent_id")->status_is(404) + ->json_is( '/error' => 'FTP/SFTP server not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 15; + + $schema->storage->txn_begin; + + Koha::SFTP::Servers->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + my $sftp_server_data = $sftp_server->to_api; + delete $sftp_server_data->{sftp_server_id}; + $sftp_server->delete; + + # Unauthorized attempt to write + $t->post_ok( "//$unauth_userid:$password@/api/v1/config/sftp_servers" => json => $sftp_server_data ) + ->status_is(403); + + # Authorized attempt to write invalid data + my $sftp_server_with_invalid_field = { + name => 'Some other server', + blah => 'blah' + }; + + $t->post_ok( "//$userid:$password@/api/v1/config/sftp_servers" => json => $sftp_server_with_invalid_field ) + ->status_is(400)->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + my $sftp_server_id = + $t->post_ok( "//$userid:$password@/api/v1/config/sftp_servers" => json => $sftp_server_data ) + ->status_is( 201, 'SWAGGER3.2.1' ) + ->header_like( Location => qr|^\/api\/v1\/config\/sftp_servers\/\d*|, 'SWAGGER3.4.1' ) + ->json_is( '/name' => $sftp_server_data->{name} )->tx->res->json->{sftp_server_id}; + + # Authorized attempt to create with null id + $sftp_server_data->{sftp_server_id} = undef; + $t->post_ok( "//$userid:$password@/api/v1/config/sftp_servers" => json => $sftp_server_data )->status_is(400) + ->json_has('/errors'); + + # Authorized attempt to create with existing id + $sftp_server_data->{sftp_server_id} = $sftp_server_id; + $t->post_ok( "//$userid:$password@/api/v1/config/sftp_servers" => json => $sftp_server_data )->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Read-only.", + path => "/body/sftp_server_id" + } + ] + ); + + $schema->storage->txn_rollback; +}; + +subtest 'update() tests' => sub { + + plan tests => 15; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $sftp_server_id = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + )->id; + + # Unauthorized attempt to update + $t->put_ok( "//$unauth_userid:$password@/api/v1/config/sftp_servers/$sftp_server_id" => json => + { name => 'New unauthorized name change' } )->status_is(403); + + # Attempt partial update on a PUT + my $sftp_server_with_missing_field = { + host => 'localhost', + passive => '1' + }; + + $t->put_ok( + "//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id" => json => $sftp_server_with_missing_field ) + ->status_is(400)->json_is( "/errors" => [ { message => "Missing property.", path => "/body/name" } ] ); + + # Full object update on PUT + my $sftp_server_with_updated_field = { + name => "Some name", + password => "some_pass", + }; + + $t->put_ok( + "//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id" => json => $sftp_server_with_updated_field ) + ->status_is(200)->json_is( '/name' => 'Some name' ); + + # Authorized attempt to write invalid data + my $sftp_server_with_invalid_field = { + blah => "Blah", + name => 'Some name' + }; + + $t->put_ok( + "//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id" => json => $sftp_server_with_invalid_field ) + ->status_is(400)->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + my $sftp_server_to_delete = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + my $non_existent_id = $sftp_server_to_delete->id; + $sftp_server_to_delete->delete; + + $t->put_ok( + "//$userid:$password@/api/v1/config/sftp_servers/$non_existent_id" => json => $sftp_server_with_updated_field ) + ->status_is(404); + + # Wrong method (POST) + $sftp_server_with_updated_field->{sftp_server_id} = 2; + + $t->post_ok( + "//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id" => json => $sftp_server_with_updated_field ) + ->status_is(404); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + + plan tests => 7; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $sftp_server_id = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + )->id; + + # Unauthorized attempt to delete + $t->delete_ok("//$unauth_userid:$password@/api/v1/config/sftp_servers/$sftp_server_id")->status_is(403); + + $t->delete_ok("//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id")->status_is( 204, 'SWAGGER3.2.4' ) + ->content_is( '', 'SWAGGER3.3.4' ); + + $t->delete_ok("//$userid:$password@/api/v1/config/sftp_servers/$sftp_server_id")->status_is(404); + + $schema->storage->txn_rollback; +}; + +subtest 'test() tests' => sub { + + plan tests => 5; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3**2 } # parameters flag = 3 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + my $sftp_server_id = $sftp_server->id; + + # Unauthorized attempt to test + $t->get_ok("//$unauth_userid:$password@/api/v1/sftp_server/$sftp_server_id/test_connection")->status_is(403); + + $t->get_ok("//$userid:$password@/api/v1/sftp_server/$sftp_server_id/test_connection") + ->status_is( 200, 'SWAGGER3.2.4' ) + ->content_is( '{"1_ftp_conn":{"err":"cannot connect to ' + . $sftp_server->host + . ': Name or service not known","msg":null,"passed":false}}', 'SWAGGER3.3.4' ); + + $schema->storage->txn_rollback; +}; -- 2.48.1