From ce601fee2b31be7eade4d961ead7b78770bab6f1 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 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 --- Koha/REST/V1/Config/SFTP/Servers.pm | 184 ++++ Koha/REST/V1/SFTPServer.pm | 55 ++ Koha/SFTP/Server.pm | 319 +++++++ Koha/SFTP/Servers.pm | 55 ++ Koha/Schema/Result/SftpServer.pm | 166 ++++ admin/sftp_servers.pl | 218 +++++ api/v1/swagger/definitions/sftp_server.yaml | 54 ++ api/v1/swagger/paths/config_sftp_servers.yaml | 236 +++++ api/v1/swagger/paths/test_sftp_servers.yaml | 48 + api/v1/swagger/swagger.yaml | 17 + .../bug_35761-add_SFTP_tables_and_perm.pl | 41 + installer/data/mysql/kohastructure.sql | 24 + .../data/mysql/mandatory/userpermissions.sql | 1 + .../prog/en/includes/admin-menu.inc | 5 +- .../prog/en/includes/permissions.inc | 5 + .../prog/en/modules/admin/admin-home.tt | 6 +- .../prog/en/modules/admin/sftp_servers.tt | 876 ++++++++++++++++++ t/Koha/Auth/Permissions.t | 1 + t/db_dependent/Koha/SFTP/Server.t | 165 ++++ t/db_dependent/api/v1/sftp_servers.t | 443 +++++++++ 20 files changed, 2917 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 100644 Koha/Schema/Result/SftpServer.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 100755 installer/data/mysql/atomicupdate/bug_35761-add_SFTP_tables_and_perm.pl 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..4fe80afed71 --- /dev/null +++ b/Koha/SFTP/Server.pm @@ -0,0 +1,319 @@ +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::SFTP::Servers; + +use Try::Tiny qw( catch try ); +use Mojo::JSON; +use Net::SFTP::Foreign; +use Net::FTP; + +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. + +=cut + +sub store { + my ($self) = @_; + + $self->password( + $self->password + ? Koha::Encryption->new->encrypt_hex( $self->password ) + : undef + ); + + $self->key_file( + $self->key_file + ? Koha::Encryption->new->encrypt_hex( _dos2unix( $self->key_file ) ) + : undef + ); + + $self->write_key_file; + + return $self->SUPER::store; +} + +=head3 to_api + + my $json = $sftp_server->to_api; + +Overloaded method that returns a JSON representation of the Koha::SFTP::Server object, +suitable for API output. + +=cut + +sub to_api { + my ( $self, $params ) = @_; + + my $json_sftp = $self->SUPER::to_api($params); + return unless $json_sftp; + delete $json_sftp->{password}; + + 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 + + $server->plain_text_password; +Fetches the plaintext password, from the object + +=cut + +sub plain_text_password { + my ($self) = @_; + + return Koha::Encryption->new->decrypt_hex( $self->password ) + if $self->password; + +} + +=head3 plain_text_key + + $server->plain_text_key; +Fetches the plaintext key file, from the object + +=cut + +sub plain_text_key { + my ($self) = @_; + + return Koha::Encryption->new->decrypt_hex( $self->key_file ) . "\n" + if $self->key_file; +} + +=head3 write_key_file + + $server->write_key_file; +Writes the keyfile from the db into a file + +=cut + +sub write_key_file { + my ($self) = @_; + my $upload_path = C4::Context->config('upload_path') or return; + my $key_path = $upload_path . '/ssh_keys'; + my $key_file = $key_path . '/id_ssh_' . $self->id; + + mkdir $key_path if ( !-d $key_path ); + + unlink $key_file if ( -f $key_file ); + my $fh; + $fh = IO::File->new( $key_file, 'w' ) or return; + chmod 0600, $key_file if ( -f $key_file ); + + print $fh $self->plain_text_key; + + undef $fh; + + return 1; +} + +=head3 locate_key_file + + $server->locate_key_file; +Returns the keyfile's expected path + +=cut + +sub locate_key_file { + my ($self) = @_; + my $upload_path = C4::Context->config('upload_path'); + return if not defined $upload_path; + + my $keyf_path = $upload_path . '/ssh_keys/id_ssh_' . $self->id; + + return ( -f $keyf_path ) ? $keyf_path : undef; +} + +=head3 test_conn + + $server->test_conn; +Tests a connection to a given sftp server + +=cut + +sub test_conn { + my ($self) = @_; + my $default_result = { + passed => Mojo::JSON->false, + err => undef, + msg => undef, + }; + my $sftp_test_results; + my $ftp_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), + ], + ); + $sftp_test_results->{'1_sftp_conn'}->{'err'} = $sftp->error + if $sftp->error; + $sftp_test_results->{'1_sftp_conn'}->{'passed'} = Mojo::JSON->true + unless $sftp->error; + + unless ( $sftp->error ) { + $sftp_test_results->{'2_sftp_ls'} = {%$default_result}; + $sftp->ls(); + $sftp_test_results->{'2_sftp_ls'}->{'err'} = $sftp->error + if $sftp->error; + $sftp_test_results->{'2_sftp_ls'}->{'passed'} = Mojo::JSON->true + unless $sftp->error; + + $sftp_test_results->{'3_sftp_write'} = {%$default_result}; + open my $fh, '<', \"Hello, world!\n"; + close $fh if ( $sftp->put( $fh, '.koha_test_file' ) ); + $sftp_test_results->{'3_sftp_write'}->{'err'} = $sftp->error + if $sftp->error; + $sftp_test_results->{'3_sftp_write'}->{'passed'} = Mojo::JSON->true + unless $sftp->error; + + $sftp_test_results->{'4_sftp_del'} = {%$default_result}; + $sftp->remove('.koha_test_file'); + $sftp_test_results->{'4_sftp_del'}->{'err'} = $sftp->error + if $sftp->error; + $sftp_test_results->{'4_sftp_del'}->{'passed'} = Mojo::JSON->true + unless $sftp->error; + } + + return ( 1, $sftp_test_results ); + } elsif ( $self->transport eq 'ftp' ) { + $ftp_test_results->{'1_ftp_conn'} = {%$default_result}; + my $ftp = Net::FTP->new( + $self->host, + Port => $self->port, + Timeout => 10, + Passive => ( scalar $self->passiv ) ? 1 : 0, + ); + if ($ftp) { + $ftp_test_results->{'1_ftp_conn'}->{'passed'} = Mojo::JSON->true; + $ftp_test_results->{'1_ftp_conn'}->{'msg'} = $ftp->message; + } else { + $ftp_test_results->{'1_ftp_conn'}->{'err'} = 'cannot connect to ' . $self->host . ': ' . $@; + } + + if ($ftp) { + $ftp_test_results->{'2_ftp_login'} = {%$default_result}; + my $login = $ftp->login( + $self->user_name, + $self->plain_text_password, + ); + if ($login) { + $ftp_test_results->{'2_ftp_login'}->{'passed'} = Mojo::JSON->true; + $ftp_test_results->{'2_ftp_login'}->{'msg'} = $ftp->message; + } else { + $ftp_test_results->{'2_ftp_login'}->{'err'} = $ftp->message; + } + + $ftp_test_results->{'3_ftp_ls'} = {%$default_result}; + my $ls = $ftp->ls('~'); + if ($ls) { + $ftp_test_results->{'3_ftp_ls'}->{'passed'} = Mojo::JSON->true; + $ftp_test_results->{'3_ftp_ls'}->{'msg'} = $ftp->message; + } else { + $ftp_test_results->{'3_ftp_ls'}->{'err'} = $ftp->message; + } + + $ftp_test_results->{'4_ftp_write'} = {%$default_result}; + open my $fh, '<', \"Hello, world!\n"; + close $fh if ( my $put = $ftp->put( $fh, '.koha_test_file' ) ); + if ($put) { + $ftp_test_results->{'4_ftp_write'}->{'passed'} = Mojo::JSON->true; + $ftp_test_results->{'4_ftp_write'}->{'msg'} = $ftp->message; + } else { + $ftp_test_results->{'4_ftp_write'}->{'err'} = $ftp->message; + } + + $ftp_test_results->{'5_ftp_del'} = {%$default_result}; + my $delete = $ftp->delete('.koha_test_file'); + if ($delete) { + $ftp_test_results->{'5_ftp_del'}->{'passed'} = Mojo::JSON->true; + $ftp_test_results->{'5_ftp_del'}->{'msg'} = $ftp->message; + } else { + $ftp_test_results->{'5_ftp_del'}->{'err'} = $ftp->message; + } + } + + return ( 1, $ftp_test_results ); + } + + return ( 0, undef ); +} + +=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/Koha/Schema/Result/SftpServer.pm b/Koha/Schema/Result/SftpServer.pm new file mode 100644 index 00000000000..33bb3e76f91 --- /dev/null +++ b/Koha/Schema/Result/SftpServer.pm @@ -0,0 +1,166 @@ +use utf8; +package Koha::Schema::Result::SftpServer; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::SftpServer + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("sftp_servers"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 name + + data_type: 'varchar' + is_nullable: 0 + size: 80 + +=head2 host + + data_type: 'varchar' + default_value: 'localhost' + is_nullable: 0 + size: 80 + +=head2 port + + data_type: 'integer' + default_value: 22 + is_nullable: 0 + +=head2 transport + + data_type: 'enum' + default_value: 'sftp' + extra: {list => ["ftp","sftp"]} + is_nullable: 0 + +=head2 passiv + + data_type: 'tinyint' + default_value: 1 + is_nullable: 0 + +=head2 user_name + + data_type: 'varchar' + is_nullable: 1 + size: 80 + +=head2 password + + data_type: 'varchar' + is_nullable: 1 + size: 80 + +=head2 key_file + + data_type: 'varchar' + is_nullable: 1 + size: 4096 + +=head2 auth_mode + + data_type: 'enum' + default_value: 'password' + extra: {list => ["password","key_file","noauth"]} + is_nullable: 0 + +=head2 debug + + data_type: 'tinyint' + default_value: 0 + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "name", + { data_type => "varchar", is_nullable => 0, size => 80 }, + "host", + { + data_type => "varchar", + default_value => "localhost", + is_nullable => 0, + size => 80, + }, + "port", + { data_type => "integer", default_value => 22, is_nullable => 0 }, + "transport", + { + data_type => "enum", + default_value => "sftp", + extra => { list => ["ftp", "sftp"] }, + is_nullable => 0, + }, + "passiv", + { data_type => "tinyint", default_value => 1, is_nullable => 0 }, + "user_name", + { data_type => "varchar", is_nullable => 1, size => 80 }, + "password", + { data_type => "varchar", is_nullable => 1, size => 80 }, + "key_file", + { data_type => "varchar", is_nullable => 1, size => 4096 }, + "auth_mode", + { + data_type => "enum", + default_value => "password", + extra => { list => ["password", "key_file", "noauth"] }, + is_nullable => 0, + }, + "debug", + { data_type => "tinyint", default_value => 0, is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + + +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2024-12-02 11:43:19 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:K1+15gYfWmqYKldm0NdgGQ + +__PACKAGE__->add_columns( + '+passiv' => { is_boolean => 1 }, + '+debug' => { is_boolean => 1 }, +); + +sub koha_objects_class { + 'Koha::SFTP::Servers'; +} + +sub koha_object_class { + 'Koha::SFTP::Server'; +} + +1; diff --git a/admin/sftp_servers.pl b/admin/sftp_servers.pl new file mode 100755 index 00000000000..8940a4c1bf8 --- /dev/null +++ b/admin/sftp_servers.pl @@ -0,0 +1,218 @@ +#!/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 $passiv = ( 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 $debug = ( scalar $input->param('sftp_debug_mode') ) ? 1 : 0; + + try { + Koha::SFTP::Server->new( + { + name => $name, + host => $host, + port => $port, + transport => $transport, + passiv => $passiv, + auth_mode => $auth_mode, + user_name => $user_name, + password => $password, + key_file => $key_file, + debug => $debug, + } + )->store; + + 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 $passiv = ( 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 $debug = ( scalar $input->param('sftp_debug_mode') ) ? 1 : 0; + + try { + $sftp_server->password($password) + if defined $password and $password ne '****' + or not defined $password; + + $sftp_server->set( + { + name => $name, + host => $host, + port => $port, + transport => $transport, + passiv => $passiv, + auth_mode => $auth_mode, + user_name => $user_name, + password => $password, + key_file => $key_file, + debug => $debug, + } + )->store; + + 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..c2937c9e35a --- /dev/null +++ b/api/v1/swagger/definitions/sftp_server.yaml @@ -0,0 +1,54 @@ +--- +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 + port: + type: integer + description: TCP port number + transport: + type: string + enum: + - ftp + - sftp + - file + description: Transport of FTP/SFTP server + auth_mode: + type: string + enum: + - password + - key_file + - noauth + description: Authentication mode to use + passiv: + type: boolean + description: Whether or not to use passive mode + 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) + debug: + type: boolean + description: If the SMTP connection is set to debug mode +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..d728829f3da --- /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 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..ffc4bf3c9b2 --- /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 diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index ff7b94a174e..53cfc4731c3 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -174,6 +174,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: @@ -305,6 +307,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}": @@ -551,6 +557,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}": @@ -931,6 +939,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 @@ -1278,6 +1292,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/installer/data/mysql/atomicupdate/bug_35761-add_SFTP_tables_and_perm.pl b/installer/data/mysql/atomicupdate/bug_35761-add_SFTP_tables_and_perm.pl new file mode 100755 index 00000000000..5d78d98c278 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_35761-add_SFTP_tables_and_perm.pl @@ -0,0 +1,41 @@ +use Modern::Perl; + +return { + bug_number => "35761", + description => "Add new table and permission for generalised SFTP", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + $dbh->do( + q{ + INSERT IGNORE INTO permissions (module_bit, code, description) + VALUES (3, 'manage_sftp_servers', 'Manage FTP/SFTP servers configuration'); + } + ); + say $out "Added new manage_sftp_servers permission"; + + unless ( TableExists('sftp_servers') ) { + $dbh->do( + q { + CREATE TABLE `sftp_servers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(80) NOT NULL, + `host` varchar(80) NOT NULL DEFAULT 'localhost', + `port` int(11) NOT NULL DEFAULT 22, + `transport` enum('ftp','sftp') NOT NULL DEFAULT 'sftp', + `passiv` tinyint(1) NOT NULL DEFAULT 1, + `user_name` varchar(80) DEFAULT NULL, + `password` varchar(80) DEFAULT NULL, + `key_file` varchar(4096) DEFAULT NULL, + `auth_mode` enum('password','key_file','noauth') NOT NULL DEFAULT 'password', + `debug` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `host_idx` (`host`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + } + ); + say $out "Added new sftp_servers table"; + } + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 445d476b704..82ed7e2f642 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -5922,6 +5922,30 @@ CREATE TABLE `sessions` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `sftp_servers` +-- + +DROP TABLE IF EXISTS `sftp_servers`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `sftp_servers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(80) NOT NULL, + `host` varchar(80) NOT NULL DEFAULT 'localhost', + `port` int(11) NOT NULL DEFAULT 22, + `transport` enum('ftp','sftp') NOT NULL DEFAULT 'sftp', + `passiv` tinyint(1) NOT NULL DEFAULT 1, + `user_name` varchar(80) DEFAULT NULL, + `password` varchar(80) DEFAULT NULL, + `key_file` varchar(4096) DEFAULT NULL, + `auth_mode` enum('password','key_file','noauth') NOT NULL DEFAULT 'password', + `debug` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `host_idx` (`host`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `sms_providers` -- diff --git a/installer/data/mysql/mandatory/userpermissions.sql b/installer/data/mysql/mandatory/userpermissions.sql index 1d8b7d034f2..d68932d714d 100644 --- a/installer/data/mysql/mandatory/userpermissions.sql +++ b/installer/data/mysql/mandatory/userpermissions.sql @@ -39,6 +39,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES ( 3, 'manage_additional_fields', 'Add, edit, or delete additional custom fields for baskets or subscriptions (also requires order_manage or edit_subscription permissions)'), ( 3, 'manage_keyboard_shortcuts', 'Manage keyboard shortcuts for the advanced cataloging editor'), ( 3, 'manage_smtp_servers', 'Manage SMTP servers configuration'), + ( 3, 'manage_sftp_servers', 'Manage FTP/SFTP servers configuration'), ( 3, 'manage_background_jobs', 'Manage background jobs'), ( 3, 'manage_curbside_pickups', 'Manage curbside pickups'), ( 3, 'manage_search_filters', 'Manage custom search filters'), 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 4851cef036c..e205044ddcb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -147,7 +147,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) %] @@ -160,6 +160,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 9d8cb5b9102..52ecbd4266e 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc @@ -257,6 +257,11 @@ Manage SMTP servers ([% name | html %]) + [%- CASE 'manage_sftp_servers' -%] + + Manage FTP/SFTP servers + + ([% name | html %]) [%- CASE 'manage_column_config' -%] Manage column configuration 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 71df7e12e0f..f5be1ec356b 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 @@ -253,7 +253,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. + + + Enables additional debug output in the logs +
    18. +
    +
    +
    + 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. + + + Enables additional debug output in the logs +
    18. +
    +
    +
    + Submit + Cancel +
    +
    +[% END %] + +[% IF op == 'test_form' %] + + +

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

    +
    + [% IF sftp_server.id %] +
    +
    +

    Test results

    +
    +
    + Loading... +
    + Running tests... +
    +
    +
    +

    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 %] +
    +
    +
    + [% 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 %] +
    + + + + + + + + + + + + + + +
    NameHostPortTransportPassive modeAuthentication modeUsernameDebugActions
    +
    + [% END %] +[% END %] + + + + + + +
    + +
    + + + +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/admin-menu.js") | $raw %] + [% INCLUDE 'datatables.inc' %] + +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/t/Koha/Auth/Permissions.t b/t/Koha/Auth/Permissions.t index 4b398646f3b..b956f12dfbf 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..47e0dbfe8c4 --- /dev/null +++ b/t/db_dependent/Koha/SFTP/Server.t @@ -0,0 +1,165 @@ +#!/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 => 5; +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 'store() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + $sftp_server->set( { password => 'test123' } )->store; + + 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 'to_api() tests' => sub { + + plan tests => 1; + + $schema->storage->txn_begin; + + my $sftp_server = $builder->build_object( + { + class => 'Koha::SFTP::Servers', + value => { + password => undef, + key_file => undef, + }, + } + ); + + ok( !exists $sftp_server->to_api->{password}, 'Password is not part of the API representation' ); + + $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', + value => { + password => undef, + key_file => undef, + }, + } + ); + + $sftp_server->set( { password => 'test123' } )->store; + + 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', + value => { + password => undef, + key_file => undef, + }, + } + ); + + $sftp_server->set( { key_file => '321tset' } )->store; + + 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', + value => { + password => undef, + key_file => undef, + }, + } + ); + + $sftp_server->set( { key_file => '321tset' } )->store; + + 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..f7393316a67 --- /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', + passiv => '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.43.0