From 80f81abc507eeb4d760ef07e54ec08f52c2f9902 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 18 Aug 2017 15:47:43 +0300 Subject: [PATCH] Bug 19133-2: Allow third party services to send password recovery emails instead of Koha Adds a new parameter "skip_mail" to POST /api/v1/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/passwordrecovery.t 3. Add yourself borrowers.get_password_reset_uuid permission 4. Send POST request to /api/v1/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 --- Koha/Patron/Password/Recovery.pm | 12 ++++ Koha/REST/V1/Patron/Password.pm | 35 +++++++++- api/v1/swagger/definitions/passwordRecovery.json | 14 +++- api/v1/swagger/paths/patrons.json | 8 +++ ...3-2_Add_permission_get_password_reset_uuid.perl | 8 +++ installer/data/mysql/userpermissions.sql | 1 + t/db_dependent/api/v1/passwordrecovery.t | 74 +++++++++++++++++++++- 7 files changed, 147 insertions(+), 5 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 d95fb45..456e69d 100644 --- a/Koha/Patron/Password/Recovery.pm +++ b/Koha/Patron/Password/Recovery.pm @@ -106,6 +106,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 { @@ -179,6 +182,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 to/from emails my $kohaEmail = C4::Context->preference('KohaAdminEmailAddress'); # from diff --git a/Koha/REST/V1/Patron/Password.pm b/Koha/REST/V1/Patron/Password.pm index e928411..ba5fd4a 100644 --- a/Koha/REST/V1/Patron/Password.pm +++ b/Koha/REST/V1/Patron/Password.pm @@ -31,6 +31,8 @@ use Koha::Patron::Password::Recovery qw( use Koha::Patrons; use Koha::Exceptions; +use Koha::Exceptions::Authentication; +use Koha::Exceptions::Authorization; use Try::Tiny; @@ -70,13 +72,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 @@ -92,6 +115,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 }); + } Koha::Exceptions::rethrow_exception($_); }; } diff --git a/api/v1/swagger/definitions/passwordRecovery.json b/api/v1/swagger/definitions/passwordRecovery.json index 30d62de..164fdd5 100644 --- a/api/v1/swagger/definitions/passwordRecovery.json +++ b/api/v1/swagger/definitions/passwordRecovery.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 40c2eae..6ef8fb0 100644 --- a/api/v1/swagger/paths/patrons.json +++ b/api/v1/swagger/paths/patrons.json @@ -1177,6 +1177,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"] @@ -1192,6 +1196,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 0000000..aeb6d73 --- /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, code, description) VALUES ( 'borrowers', '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/userpermissions.sql b/installer/data/mysql/userpermissions.sql index 9211803..829f823 100644 --- a/installer/data/mysql/userpermissions.sql +++ b/installer/data/mysql/userpermissions.sql @@ -11,6 +11,7 @@ INSERT INTO permissions (module, code, description) VALUES ( 'borrowers', 'view_borrowers', 'Show borrower details and search for borrowers.'), ( 'borrowers', 'manage_api_keys', "Manage Borrowers' REST API keys"), ( 'borrowers', 'get_self_service_status', 'Allow the user to get the self-service status of a borrower. Eg. can the borrower access self-service resources'), + ( 'borrowers', '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.'), ( 'permissions', 'set_permissions', 'Set user permissions'), ( 'reserveforothers','place_holds', 'Place holds for patrons'), ( 'reserveforothers','modify_holds_priority', 'Modify holds priority'), diff --git a/t/db_dependent/api/v1/passwordrecovery.t b/t/db_dependent/api/v1/passwordrecovery.t index 152a326..c38cd74 100644 --- a/t/db_dependent/api/v1/passwordrecovery.t +++ b/t/db_dependent/api/v1/passwordrecovery.t @@ -44,7 +44,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; @@ -246,6 +246,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({ + borrowers => 'get_password_reset_uuid' + }); + + $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.7.4