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

(-)a/Koha/REST/V1/Patron/Password.pm (+127 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
35
use Try::Tiny;
36
37
sub recovery {
38
    my $c = shift->openapi->valid_input or return;
39
40
    my $patron;
41
    return try {
42
        my $body = $c->req->json;
43
44
        unless (C4::Context->preference('OpacPasswordChange') and
45
                C4::Context->preference('OpacPasswordReset'))
46
        {
47
            return $c->render(status => 403, openapi => {
48
                error => 'Password recovery is disabled.'
49
            });
50
        }
51
52
        unless (defined $body->{userid} or defined $body->{cardnumber}) {
53
            Koha::Exceptions::BadParameter->throw(
54
                error => 'Either userid or cardnumber must be given.'
55
            );
56
        }
57
58
        my $patron = Koha::Patrons->search({
59
            email => $body->{email},
60
            '-or' => {
61
                userid => $body->{userid},
62
                cardnumber => $body->{cardnumber},
63
            }
64
        })->next;
65
66
        unless ($patron) {
67
            Koha::Exceptions::Patron::NotFound->throw(
68
                error => 'Patron not found'
69
            );
70
        }
71
72
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
73
74
        SendPasswordRecoveryEmail($patron, $patron->email, $resend);
75
76
        return $c->render(status => 201, openapi => {
77
            status => 1,
78
            to_address => $patron->email
79
        });
80
    }
81
    catch {
82
        if ($_->isa('Koha::Exceptions::BadParameter')) {
83
            return $c->render(status => 400, openapi => { error => $_->error });
84
        }
85
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
86
            return $c->render(status => 404, openapi => { error => $_->error });
87
        }
88
        Koha::Exceptions::rethrow_exception($_);
89
    };
90
}
91
92
sub complete_recovery {
93
    my $c = shift->openapi->valid_input or return;
94
95
    my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery');
96
    return try {
97
        my $body = $c->req->json;
98
99
        my $password_recovery = $rs->find({
100
            uuid => $body->{uuid}
101
        });
102
        unless ($password_recovery) {
103
            return $c->render(status => 404, openapi => {
104
                error => 'Password recovery request with given uuid not found.'
105
            });
106
        }
107
108
        my $patron = Koha::Patrons->find($password_recovery->borrowernumber);
109
        my $categorycode = $patron->categorycode;
110
        my ($success, $error, $errmsg) = C4::Members::ValidateMemberPassword(
111
            $categorycode, $body->{new_password}, $body->{confirm_new_password}
112
        );
113
        if ($error) {
114
            return $c->render(status => 400, openapi => {
115
                error => $errmsg
116
            });
117
        }
118
        my $password = $body->{new_password};
119
        $patron->update_password( $patron->userid, hash_password($password) );
120
        return $c->render(status => 200, openapi => {});
121
    }
122
    catch {
123
        Koha::Exceptions::rethrow_exception($_);
124
    };
125
}
126
127
1;
(-)a/api/v1/swagger/definitions.json (+3 lines)
Lines 23-28 Link Here
23
  "CPUinvoiceReport": {
23
  "CPUinvoiceReport": {
24
    "$ref": "definitions/CPUinvoiceReport.json"
24
    "$ref": "definitions/CPUinvoiceReport.json"
25
  },
25
  },
26
  "passwordRecovery": {
27
    "$ref": "definitions/passwordRecovery.json"
28
  },
26
  "patron": {
29
  "patron": {
27
    "$ref": "definitions/patron.json"
30
    "$ref": "definitions/patron.json"
28
  },
31
  },
(-)a/api/v1/swagger/definitions/passwordRecovery.json (+13 lines)
Line 0 Link Here
1
{
2
  "type": "object",
3
  "properties": {
4
    "to_address": {
5
      "description": "Patron email address",
6
      "type": ["string", "null"]
7
    },
8
    "status": {
9
      "description": "Status code. 1 = email has been enqueued",
10
      "type": "integer"
11
    }
12
  }
13
}
(-)a/api/v1/swagger/paths.json (+6 lines)
Lines 128-133 Link Here
128
  "/patrons/{borrowernumber}/status": {
128
  "/patrons/{borrowernumber}/status": {
129
    "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}~1status"
129
    "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}~1status"
130
  },
130
  },
131
  "/patrons/password/recovery": {
132
    "$ref": "paths/patrons.json#/~1patrons~1password~1recovery"
133
  },
134
  "/patrons/password/recovery/complete": {
135
    "$ref": "paths/patrons.json#/~1patrons~1password~1recovery~1complete"
136
  },
131
  "/payments/transaction/{invoicenumber}": {
137
  "/payments/transaction/{invoicenumber}": {
132
    "$ref": "paths/payments/transactions.json#/~1payments~1transactions~1{invoicenumber}"
138
    "$ref": "paths/payments/transactions.json#/~1payments~1transactions~1{invoicenumber}"
133
  },
139
  },
(-)a/api/v1/swagger/paths/patrons.json (+128 lines)
Lines 1147-1151 Link Here
1147
        }
1147
        }
1148
      }
1148
      }
1149
    }
1149
    }
1150
  },
1151
  "/patrons/password/recovery": {
1152
    "post": {
1153
      "x-mojo-to": "Patron::Password#recovery",
1154
      "operationId": "addPasswordRecoveryRequest",
1155
      "description": "Creates a new password recovery request.",
1156
      "tags": ["password"],
1157
      "parameters": [{
1158
        "name": "body",
1159
        "in": "body",
1160
        "description": "A JSON object containing fields for recovery request",
1161
        "required": true,
1162
        "schema": {
1163
          "type": "object",
1164
          "properties": {
1165
            "userid": {
1166
              "description": "Patron's userid (this field or cardnumber required)",
1167
              "type": "string"
1168
            },
1169
            "cardnumber": {
1170
              "description": "Patron's cardnumber (this field or userid required)",
1171
              "type": "string"
1172
            },
1173
            "email": {
1174
              "description": "Patron's email (required)",
1175
              "type": "string"
1176
            }
1177
          },
1178
          "required": ["email"]
1179
        }
1180
      }],
1181
      "produces": ["application/json"],
1182
      "responses": {
1183
        "201": {
1184
          "description": "Password recovery request created",
1185
          "schema": { "$ref": "../definitions.json#/passwordRecovery" }
1186
        },
1187
        "400": {
1188
          "description": "Bad parameter(s)",
1189
          "schema": { "$ref": "../definitions.json#/error" }
1190
        },
1191
        "403": {
1192
          "description": "Password recovery disabled, no access to this endpoint",
1193
          "schema": {
1194
            "$ref": "../definitions.json#/error"
1195
          }
1196
        },
1197
        "404": {
1198
          "description": "One or more of the given parameters not found",
1199
          "schema": {
1200
            "$ref": "../definitions.json#/error"
1201
          }
1202
        },
1203
        "500": {
1204
          "description": "Internal server error",
1205
          "schema": {
1206
            "$ref": "../definitions.json#/error"
1207
          }
1208
        },
1209
        "503": {
1210
          "description": "Under maintenance",
1211
          "schema": {
1212
            "$ref": "../definitions.json#/error"
1213
          }
1214
        }
1215
      }
1216
    }
1217
  },
1218
  "/patrons/password/recovery/complete": {
1219
    "post": {
1220
      "x-mojo-to": "Patron::Password#complete_recovery",
1221
      "operationId": "completePasswordRecoveryRequest",
1222
      "description": "Completes a password recovery request.",
1223
      "tags": ["password"],
1224
      "parameters": [{
1225
        "name": "body",
1226
        "in": "body",
1227
        "description": "A JSON object containing fields for completing recovery request",
1228
        "required": true,
1229
        "schema": {
1230
          "type": "object",
1231
          "properties": {
1232
            "uuid": {
1233
              "description": "Uuid generated in /patrons/password/recovery",
1234
              "type": "string"
1235
            },
1236
            "new_password": {
1237
              "description": "Patron's new password",
1238
              "type": "string"
1239
            },
1240
            "confirm_new_password": {
1241
              "description": "Confirm patron's new password",
1242
              "type": "string"
1243
            }
1244
          },
1245
          "required": ["uuid", "new_password", "confirm_new_password"]
1246
        }
1247
      }],
1248
      "produces": ["application/json"],
1249
      "responses": {
1250
        "200": {
1251
          "description": "Password recovery completed",
1252
          "schema": { "type": "object" }
1253
        },
1254
        "400": {
1255
          "description": "Bad parameter(s)",
1256
          "schema": { "$ref": "../definitions.json#/error" }
1257
        },
1258
        "404": {
1259
          "description": "Uuid not found",
1260
          "schema": {
1261
            "$ref": "../definitions.json#/error"
1262
          }
1263
        },
1264
        "500": {
1265
          "description": "Internal server error",
1266
          "schema": {
1267
            "$ref": "../definitions.json#/error"
1268
          }
1269
        },
1270
        "503": {
1271
          "description": "Under maintenance",
1272
          "schema": {
1273
            "$ref": "../definitions.json#/error"
1274
          }
1275
        }
1276
      }
1277
    }
1150
  }
1278
  }
1151
}
1279
}
(-)a/t/db_dependent/api/v1/passwordrecovery.t (-1 / +273 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
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
21
use Test::More tests => 2;
22
use Test::Mojo;
23
use Test::MockModule;
24
25
use t::lib::Mocks;
26
use t::lib::TestBuilder;
27
28
use C4::Auth;
29
use C4::Context;
30
31
use Koha::Database;
32
use Koha::Notice::Messages;
33
34
use Crypt::Eksblowfish::Bcrypt qw(en_base64);
35
36
my $schema  = Koha::Database->new->schema;
37
my $builder = t::lib::TestBuilder->new;
38
39
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
40
# this affects the other REST api tests
41
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
42
43
my $remote_address = '127.0.0.1';
44
my $t              = Test::Mojo->new('Koha::REST::V1');
45
46
subtest 'recovery() tests' => sub {
47
    plan tests => 29;
48
49
    $schema->storage->txn_begin;
50
51
    my $url = '/api/v1/patrons/password/recovery';
52
53
    my ($patron, $session) = create_user_and_session();
54
55
    my $tx = $t->ua->build_tx(POST => $url => json => {});
56
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
57
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
58
    $t->request_ok($tx)
59
      ->status_is(400);
60
61
    t::lib::Mocks::mock_preference('OpacPasswordReset', 0);
62
    t::lib::Mocks::mock_preference('OpacPasswordChange', 0);
63
    $tx = $t->ua->build_tx(POST => $url => json => {
64
        email      => $patron->email,
65
        cardnumber => $patron->cardnumber
66
    });
67
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
68
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
69
    $t->request_ok($tx)
70
      ->status_is(403);
71
72
    t::lib::Mocks::mock_preference('OpacPasswordChange', 1);
73
    $tx = $t->ua->build_tx(POST => $url => json => {
74
        email      => $patron->email,
75
        cardnumber => $patron->cardnumber
76
    });
77
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
78
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
79
    $t->request_ok($tx)
80
      ->status_is(403);
81
82
    t::lib::Mocks::mock_preference('OpacPasswordReset', 1);
83
    t::lib::Mocks::mock_preference('OpacPasswordChange', 0);
84
    $tx = $t->ua->build_tx(POST => $url => json => {
85
        email      => $patron->email,
86
        cardnumber => $patron->cardnumber
87
    });
88
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
89
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
90
    $t->request_ok($tx)
91
      ->status_is(403);
92
93
    t::lib::Mocks::mock_preference('OpacPasswordChange', 1);
94
95
    $tx = $t->ua->build_tx(POST => $url => json => {
96
        email      => 'nonexistent',
97
        cardnumber => $patron->cardnumber
98
    });
99
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
100
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
101
    $t->request_ok($tx)
102
      ->status_is(404);
103
104
    $tx = $t->ua->build_tx(POST => $url => json => {
105
        email      => $patron->email,
106
        cardnumber => 'nonexistent'
107
    });
108
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
109
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
110
    $t->request_ok($tx)
111
      ->status_is(404);
112
113
    $tx = $t->ua->build_tx(POST => $url => json => {
114
        email      => 'nonexistent',
115
        userid     => $patron->userid
116
    });
117
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
118
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
119
    $t->request_ok($tx)
120
      ->status_is(404);
121
122
    $tx = $t->ua->build_tx(POST => $url => json => {
123
        email      => $patron->email,
124
        userid     => 'nonexistent'
125
    });
126
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
127
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
128
    $t->request_ok($tx)
129
      ->status_is(404);
130
131
    $tx = $t->ua->build_tx(POST => $url => json => {
132
        email      => $patron->email,
133
        cardnumber => $patron->cardnumber
134
    });
135
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
136
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
137
    $t->request_ok($tx)
138
      ->status_is(201)
139
      ->json_is('/status' => 1);
140
141
    my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery');
142
    is(
143
        $rs->search({ borrowernumber => $patron->borrowernumber })->count, 1,
144
        'Password modification request found in database'
145
    );
146
147
    $tx = $t->ua->build_tx(POST => $url => json => {
148
        email      => $patron->email,
149
        userid     => $patron->userid
150
    });
151
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
152
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
153
    $t->request_ok($tx)
154
      ->status_is(201)
155
      ->json_is('/status' => 1);
156
157
    is(
158
        $rs->search({ borrowernumber => $patron->borrowernumber })->count, 1,
159
        'Password modification request found in database'
160
    );
161
162
    $tx = $t->ua->build_tx(POST => $url => json => {
163
        email      => $patron->email,
164
        userid     => $patron->userid,
165
        cardnumber => $patron->cardnumber,
166
    });
167
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
168
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
169
    $t->request_ok($tx)
170
      ->status_is(201)
171
      ->json_is('/status' => 1);
172
173
    is(
174
        $rs->search({ borrowernumber => $patron->borrowernumber })->count, 1,
175
        'Password modification request found in database'
176
    );
177
178
    my $notice = Koha::Notice::Messages->search({
179
        borrowernumber => $patron->borrowernumber,
180
        letter_code => 'PASSWORD_RESET',
181
        message_transport_type => 'email'
182
    })->count;
183
    is($notice, 3, 'Found password reset letters in message queue.');
184
185
    $schema->storage->txn_rollback;
186
};
187
188
subtest 'complete_recovery() tests' => sub {
189
    plan tests => 7;
190
191
    $schema->storage->txn_begin;
192
193
    my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery');
194
195
    my ($patron, $session) = create_user_and_session();
196
    my $uuid_str;
197
    do {
198
        $uuid_str = '$2a$08$'.en_base64(Koha::AuthUtils::generate_salt('weak', 16));
199
    } while ( substr ( $uuid_str, -1, 1 ) eq '.' );
200
    my $recovery = $builder->build({
201
        source => 'BorrowerPasswordRecovery',
202
        value  => {
203
            borrowernumber => $patron->borrowernumber,
204
            uuid => $uuid_str
205
        }
206
    });
207
208
    my $url = '/api/v1/patrons/password/recovery/complete';
209
210
    my $tx = $t->ua->build_tx(POST => $url => json => {});
211
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
212
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
213
    $t->request_ok($tx)
214
      ->status_is(400);
215
216
    $tx = $t->ua->build_tx(POST => $url.'notfound' => json => {
217
        uuid                 => $uuid_str,
218
        new_password         => 'test',
219
        confirm_new_password => 'test',
220
    });
221
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
222
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
223
    $t->request_ok($tx)
224
      ->status_is(404);
225
226
    t::lib::Mocks::mock_preference('minPasswordLength', 4);
227
    $tx = $t->ua->build_tx(POST => $url => json => {
228
        uuid                 => $uuid_str,
229
        new_password         => '1234',
230
        confirm_new_password => '1234',
231
    });
232
    $tx->req->cookies({name => 'CGISESSID', value => $session->id});
233
    $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
234
    $t->request_ok($tx)
235
      ->status_is(200);
236
237
    my $stored_pw = Koha::Patrons->find($patron->borrowernumber)->password;
238
    is(
239
      $stored_pw,
240
       Koha::AuthUtils::hash_password('1234', $stored_pw), 'Password changed'
241
    );
242
243
    $schema->storage->txn_rollback;
244
};
245
246
sub create_user_and_session {
247
    my ($flags) = @_;
248
249
    my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
250
    my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
251
252
    my $borrower = $builder->build({
253
        source => 'Borrower',
254
        value => {
255
            branchcode   => $branchcode,
256
            categorycode => $categorycode,
257
            lost         => 0,
258
        }
259
    });
260
261
    my $session = C4::Auth::get_session('');
262
    $session->param('number', $borrower->{ borrowernumber });
263
    $session->param('id', $borrower->{ userid });
264
    $session->param('ip', '127.0.0.1');
265
    $session->param('lasttime', time());
266
    $session->flush;
267
    my $patron = Koha::Patrons->find($borrower->{borrowernumber});
268
    if ( $flags ) {
269
        Koha::Auth::PermissionManager->grantPermissions($patron, $flags);
270
    }
271
272
    return ($patron, $session);
273
}

Return to bug 19133