From 2270fe8fceaa6bc347c2d3de5b3fc4f8d2e9ea04 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 18 Aug 2017 15:47:43 +0300 Subject: [PATCH] Bug 19133: Allow third party services to send password recovery emails instead of Koha Adds a new parameter "skip_mail" to POST /api/v1/public/patrons/password/recovery. This allows third party services to avoid Koha enqueueing recovery email to message queue. Instead, letter content and the link containing uuid is returned. Requires permission to do so. Adds a new permission borrowers.get_password_reset_uuid that allows the above. To test: 1. Apply patch and run updatedatabase.pl 2. prove t/db_dependent/api/v1/patrons_password_recovery.t 3. Add yourself borrowers.get_password_reset_uuid permission 4. Send POST request to /api/v1/public/patrons/password/recovery containing a JSON body { "email": "patrons@email.com", "userid": "patrons userid", "custom_link": "https://allowed/reset.pl?token={uuid}", "skip_mail": true } (Make sure custom link is allowed by the system preference introduced in previous patch) 5. Check that the message contents and other related data is returned 6. Check borrower_password_recovery table - there should be a new entry for you 7. Check message_queue table - this message should not be enqueued Sponsored-by: Koha-Suomi Oy Signed-off-by: Tomas Cohen Arazi --- Koha/Patron/Password/Recovery.pm | 12 +++ Koha/REST/V1/Patrons/Password.pm | 31 +++++++- .../definitions/patron_password_recovery.json | 14 +++- api/v1/swagger/paths/patrons.json | 8 ++ ...dd_permission_get_password_reset_uuid.perl | 8 ++ .../data/mysql/mandatory/userpermissions.sql | 1 + .../api/v1/patrons_password_recovery.t | 74 ++++++++++++++++++- 7 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/Bug-19133-2_Add_permission_get_password_reset_uuid.perl diff --git a/Koha/Patron/Password/Recovery.pm b/Koha/Patron/Password/Recovery.pm index 6bf17f2948c..753601378ce 100644 --- a/Koha/Patron/Password/Recovery.pm +++ b/Koha/Patron/Password/Recovery.pm @@ -107,6 +107,9 @@ sub GetValidLinkInfo { C<$update> Update existing password recovery request C<$params> Additional parameters as follows: url: Custom password reset link (third party integrations) + skip_mail: If true, will not enqueue the letter, but return it + instead. Use this flag if you want a third party + service to handle emailing instead of Koha. =cut sub SendPasswordRecoveryEmail { @@ -180,6 +183,15 @@ sub SendPasswordRecoveryEmail { { passwordreseturl => $uuidLink, user => $borrower->userid }, ); + if ($params->{skip_mail}) { + return { + to_address => $userEmail, + uuid => $uuid_str, + uuidLink => $uuidLink, + letter => $letter->{content}, + }; + } + # define from emails my $library = $borrower->library; my $kohaEmail = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'); # send from patron's branch or Koha Admin diff --git a/Koha/REST/V1/Patrons/Password.pm b/Koha/REST/V1/Patrons/Password.pm index 5aa52b87970..ccf1cb90592 100644 --- a/Koha/REST/V1/Patrons/Password.pm +++ b/Koha/REST/V1/Patrons/Password.pm @@ -80,13 +80,34 @@ sub recovery { } my $link = $body->{'custom_link'}; + my $skip_mail = 0; + if ($body->{'skip_mail'}) { + Koha::Exceptions::Authentication::Required->throw( + error => 'Authentication required while skip_mail parameter is on' + ) unless ref($c->stash('koha.user')) eq 'Koha::Patron'; + if (my $user = $c->stash('koha.user')) { + Koha::Exceptions::Authorization::Unauthorized->throw( + error => 'Permission get_password_reset_uuid required while ' + .'skip_mail parameter is on' + ) unless C4::Auth::haspermission($user->userid, { + borrowers => 'get_password_reset_uuid' + }); + $skip_mail = 1; + } + } my $resend = ValidateBorrowernumber($patron->borrowernumber); - SendPasswordRecoveryEmail($patron, $patron->email, $resend, { - url => $link + my $ret = SendPasswordRecoveryEmail($patron, $patron->email, $resend, { + url => $link, + skip_mail => $skip_mail, }); + if (ref($ret) eq 'HASH') { + $ret->{status} = 2; + return $c->render(status => 201, openapi => $ret); + } + return $c->render(status => 201, openapi => { status => 1, to_address => $patron->email @@ -103,6 +124,12 @@ sub recovery { elsif ($_->isa('Koha::Exceptions::WrongParameter')) { return $c->render(status => 400, openapi => { error => $_->error }); } + elsif ($_->isa('Koha::Exceptions::Authentication::Required')) { + return $c->render(status => 401, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) { + return $c->render(status => 403, openapi => { error => $_->error }); + } elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) { return $c->render(status => 404, openapi => { error => $_->error }); } diff --git a/api/v1/swagger/definitions/patron_password_recovery.json b/api/v1/swagger/definitions/patron_password_recovery.json index 30d62de385e..164fdd57134 100644 --- a/api/v1/swagger/definitions/patron_password_recovery.json +++ b/api/v1/swagger/definitions/patron_password_recovery.json @@ -1,12 +1,24 @@ { "type": "object", "properties": { + "letter": { + "description": "Content of the password recovery letter (returned only if Koha skips letter enqueueing)", + "type": "string" + }, "to_address": { "description": "Patron email address", "type": ["string", "null"] }, + "uuid": { + "description": "Uuid of password recovery request (the token that patron can reset their password with) (returned only if Koha skips letter enqueueing)", + "type": "string" + }, + "uuidLink": { + "description": "The URL that password reset can be completed at (returned only if Koha skips letter enqueueing)", + "type": "string" + }, "status": { - "description": "Status code. 1 = email has been enqueued", + "description": "Status code. 1 = email has been enqueued in Koha, 2 = recovery request stored and uuid returned (but not emailed - API user should decide how to deliver the recovery request to patron)", "type": "integer" } } diff --git a/api/v1/swagger/paths/patrons.json b/api/v1/swagger/paths/patrons.json index 6d0461c2667..522b072d93b 100644 --- a/api/v1/swagger/paths/patrons.json +++ b/api/v1/swagger/paths/patrons.json @@ -733,6 +733,10 @@ "complete_url": { "description": "The location where patron will complete their password recovery. This location will be added into the password recovery email. To avoid malicious use, whitelist certain hosts in OpacResetPasswordHostWhitelist system preference and forbid all other custom links. Use {uuid} for adding the password recovery uuid into your custom link. It will be replaced by the actual uuid in the email.", "type": "string" + }, + "skip_email": { + "description": "If true, Koha will not enqueue password recovery email. Makes this endpoint return the letter, uuid and patron email. Requires permission borrowers.get_password_recovery_uuid. Useful for third party service integrations that wish to handle emailing instead of Koha.", + "type": "boolean" } }, "required": ["email"] @@ -748,6 +752,10 @@ "description": "Bad parameter(s)", "schema": { "$ref": "../definitions.json#/error" } }, + "401": { + "description": "Authentication required", + "schema": { "$ref": "../definitions.json#/error" } + }, "403": { "description": "Password recovery disabled, no access to this endpoint", "schema": { diff --git a/installer/data/mysql/atomicupdate/Bug-19133-2_Add_permission_get_password_reset_uuid.perl b/installer/data/mysql/atomicupdate/Bug-19133-2_Add_permission_get_password_reset_uuid.perl new file mode 100644 index 00000000000..924631ce35e --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug-19133-2_Add_permission_get_password_reset_uuid.perl @@ -0,0 +1,8 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + + $dbh->do( "INSERT INTO permissions (module_bit, code, description) VALUES ( 4, 'get_password_reset_uuid', 'Allow the user to get password reset uuid when recovering passwords. Useful for third party service integrations that wish to do something with the uuid, such as handle emails themselves instead of Koha.')" ); + + SetVersion( $DBversion ); + print "Upgrade to $DBversion done (Bug 19133 - Add permission get_password_reset_uuid.)\n"; +} diff --git a/installer/data/mysql/mandatory/userpermissions.sql b/installer/data/mysql/mandatory/userpermissions.sql index 08d6ffebf8e..69e111af5b8 100644 --- a/installer/data/mysql/mandatory/userpermissions.sql +++ b/installer/data/mysql/mandatory/userpermissions.sql @@ -38,6 +38,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES ( 3, 'manage_background_jobs', 'Manage background jobs'), ( 4, 'edit_borrowers', 'Add, modify and view patron information'), ( 4, 'view_borrower_infos_from_any_libraries', 'View patron infos from any libraries'), + ( 4, 'get_password_reset_uuid', 'Allow the user to get password reset uuid when recovering passwords. Useful for third party service integrations that wish to do something with the uuid, such as handle emails themselves instead of Koha.'), ( 6, 'place_holds', 'Place holds for patrons'), ( 6, 'modify_holds_priority', 'Modify holds priority'), ( 9, 'edit_catalogue', 'Edit catalog (Modify bibliographic/holdings data)'), diff --git a/t/db_dependent/api/v1/patrons_password_recovery.t b/t/db_dependent/api/v1/patrons_password_recovery.t index 3a1c57614fe..62cb6e28856 100644 --- a/t/db_dependent/api/v1/patrons_password_recovery.t +++ b/t/db_dependent/api/v1/patrons_password_recovery.t @@ -43,7 +43,7 @@ my $remote_address = '127.0.0.1'; my $t = Test::Mojo->new('Koha::REST::V1'); subtest 'recovery() tests' => sub { - plan tests => 31; + plan tests => 32; $schema->storage->txn_begin; @@ -245,6 +245,78 @@ subtest 'recovery() tests' => sub { ); }; + subtest 'skip letter enqueueing' => sub { + plan tests => 10; + + t::lib::Mocks::mock_preference( + 'OpacResetPasswordHostWhitelist', 'anotherallowed' + ); + my ($service_borrowernumber, $service_session) = create_user_and_session({ + authorized => 1 + }); + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://anotherallowed/reset-password.pl', + skip_mail => Mojo::JSON->true + }); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(401); + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://anotherallowed/reset-password.pl', + skip_mail => Mojo::JSON->true + }); + $tx->req->cookies({name => 'CGISESSID', value => $session->id}); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(403); + + Koha::Notice::Messages->search({ + borrowernumber => $patron->borrowernumber, + letter_code => 'PASSWORD_RESET', + message_transport_type => 'email' + })->delete; + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://notallowed/reset-password.pl', + skip_mail => Mojo::JSON->true, + }); + $tx->req->cookies({name => 'CGISESSID', value => $service_session->id}); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(400); + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://anotherallowed/reset-password.pl', + skip_mail => Mojo::JSON->true, + }); + $tx->req->cookies({name => 'CGISESSID', value => $service_session->id}); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(201); + my $uuid = $rs->search({ + borrowernumber => $patron->borrowernumber + }, { + order_by => { '-desc' => 'valid_until' } + })->next->uuid; + $t->json_is('/uuid', $uuid); + + is(Koha::Notice::Messages->search({ + borrowernumber => $patron->borrowernumber, + letter_code => 'PASSWORD_RESET', + message_transport_type => 'email' + })->count, 0, 'Email not enqueued'); + }; + $schema->storage->txn_rollback; }; -- 2.29.2