View | Details | Raw Unified | Return to bug 19133
Collapse All | Expand All

(-)a/Koha/Patron/Password/Recovery.pm (+12 lines)
Lines 106-111 sub GetValidLinkInfo { Link Here
106
 C<$update>     Update existing password recovery request
106
 C<$update>     Update existing password recovery request
107
 C<$params>     Additional parameters as follows:
107
 C<$params>     Additional parameters as follows:
108
                  url:       Custom password reset link (third party integrations)
108
                  url:       Custom password reset link (third party integrations)
109
                  skip_mail: If true, will not enqueue the letter, but return it
110
                             instead. Use this flag if you want a third party
111
                             service to handle emailing instead of Koha.
109
=cut
112
=cut
110
113
111
sub SendPasswordRecoveryEmail {
114
sub SendPasswordRecoveryEmail {
Lines 179-184 sub SendPasswordRecoveryEmail { Link Here
179
          { passwordreseturl => $uuidLink, user => $borrower->userid },
182
          { passwordreseturl => $uuidLink, user => $borrower->userid },
180
    );
183
    );
181
184
185
    if ($params->{skip_mail}) {
186
        return {
187
            to_address => $userEmail,
188
            uuid       => $uuid_str,
189
            uuidLink   => $uuidLink,
190
            letter     => $letter->{content},
191
        };
192
    }
193
182
    # define from emails
194
    # define from emails
183
    my $library = $borrower->library;
195
    my $library = $borrower->library;
184
    my $kohaEmail = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');  # send from patron's branch or Koha Admin
196
    my $kohaEmail = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');  # send from patron's branch or Koha Admin
(-)a/Koha/REST/V1/Patron/Password.pm (+163 lines)
Line 0 Link Here
1
package Koha::REST::V1::Patron::Password;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Mojo::Base 'Mojolicious::Controller';
21
22
use C4::Context;
23
use C4::Members;
24
use Koha::AuthUtils qw(hash_password);
25
use Koha::Database;
26
use Koha::Patron::Password::Recovery qw(
27
    SendPasswordRecoveryEmail
28
    ValidateBorrowernumber
29
    CompletePasswordRecovery
30
);
31
use Koha::Patrons;
32
33
use Koha::Exceptions;
34
use Koha::Exceptions::Authentication;
35
use Koha::Exceptions::Authorization;
36
37
use Try::Tiny;
38
39
sub recovery {
40
    my $c = shift->openapi->valid_input or return;
41
42
    my $patron;
43
    return try {
44
        my $body = $c->req->json;
45
46
        unless (C4::Context->preference('OpacPasswordChange') and
47
                C4::Context->preference('OpacPasswordReset'))
48
        {
49
            return $c->render(status => 403, openapi => {
50
                error => 'Password recovery is disabled.'
51
            });
52
        }
53
54
        unless (defined $body->{userid} or defined $body->{cardnumber}) {
55
            Koha::Exceptions::BadParameter->throw(
56
                error => 'Either userid or cardnumber must be given.'
57
            );
58
        }
59
60
        my $patron = Koha::Patrons->search({
61
            email => $body->{email},
62
            '-or' => {
63
                userid => $body->{userid},
64
                cardnumber => $body->{cardnumber},
65
            }
66
        })->next;
67
68
        unless ($patron) {
69
            Koha::Exceptions::Patron::NotFound->throw(
70
                error => 'Patron not found'
71
            );
72
        }
73
74
        my $link = $body->{'custom_link'};
75
        my $skip_mail =  0;
76
        if ($body->{'skip_mail'}) {
77
            Koha::Exceptions::Authentication::Required->throw(
78
                error => 'Authentication required while skip_mail parameter is on'
79
            ) unless ref($c->stash('koha.user')) eq 'Koha::Patron';
80
            if (my $user = $c->stash('koha.user')) {
81
                Koha::Exceptions::Authorization::Unauthorized->throw(
82
                    error => 'Permission get_password_reset_uuid required while '
83
                            .'skip_mail parameter is on'
84
                ) unless C4::Auth::haspermission($user->userid, {
85
                    borrowers => 'get_password_reset_uuid'
86
                });
87
                $skip_mail = 1;
88
            }
89
        }
90
91
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
92
93
        my $ret = SendPasswordRecoveryEmail($patron, $patron->email, $resend, {
94
            url => $link,
95
            skip_mail => $skip_mail,
96
        });
97
98
        if (ref($ret) eq 'HASH') {
99
            $ret->{status} = 2;
100
            return $c->render(status => 201, openapi => $ret);
101
        }
102
103
        return $c->render(status => 201, openapi => {
104
            status => 1,
105
            to_address => $patron->email
106
        });
107
    }
108
    catch {
109
        if ($_->isa('Koha::Exceptions::BadParameter')) {
110
            return $c->render(status => 400, openapi => { error => $_->error });
111
        }
112
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
113
            return $c->render(status => 404, openapi => { error => $_->error });
114
        }
115
        elsif ($_->isa('Koha::Exceptions::WrongParameter')) {
116
            return $c->render(status => 400, openapi => { error => $_->error });
117
        }
118
        elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
119
            return $c->render(status => 401, openapi => { error => $_->error });
120
        }
121
        elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
122
            return $c->render(status => 403, openapi => { error => $_->error });
123
        }
124
        Koha::Exceptions::rethrow_exception($_);
125
    };
126
}
127
128
sub complete_recovery {
129
    my $c = shift->openapi->valid_input or return;
130
131
    my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery');
132
    return try {
133
        my $body = $c->req->json;
134
135
        my $password_recovery = $rs->find({
136
            uuid => $body->{uuid}
137
        });
138
        unless ($password_recovery) {
139
            return $c->render(status => 404, openapi => {
140
                error => 'Password recovery request with given uuid not found.'
141
            });
142
        }
143
144
        my $patron = Koha::Patrons->find($password_recovery->borrowernumber);
145
        my $categorycode = $patron->categorycode;
146
        my ($success, $error, $errmsg) = C4::Members::ValidateMemberPassword(
147
            $categorycode, $body->{new_password}, $body->{confirm_new_password}
148
        );
149
        if ($error) {
150
            return $c->render(status => 400, openapi => {
151
                error => $errmsg
152
            });
153
        }
154
        my $password = $body->{new_password};
155
        $patron->update_password( $patron->userid, hash_password($password) );
156
        return $c->render(status => 200, openapi => {});
157
    }
158
    catch {
159
        Koha::Exceptions::rethrow_exception($_);
160
    };
161
}
162
163
1;
(-)a/Koha/REST/V1/Patrons/Password.pm (-2 / +29 lines)
Lines 80-92 sub recovery { Link Here
80
        }
80
        }
81
81
82
        my $link = $body->{'custom_link'};
82
        my $link = $body->{'custom_link'};
83
        my $skip_mail =  0;
84
        if ($body->{'skip_mail'}) {
85
            Koha::Exceptions::Authentication::Required->throw(
86
                error => 'Authentication required while skip_mail parameter is on'
87
            ) unless ref($c->stash('koha.user')) eq 'Koha::Patron';
88
            if (my $user = $c->stash('koha.user')) {
89
                Koha::Exceptions::Authorization::Unauthorized->throw(
90
                    error => 'Permission get_password_reset_uuid required while '
91
                            .'skip_mail parameter is on'
92
                ) unless C4::Auth::haspermission($user->userid, {
93
                    borrowers => 'get_password_reset_uuid'
94
                });
95
                $skip_mail = 1;
96
            }
97
        }
83
98
84
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
99
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
85
100
86
        SendPasswordRecoveryEmail($patron, $patron->email, $resend, {
101
        my $ret = SendPasswordRecoveryEmail($patron, $patron->email, $resend, {
87
            url => $link
102
            url => $link,
103
            skip_mail => $skip_mail,
88
        });
104
        });
89
105
106
        if (ref($ret) eq 'HASH') {
107
            $ret->{status} = 2;
108
            return $c->render(status => 201, openapi => $ret);
109
        }
110
90
        return $c->render(status => 201, openapi => {
111
        return $c->render(status => 201, openapi => {
91
            status => 1,
112
            status => 1,
92
            to_address => $patron->email
113
            to_address => $patron->email
Lines 103-108 sub recovery { Link Here
103
        elsif ($_->isa('Koha::Exceptions::WrongParameter')) {
124
        elsif ($_->isa('Koha::Exceptions::WrongParameter')) {
104
            return $c->render(status => 400, openapi => { error => $_->error });
125
            return $c->render(status => 400, openapi => { error => $_->error });
105
        }
126
        }
127
        elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
128
            return $c->render(status => 401, openapi => { error => $_->error });
129
        }
130
        elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
131
            return $c->render(status => 403, openapi => { error => $_->error });
132
        }
106
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
133
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
107
            return $c->render(status => 404, openapi => { error => $_->error });
134
            return $c->render(status => 404, openapi => { error => $_->error });
108
        }
135
        }
(-)a/api/v1/swagger/definitions/patron_password_recovery.json (-1 / +13 lines)
Lines 1-12 Link Here
1
{
1
{
2
  "type": "object",
2
  "type": "object",
3
  "properties": {
3
  "properties": {
4
    "letter": {
5
      "description": "Content of the password recovery letter (returned only if Koha skips letter enqueueing)",
6
      "type": "string"
7
    },
4
    "to_address": {
8
    "to_address": {
5
      "description": "Patron email address",
9
      "description": "Patron email address",
6
      "type": ["string", "null"]
10
      "type": ["string", "null"]
7
    },
11
    },
12
    "uuid": {
13
      "description": "Uuid of password recovery request (the token that patron can reset their password with) (returned only if Koha skips letter enqueueing)",
14
      "type": "string"
15
    },
16
    "uuidLink": {
17
      "description": "The URL that password reset can be completed at (returned only if Koha skips letter enqueueing)",
18
      "type": "string"
19
    },
8
    "status": {
20
    "status": {
9
      "description": "Status code. 1 = email has been enqueued",
21
      "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)",
10
      "type": "integer"
22
      "type": "integer"
11
    }
23
    }
12
  }
24
  }
(-)a/api/v1/swagger/paths/patrons.json (+8 lines)
Lines 730-735 Link Here
730
            "complete_url": {
730
            "complete_url": {
731
              "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.",
731
              "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.",
732
              "type": "string"
732
              "type": "string"
733
            },
734
            "skip_email": {
735
              "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.",
736
              "type": "boolean"
733
            }
737
            }
734
          },
738
          },
735
          "required": ["email"]
739
          "required": ["email"]
Lines 745-750 Link Here
745
          "description": "Bad parameter(s)",
749
          "description": "Bad parameter(s)",
746
          "schema": { "$ref": "../definitions.json#/error" }
750
          "schema": { "$ref": "../definitions.json#/error" }
747
        },
751
        },
752
        "401": {
753
          "description": "Authentication required",
754
          "schema": { "$ref": "../definitions.json#/error" }
755
        },
748
        "403": {
756
        "403": {
749
          "description": "Password recovery disabled, no access to this endpoint",
757
          "description": "Password recovery disabled, no access to this endpoint",
750
          "schema": {
758
          "schema": {
(-)a/installer/data/mysql/atomicupdate/Bug-19133-2_Add_permission_get_password_reset_uuid.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX';  # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
4
    $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.')" );
5
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 19133 - Add permission get_password_reset_uuid.)\n";
8
}
(-)a/installer/data/mysql/userpermissions.sql (+1 lines)
Lines 34-39 INSERT INTO permissions (module_bit, code, description) VALUES Link Here
34
   ( 3, 'manage_keyboard_shortcuts', 'Manage keyboard shortcuts for the advanced cataloging editor'),
34
   ( 3, 'manage_keyboard_shortcuts', 'Manage keyboard shortcuts for the advanced cataloging editor'),
35
   ( 4, 'edit_borrowers', 'Add, modify and view patron information'),
35
   ( 4, 'edit_borrowers', 'Add, modify and view patron information'),
36
   ( 4, 'view_borrower_infos_from_any_libraries', 'View patron infos from any libraries'),
36
   ( 4, 'view_borrower_infos_from_any_libraries', 'View patron infos from any libraries'),
37
   ( 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.'),
37
   ( 6, 'place_holds', 'Place holds for patrons'),
38
   ( 6, 'place_holds', 'Place holds for patrons'),
38
   ( 6, 'modify_holds_priority', 'Modify holds priority'),
39
   ( 6, 'modify_holds_priority', 'Modify holds priority'),
39
   ( 9, 'edit_catalogue', 'Edit catalog (Modify bibliographic/holdings data)'),
40
   ( 9, 'edit_catalogue', 'Edit catalog (Modify bibliographic/holdings data)'),
(-)a/t/db_dependent/api/v1/patrons_password_recovery.t (-2 / +73 lines)
Lines 43-49 my $remote_address = '127.0.0.1'; Link Here
43
my $t              = Test::Mojo->new('Koha::REST::V1');
43
my $t              = Test::Mojo->new('Koha::REST::V1');
44
44
45
subtest 'recovery() tests' => sub {
45
subtest 'recovery() tests' => sub {
46
    plan tests => 31;
46
    plan tests => 32;
47
47
48
    $schema->storage->txn_begin;
48
    $schema->storage->txn_begin;
49
49
Lines 245-250 subtest 'recovery() tests' => sub { Link Here
245
        );
245
        );
246
    };
246
    };
247
247
248
    subtest 'skip letter enqueueing' => sub {
249
        plan tests => 10;
250
251
        t::lib::Mocks::mock_preference(
252
            'OpacResetPasswordHostWhitelist', 'anotherallowed'
253
        );
254
        my ($service_borrowernumber, $service_session) = create_user_and_session({
255
            authorized => 1
256
        });
257
258
        $tx = $t->ua->build_tx(POST => $url => json => {
259
            email      => $patron->email,
260
            userid     => $patron->userid,
261
            custom_link => 'https://anotherallowed/reset-password.pl',
262
            skip_mail => Mojo::JSON->true
263
        });
264
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
265
        $t->request_ok($tx)
266
          ->status_is(401);
267
268
        $tx = $t->ua->build_tx(POST => $url => json => {
269
            email      => $patron->email,
270
            userid     => $patron->userid,
271
            custom_link => 'https://anotherallowed/reset-password.pl',
272
            skip_mail => Mojo::JSON->true
273
        });
274
        $tx->req->cookies({name => 'CGISESSID', value => $session->id});
275
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
276
        $t->request_ok($tx)
277
          ->status_is(403);
278
279
        Koha::Notice::Messages->search({
280
            borrowernumber => $patron->borrowernumber,
281
            letter_code => 'PASSWORD_RESET',
282
            message_transport_type => 'email'
283
        })->delete;
284
285
        $tx = $t->ua->build_tx(POST => $url => json => {
286
            email      => $patron->email,
287
            userid     => $patron->userid,
288
            custom_link => 'https://notallowed/reset-password.pl',
289
            skip_mail => Mojo::JSON->true,
290
        });
291
        $tx->req->cookies({name => 'CGISESSID', value => $service_session->id});
292
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
293
        $t->request_ok($tx)
294
          ->status_is(400);
295
296
        $tx = $t->ua->build_tx(POST => $url => json => {
297
            email      => $patron->email,
298
            userid     => $patron->userid,
299
            custom_link => 'https://anotherallowed/reset-password.pl',
300
            skip_mail => Mojo::JSON->true,
301
        });
302
        $tx->req->cookies({name => 'CGISESSID', value => $service_session->id});
303
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
304
        $t->request_ok($tx)
305
          ->status_is(201);
306
        my $uuid = $rs->search({
307
            borrowernumber => $patron->borrowernumber
308
        }, {
309
            order_by => { '-desc' => 'valid_until' }
310
        })->next->uuid;
311
        $t->json_is('/uuid', $uuid);
312
313
       is(Koha::Notice::Messages->search({
314
            borrowernumber => $patron->borrowernumber,
315
            letter_code => 'PASSWORD_RESET',
316
            message_transport_type => 'email'
317
        })->count, 0, 'Email not enqueued');
318
    };
319
248
    $schema->storage->txn_rollback;
320
    $schema->storage->txn_rollback;
249
};
321
};
250
322
251
- 

Return to bug 19133