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

(-)a/Koha/Patron.pm (+27 lines)
Lines 24-29 use Carp; Link Here
24
24
25
use C4::Context;
25
use C4::Context;
26
use C4::Log;
26
use C4::Log;
27
use Koha::AuthUtils;
27
use Koha::Database;
28
use Koha::Database;
28
use Koha::DateUtils;
29
use Koha::DateUtils;
29
use Koha::Issues;
30
use Koha::Issues;
Lines 267-272 sub track_login { Link Here
267
    $self->lastseen( dt_from_string() )->store;
268
    $self->lastseen( dt_from_string() )->store;
268
}
269
}
269
270
271
=head2 change_password_to
272
273
my $changed = $patron->change_password_to($cleartext_password);
274
275
Changes patron's password to C<$cleartext_password>. This subroutine
276
also makes validations for new password, but does not check the old
277
one.
278
279
=cut
280
281
sub change_password_to {
282
    my ($self, $cleartext_password) = @_;
283
284
    my $min_length = C4::Context->preference("minPasswordLength");
285
    if ($min_length > length($cleartext_password)) {
286
        return (undef, "Password is too short. Minimum length: $min_length.");
287
    }
288
    if ($cleartext_password =~ m|^\s+| or $cleartext_password =~ m|\s+$|) {
289
        return (undef, "Password cannot contain trailing whitespaces.");
290
    }
291
    my $hashed_password = Koha::AuthUtils::hash_password($cleartext_password);
292
    $self->set({ password => $hashed_password })->store;
293
    logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
294
    return 1;
295
}
296
270
=head3 type
297
=head3 type
271
298
272
=cut
299
=cut
(-)a/Koha/REST/V1/Patron.pm (+22 lines)
Lines 19-24 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious::Controller';
20
use Mojo::Base 'Mojolicious::Controller';
21
21
22
23
use C4::Auth qw( haspermission checkpw_internal );
22
use Koha::Patrons;
24
use Koha::Patrons;
23
25
24
sub list {
26
sub list {
Lines 44-47 sub get { Link Here
44
    return $c->$cb($patron->unblessed, 200);
46
    return $c->$cb($patron->unblessed, 200);
45
}
47
}
46
48
49
sub changepassword {
50
    my ($c, $args, $cb) = @_;
51
52
    my $user = $c->stash('koha.user');
53
    my $patron = Koha::Patrons->find($args->{borrowernumber});
54
    return $c->$cb({ error => "Patron not found." }, 404) unless $patron;
55
56
    my $pw = $args->{'body'};
57
    my $dbh = C4::Context->dbh;
58
    unless (checkpw_internal($dbh, $user->userid, $pw->{'current_password'})) {
59
        return $c->$cb({ error => "Wrong current password." }, 400);
60
    }
61
62
    my ($success, $errmsg) = $user->change_password_to($pw->{'new_password'});
63
    if ($errmsg) {
64
        return $c->$cb({ error => $errmsg }, 400);
65
    }
66
    return $c->$cb({}, 200);
67
}
68
47
1;
69
1;
(-)a/api/v1/swagger.json (+410 lines)
Line 0 Link Here
1
{
2
  "swagger": "2.0",
3
  "info": {
4
    "title": "Koha REST API",
5
    "version": "1",
6
    "license": {
7
      "name": "GPL v3",
8
      "url": "http://www.gnu.org/licenses/gpl.txt"
9
    },
10
    "contact": {
11
      "name": "Koha Team",
12
      "url": "http://koha-community.org/"
13
    }
14
  },
15
  "basePath": "/api/v1",
16
  "paths": {
17
    "/patrons": {
18
      "get": {
19
        "operationId": "listPatrons",
20
        "tags": ["patrons"],
21
        "produces": [
22
          "application/json"
23
        ],
24
        "responses": {
25
          "200": {
26
            "description": "A list of patrons",
27
            "schema": {
28
              "type": "array",
29
              "items": {
30
                "$ref": "#/definitions/patron"
31
              }
32
            }
33
          },
34
          "403": {
35
            "description": "Access forbidden",
36
            "schema": {
37
              "$ref": "#/definitions/error"
38
            }
39
          }
40
        }
41
      }
42
    },
43
    "/patrons/{borrowernumber}": {
44
      "get": {
45
        "operationId": "getPatron",
46
        "tags": ["patrons"],
47
        "parameters": [
48
          {
49
            "$ref": "#/parameters/borrowernumberPathParam"
50
          }
51
        ],
52
        "produces": [
53
          "application/json"
54
        ],
55
        "responses": {
56
          "200": {
57
            "description": "A patron",
58
            "schema": {
59
              "$ref": "#/definitions/patron"
60
            }
61
          },
62
          "403": {
63
            "description": "Access forbidden",
64
            "schema": {
65
              "$ref": "#/definitions/error"
66
            }
67
          },
68
          "404": {
69
            "description": "Patron not found",
70
            "schema": {
71
              "$ref": "#/definitions/error"
72
            }
73
          }
74
        }
75
      }
76
    },
77
    "/patrons/{borrowernumber}/password": {
78
      "patch": {
79
        "operationId": "changepasswordPatron",
80
        "tags": ["patrons"],
81
        "parameters": [
82
          { "$ref": "#/parameters/borrowernumberPathParam" },
83
          {
84
            "name": "body",
85
            "in": "body",
86
            "description": "A JSON object containing informations about passwords",
87
            "required": true,
88
            "schema": {
89
              "type": "object",
90
              "properties": {
91
                "current_password": {
92
                  "description": "Current password",
93
                  "type": "string"
94
                },
95
                "new_password": {
96
                  "description": "New password",
97
                  "type": "string"
98
                }
99
              }
100
            }
101
          }
102
        ],
103
        "produces": [
104
          "application/json"
105
        ],
106
        "responses": {
107
          "200": {
108
            "description": "Password changed"
109
          },
110
          "400": {
111
            "description": "Bad request",
112
            "schema": {
113
              "$ref": "#/definitions/error"
114
            }
115
          },
116
          "403": {
117
            "description": "Access forbidden",
118
            "schema": {
119
              "$ref": "#/definitions/error"
120
            }
121
          },
122
          "404": {
123
            "description": "Patron not found",
124
            "schema": {
125
              "$ref": "#/definitions/error"
126
            }
127
          }
128
        }
129
      }
130
    },
131
    "/holds": {
132
      "get": {
133
        "operationId": "listHolds",
134
        "tags": ["borrowers", "holds"],
135
        "parameters": [
136
          {
137
            "name": "reserve_id",
138
            "in": "query",
139
            "description": "Internal reserve identifier",
140
            "type": "integer"
141
          },
142
          {
143
            "name": "borrowernumber",
144
            "in": "query",
145
            "description": "Internal borrower identifier",
146
            "type": "integer"
147
          },
148
          {
149
            "name": "reservedate",
150
            "in": "query",
151
            "description": "Reserve date",
152
            "type": "string"
153
          },
154
          {
155
            "name": "biblionumber",
156
            "in": "query",
157
            "description": "Internal biblio identifier",
158
            "type": "integer"
159
          },
160
          {
161
            "name": "branchcode",
162
            "in": "query",
163
            "description": "Branch code",
164
            "type": "string"
165
          },
166
          {
167
            "name": "notificationdate",
168
            "in": "query",
169
            "description": "Notification date",
170
            "type": "string"
171
          },
172
          {
173
            "name": "reminderdate",
174
            "in": "query",
175
            "description": "Reminder date",
176
            "type": "string"
177
          },
178
          {
179
            "name": "cancellationdate",
180
            "in": "query",
181
            "description": "Cancellation date",
182
            "type": "string"
183
          },
184
          {
185
            "name": "reservenotes",
186
            "in": "query",
187
            "description": "Reserve notes",
188
            "type": "string"
189
          },
190
          {
191
            "name": "priority",
192
            "in": "query",
193
            "description": "Priority",
194
            "type": "integer"
195
          },
196
          {
197
            "name": "found",
198
            "in": "query",
199
            "description": "Found status",
200
            "type": "string"
201
          },
202
          {
203
            "name": "timestamp",
204
            "in": "query",
205
            "description": "Time of latest update",
206
            "type": "string"
207
          },
208
          {
209
            "name": "itemnumber",
210
            "in": "query",
211
            "description": "Internal item identifier",
212
            "type": "integer"
213
          },
214
          {
215
            "name": "waitingdate",
216
            "in": "query",
217
            "description": "Date the item was marked as waiting for the patron",
218
            "type": "string"
219
          },
220
          {
221
            "name": "expirationdate",
222
            "in": "query",
223
            "description": "Date the hold expires",
224
            "type": "string"
225
          },
226
          {
227
            "name": "lowestPriority",
228
            "in": "query",
229
            "description": "Lowest priority",
230
            "type": "integer"
231
          },
232
          {
233
            "name": "suspend",
234
            "in": "query",
235
            "description": "Suspended",
236
            "type": "integer"
237
          },
238
          {
239
            "name": "suspend_until",
240
            "in": "query",
241
            "description": "Suspended until",
242
            "type": "string"
243
          }
244
        ],
245
        "produces": ["application/json"],
246
        "responses": {
247
          "200": {
248
            "description": "A list of holds",
249
            "schema": { "$ref": "#/definitions/holds" }
250
          },
251
          "404": {
252
            "description": "Borrower not found",
253
            "schema": { "$ref": "#/definitions/error" }
254
          }
255
        }
256
      },
257
      "post": {
258
        "operationId": "addHold",
259
        "tags": ["borrowers", "holds"],
260
        "parameters": [
261
          {
262
            "name": "body",
263
            "in": "body",
264
            "description": "A JSON object containing informations about the new hold",
265
            "required": true,
266
            "schema": {
267
              "type": "object",
268
              "properties": {
269
                "borrowernumber": {
270
                  "description": "Borrower internal identifier",
271
                  "type": "integer"
272
                },
273
                "biblionumber": {
274
                  "description": "Biblio internal identifier",
275
                  "type": "integer"
276
                },
277
                "itemnumber": {
278
                  "description": "Item internal identifier",
279
                  "type": "integer"
280
                },
281
                "branchcode": {
282
                  "description": "Pickup location",
283
                  "type": "string"
284
                },
285
                "expirationdate": {
286
                  "description": "Hold end date",
287
                  "type": "string",
288
                  "format": "date"
289
                }
290
              }
291
            }
292
          }
293
        ],
294
        "consumes": ["application/json"],
295
        "produces": ["application/json"],
296
        "responses": {
297
          "201": {
298
            "description": "Created hold",
299
            "schema": { "$ref": "#/definitions/hold" }
300
          },
301
          "400": {
302
            "description": "Missing or wrong parameters",
303
            "schema": { "$ref": "#/definitions/error" }
304
          },
305
          "403": {
306
            "description": "Hold not allowed",
307
            "schema": { "$ref": "#/definitions/error" }
308
          },
309
          "404": {
310
            "description": "Borrower not found",
311
            "schema": { "$ref": "#/definitions/error" }
312
          },
313
          "500": {
314
            "description": "Internal error",
315
            "schema": { "$ref": "#/definitions/error" }
316
          }
317
        }
318
      }
319
    },
320
    "/holds/{reserve_id}": {
321
      "put": {
322
        "operationId": "editHold",
323
        "tags": ["holds"],
324
        "parameters": [
325
          { "$ref": "#/parameters/holdIdPathParam" },
326
          {
327
            "name": "body",
328
            "in": "body",
329
            "description": "A JSON object containing fields to modify",
330
            "required": true,
331
            "schema": {
332
              "type": "object",
333
              "properties": {
334
                "priority": {
335
                  "description": "Position in waiting queue",
336
                  "type": "integer",
337
                  "minimum": 1
338
                },
339
                "branchcode": {
340
                  "description": "Pickup location",
341
                  "type": "string"
342
                },
343
                "suspend_until": {
344
                  "description": "Suspend until",
345
                  "type": "string",
346
                  "format": "date"
347
                }
348
              }
349
            }
350
          }
351
        ],
352
        "consumes": ["application/json"],
353
        "produces": ["application/json"],
354
        "responses": {
355
          "200": {
356
            "description": "Updated hold",
357
            "schema": { "$ref": "#/definitions/hold" }
358
          },
359
          "400": {
360
            "description": "Missing or wrong parameters",
361
            "schema": { "$ref": "#/definitions/error" }
362
          },
363
          "404": {
364
            "description": "Hold not found",
365
            "schema": { "$ref": "#/definitions/error" }
366
          }
367
        }
368
      },
369
      "delete": {
370
        "operationId": "deleteHold",
371
        "tags": ["holds"],
372
        "parameters": [
373
          { "$ref": "#/parameters/holdIdPathParam" }
374
        ],
375
        "produces": ["application/json"],
376
        "responses": {
377
          "200": {
378
            "description": "Successful deletion",
379
            "schema": {
380
              "type": "object"
381
            }
382
          },
383
          "404": {
384
            "description": "Hold not found",
385
            "schema": { "$ref": "#/definitions/error" }
386
          }
387
        }
388
      }
389
    }
390
  },
391
  "definitions": {
392
    "$ref": "./definitions/index.json"
393
  },
394
  "parameters": {
395
    "borrowernumberPathParam": {
396
      "name": "borrowernumber",
397
      "in": "path",
398
      "description": "Internal patron identifier",
399
      "required": true,
400
      "type": "integer"
401
    },
402
    "holdIdPathParam": {
403
      "name": "reserve_id",
404
      "in": "path",
405
      "description": "Internal hold identifier",
406
      "required": true,
407
      "type": "integer"
408
    }
409
  }
410
}
(-)a/api/v1/swagger/paths.json (+3 lines)
Lines 10-14 Link Here
10
  },
10
  },
11
  "/patrons/{borrowernumber}": {
11
  "/patrons/{borrowernumber}": {
12
    "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}"
12
    "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}"
13
  },
14
  "/patrons/{borrowernumber}/password": {
15
    "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}~1password"
13
  }
16
  }
14
}
17
}
(-)a/api/v1/swagger/paths/patrons.json (+60 lines)
Lines 69-73 Link Here
69
        }
69
        }
70
      }
70
      }
71
    }
71
    }
72
  },
73
  "/patrons/{borrowernumber}/password": {
74
    "patch": {
75
      "operationId": "changepasswordPatron",
76
      "tags": ["patrons"],
77
      "parameters": [{
78
          "$ref": "../parameters.json#/borrowernumberPathParam"
79
        }, {
80
          "name": "body",
81
          "in": "body",
82
          "description": "A JSON object containing informations about passwords",
83
          "required": true,
84
          "schema": {
85
            "type": "object",
86
            "properties": {
87
              "current_password": {
88
                "description": "Current password",
89
                "type": "string"
90
              },
91
              "new_password": {
92
                "description": "New password",
93
                "type": "string"
94
              }
95
            }
96
          }
97
        }
98
      ],
99
      "produces": [
100
          "application/json"
101
      ],
102
      "responses": {
103
        "200": {
104
          "description": "Password changed"
105
        },
106
        "400": {
107
          "description": "Bad request",
108
          "schema": {
109
            "$ref": "../definitions.json#/error"
110
          }
111
        },
112
        "403": {
113
          "description": "Access forbidden",
114
          "schema": {
115
            "$ref": "../definitions.json#/error"
116
          }
117
        },
118
        "404": {
119
          "description": "Patron not found",
120
          "schema": {
121
            "$ref": "../definitions.json#/error"
122
          }
123
        }
124
      },
125
      "x-koha-authorization": {
126
        "allow-owner": true,
127
        "permissions": {
128
          "borrowers": "1"
129
        }
130
      }
131
    }
72
  }
132
  }
73
}
133
}
(-)a/t/db_dependent/api/v1/patrons.t (-7 / +60 lines)
Lines 17-43 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 20;
20
use Test::More tests => 38;
21
use Test::Mojo;
21
use Test::Mojo;
22
use t::lib::TestBuilder;
22
use t::lib::TestBuilder;
23
use t::lib::Mocks;
23
24
24
use C4::Auth;
25
use C4::Auth;
25
use C4::Context;
26
use C4::Context;
26
27
28
use Koha::AuthUtils;
27
use Koha::Database;
29
use Koha::Database;
28
use Koha::Patron;
30
use Koha::Patron;
29
31
30
my $builder = t::lib::TestBuilder->new();
32
my $builder = t::lib::TestBuilder->new();
31
33
32
my $dbh = C4::Context->dbh;
34
my $schema = Koha::Database->new->schema;
33
$dbh->{AutoCommit} = 0;
35
$schema->storage->txn_begin;
34
$dbh->{RaiseError} = 1;
35
36
36
$ENV{REMOTE_ADDR} = '127.0.0.1';
37
$ENV{REMOTE_ADDR} = '127.0.0.1';
37
my $t = Test::Mojo->new('Koha::REST::V1');
38
my $t = Test::Mojo->new('Koha::REST::V1');
38
39
39
my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
40
my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
40
my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
41
my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
42
41
my $guarantor = $builder->build({
43
my $guarantor = $builder->build({
42
    source => 'Borrower',
44
    source => 'Borrower',
43
    value => {
45
    value => {
Lines 46-51 my $guarantor = $builder->build({ Link Here
46
        flags        => 0,
48
        flags        => 0,
47
    }
49
    }
48
});
50
});
51
52
my $password = "secret";
49
my $borrower = $builder->build({
53
my $borrower = $builder->build({
50
    source => 'Borrower',
54
    source => 'Borrower',
51
    value => {
55
    value => {
Lines 81-86 $tx->req->cookies({name => 'CGISESSID', value => $session->id}); Link Here
81
$t->request_ok($tx)
85
$t->request_ok($tx)
82
  ->status_is(403);
86
  ->status_is(403);
83
87
88
84
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . ($borrower->{ borrowernumber }-1));
89
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . ($borrower->{ borrowernumber }-1));
85
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
90
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
86
$t->request_ok($tx)
91
$t->request_ok($tx)
Lines 100-111 $t->request_ok($tx) Link Here
100
  ->status_is(200)
105
  ->status_is(200)
101
  ->json_is('/guarantorid', $guarantor->{borrowernumber});
106
  ->json_is('/guarantorid', $guarantor->{borrowernumber});
102
107
108
my $password_obj = {
109
    current_password    => $password,
110
    new_password        => "new password",
111
};
112
113
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/-100/password' => json => $password_obj);
114
$t->request_ok($tx)
115
  ->status_is(401);
116
117
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/'.$borrower->{borrowernumber}.'/password');
118
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
119
$t->request_ok($tx)
120
  ->status_is(400);
121
122
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/'.$guarantor->{borrowernumber}.'/password' => json => $password_obj);
123
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
124
$t->request_ok($tx)
125
  ->status_is(403);
126
103
my $loggedinuser = $builder->build({
127
my $loggedinuser = $builder->build({
104
    source => 'Borrower',
128
    source => 'Borrower',
105
    value => {
129
    value => {
106
        branchcode   => $branchcode,
130
        branchcode   => $branchcode,
107
        categorycode => $categorycode,
131
        categorycode => $categorycode,
108
        flags        => 16 # borrowers flag
132
        flags        => 16, # borrowers flag
133
        password     => Koha::AuthUtils::hash_password($password),
109
    }
134
    }
110
});
135
});
111
136
Lines 129-132 $t->request_ok($tx) Link Here
129
  ->json_is('/borrowernumber' => $borrower->{ borrowernumber })
154
  ->json_is('/borrowernumber' => $borrower->{ borrowernumber })
130
  ->json_is('/surname' => $borrower->{ surname });
155
  ->json_is('/surname' => $borrower->{ surname });
131
156
132
$dbh->rollback;
157
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/-100/password' => json => $password_obj);
158
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
159
$t->request_ok($tx)
160
  ->status_is(404);
161
162
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/'.$loggedinuser->{borrowernumber}.'/password' => json => $password_obj);
163
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
164
$t->request_ok($tx)
165
  ->status_is(200);
166
167
ok(C4::Auth::checkpw_hash($password_obj->{'new_password'}, Koha::Patrons->find($loggedinuser->{borrowernumber})->password), "New password in database.");
168
is(C4::Auth::checkpw_hash($password_obj->{'current_password'}, Koha::Patrons->find($loggedinuser->{borrowernumber})->password), "", "Old password is gone.");
169
170
$password_obj->{'current_password'} = $password_obj->{'new_password'};
171
$password_obj->{'new_password'} = "a";
172
t::lib::Mocks::mock_preference("minPasswordLength", 5);
173
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/'.$loggedinuser->{borrowernumber}.'/password' => json => $password_obj);
174
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
175
$t->request_ok($tx)
176
  ->status_is(400)
177
  ->json_like('/error', qr/Password is too short/, "Password too short");
178
179
$password_obj->{'new_password'} = " abcdef ";
180
$tx = $t->ua->build_tx(PATCH => '/api/v1/patrons/'.$loggedinuser->{borrowernumber}.'/password' => json => $password_obj);
181
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
182
$t->request_ok($tx)
183
  ->status_is(400)
184
  ->json_is('/error', "Password cannot contain trailing whitespaces.");
185
186
$schema->storage->txn_rollback;
133
- 

Return to bug 17006