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

(-)a/installer/data/mysql/kohastructure.sql (+20 lines)
Lines 1816-1821 CREATE TABLE `categories` ( Link Here
1816
  `can_be_guarantee` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'if patrons of this category can be guarantees',
1816
  `can_be_guarantee` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'if patrons of this category can be guarantees',
1817
  `reset_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can do the password reset flow,',
1817
  `reset_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can do the password reset flow,',
1818
  `change_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can change their passwords in the OAPC',
1818
  `change_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can change their passwords in the OAPC',
1819
  `password_history_count` smallint(6) DEFAULT NULL COMMENT 'Number of previous passwords to check against when changing password for this patron type',
1819
  `min_password_length` smallint(6) DEFAULT NULL COMMENT 'set minimum password length for patrons in this category',
1820
  `min_password_length` smallint(6) DEFAULT NULL COMMENT 'set minimum password length for patrons in this category',
1820
  `require_strong_password` tinyint(1) DEFAULT NULL COMMENT 'set required password strength for patrons in this category',
1821
  `require_strong_password` tinyint(1) DEFAULT NULL COMMENT 'set required password strength for patrons in this category',
1821
  `force_password_reset_when_set_by_staff` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category are required to reset password after being created by a staff member',
1822
  `force_password_reset_when_set_by_staff` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category are required to reset password after being created by a staff member',
Lines 6867-6872 CREATE TABLE `zebraqueue` ( Link Here
6867
/*!40101 SET character_set_client = @saved_cs_client */;
6868
/*!40101 SET character_set_client = @saved_cs_client */;
6868
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
6869
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
6869
6870
6871
--
6872
-- Table structure for table `borrower_password_history`
6873
--
6874
6875
DROP TABLE IF EXISTS `borrower_password_history`;
6876
/*!40101 SET @saved_cs_client     = @@character_set_client */;
6877
/*!40101 SET character_set_client = utf8 */;
6878
CREATE TABLE IF NOT EXISTS borrower_password_history (
6879
    id int(11) NOT NULL AUTO_INCREMENT,
6880
    borrowernumber int(11) NOT NULL,
6881
    password varchar(128) NOT NULL,
6882
    created_on timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
6883
    PRIMARY KEY (id),
6884
    KEY borrowernumber (borrowernumber),
6885
    CONSTRAINT borrower_password_history_ibfk_1 FOREIGN KEY (borrowernumber) REFERENCES borrowers(borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE
6886
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
6887
/*!40101 SET character_set_client = @saved_cs_client */;
6888
6889
6870
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
6890
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
6871
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
6891
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
6872
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
6892
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
(-)a/t/db_dependent/Koha/Patron.t (-1 / +1 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 45;
22
use Test::More tests => 47;
23
use Test::NoWarnings;
23
use Test::NoWarnings;
24
use Test::Exception;
24
use Test::Exception;
25
use Test::Warn;
25
use Test::Warn;
(-)a/t/db_dependent/Koha/PatronPasswordHistories.t (+263 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Test::More tests => 4;
21
use Test::NoWarnings;
22
23
use t::lib::TestBuilder;
24
use t::lib::Mocks;
25
26
use Koha::Database;
27
use Koha::PatronPasswordHistory;
28
use Koha::PatronPasswordHistories;
29
use Koha::AuthUtils;
30
31
my $schema  = Koha::Database->new->schema;
32
my $builder = t::lib::TestBuilder->new;
33
34
subtest 'cleanup_old_password_history' => sub {
35
    plan tests => 6;
36
37
    $schema->storage->txn_begin;
38
39
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
40
41
    # Create 5 password history entries
42
    my @passwords = ();
43
    for my $i ( 1 .. 5 ) {
44
        my $password_history = Koha::PatronPasswordHistory->new(
45
            {
46
                borrowernumber => $patron->borrowernumber,
47
                password       => Koha::AuthUtils::hash_password("password_$i")
48
            }
49
        );
50
        $password_history->store;
51
        push @passwords, $password_history;
52
        sleep(1);    # Ensure different timestamps
53
    }
54
55
    my $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
56
    is( $count, 5, 'Initially have 5 password history entries' );
57
58
    # Keep 3 most recent
59
    my $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 3 );
60
    is( $deleted, 2, 'Deleted 2 old entries' );
61
62
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
63
    is( $count, 3, 'Now have 3 password history entries' );
64
65
    # Keep 1 most recent
66
    $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 1 );
67
    is( $deleted, 2, 'Deleted 2 more entries' );
68
69
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
70
    is( $count, 1, 'Now have 1 password history entry' );
71
72
    # Delete all (count = 0)
73
    $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 0 );
74
    is( $deleted, 1, 'Deleted remaining entry' );
75
76
    $schema->storage->txn_rollback;
77
};
78
79
subtest 'has_used_password with category settings' => sub {
80
    plan tests => 10;
81
82
    $schema->storage->txn_begin;
83
84
    my $category = $builder->build_object(
85
        {
86
            class => 'Koha::Patron::Categories',
87
            value => { password_history_count => 3 }
88
        }
89
    );
90
    my $patron = $builder->build_object(
91
        {
92
            class => 'Koha::Patrons',
93
            value => {
94
                categorycode => $category->categorycode,
95
                password     => Koha::AuthUtils::hash_password('current_password')
96
            }
97
        }
98
    );
99
100
    # Create password history
101
    my $old_password1 = 'old_password_1';
102
    my $old_password2 = 'old_password_2';
103
104
    my $history1 = Koha::PatronPasswordHistory->new(
105
        {
106
            borrowernumber => $patron->borrowernumber,
107
            password       => Koha::AuthUtils::hash_password($old_password1)
108
        }
109
    );
110
    $history1->store;
111
    sleep(1);
112
113
    my $history2 = Koha::PatronPasswordHistory->new(
114
        {
115
            borrowernumber => $patron->borrowernumber,
116
            password       => Koha::AuthUtils::hash_password($old_password2)
117
        }
118
    );
119
    $history2->store;
120
121
    # Test checking against current password
122
    my $is_used = Koha::PatronPasswordHistories->has_used_password(
123
        {
124
            borrowernumber   => $patron->borrowernumber,
125
            password         => 'current_password',
126
            current_password => $patron->password
127
        }
128
    );
129
    ok( $is_used, 'Current password detected as already used' );
130
131
    # Test checking against history
132
    $is_used = Koha::PatronPasswordHistories->has_used_password(
133
        {
134
            borrowernumber   => $patron->borrowernumber,
135
            password         => $old_password1,
136
            current_password => $patron->password
137
        }
138
    );
139
    ok( $is_used, 'Old password from history detected as already used' );
140
141
    $is_used = Koha::PatronPasswordHistories->has_used_password(
142
        {
143
            borrowernumber   => $patron->borrowernumber,
144
            password         => $old_password2,
145
            current_password => $patron->password
146
        }
147
    );
148
    ok( $is_used, 'Another old password from history detected as already used' );
149
150
    # Test new password
151
    $is_used = Koha::PatronPasswordHistories->has_used_password(
152
        {
153
            borrowernumber   => $patron->borrowernumber,
154
            password         => 'brand_new_password',
155
            current_password => $patron->password
156
        }
157
    );
158
    ok( !$is_used, 'New password not detected as used' );
159
160
    # Test with category history_count = 1 (only checks current)
161
    $category->password_history_count(1)->store;
162
163
    $is_used = Koha::PatronPasswordHistories->has_used_password(
164
        {
165
            borrowernumber   => $patron->borrowernumber,
166
            password         => $old_password1,
167
            current_password => $patron->password
168
        }
169
    );
170
    ok( !$is_used, 'With history_count=1, old passwords not checked against history' );
171
172
    $is_used = Koha::PatronPasswordHistories->has_used_password(
173
        {
174
            borrowernumber   => $patron->borrowernumber,
175
            password         => 'current_password',
176
            current_password => $patron->password
177
        }
178
    );
179
    ok( $is_used, 'With history_count=1, current password still checked' );
180
181
    # Test with category history_count = 0 (no checking)
182
    $category->password_history_count(0)->store;
183
184
    $is_used = Koha::PatronPasswordHistories->has_used_password(
185
        {
186
            borrowernumber   => $patron->borrowernumber,
187
            password         => 'current_password',
188
            current_password => $patron->password
189
        }
190
    );
191
    ok( !$is_used, 'With history_count=0, no password checking performed' );
192
193
    # Test fallback to system preference
194
    $category->password_history_count(undef)->store;
195
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 );
196
197
    $is_used = Koha::PatronPasswordHistories->has_used_password(
198
        {
199
            borrowernumber   => $patron->borrowernumber,
200
            password         => $old_password2,
201
            current_password => $patron->password
202
        }
203
    );
204
    ok( $is_used, 'Falls back to system preference when category setting is null' );
205
206
    # Test without current password parameter
207
    $is_used = Koha::PatronPasswordHistories->has_used_password(
208
        {
209
            borrowernumber => $patron->borrowernumber,
210
            password       => $old_password1
211
        }
212
    );
213
    ok( $is_used, 'Can check password history without current_password parameter' );
214
215
    # Test with patron that has no password history
216
    my $new_patron = $builder->build_object(
217
        {
218
            class => 'Koha::Patrons',
219
            value => {
220
                categorycode => $category->categorycode,
221
                password     => Koha::AuthUtils::hash_password('new_patron_password')
222
            }
223
        }
224
    );
225
226
    $is_used = Koha::PatronPasswordHistories->has_used_password(
227
        {
228
            borrowernumber   => $new_patron->borrowernumber,
229
            password         => 'some_password',
230
            current_password => $new_patron->password
231
        }
232
    );
233
    ok( !$is_used, 'Returns false for patron with no password history' );
234
235
    $schema->storage->txn_rollback;
236
};
237
238
subtest 'edge cases and validation' => sub {
239
    plan tests => 4;
240
241
    $schema->storage->txn_begin;
242
243
    # Test with missing parameters
244
    my $is_used = Koha::PatronPasswordHistories->has_used_password( {} );
245
    ok( !$is_used, 'Returns false with no parameters' );
246
247
    $is_used = Koha::PatronPasswordHistories->has_used_password( { borrowernumber => 99999 } );
248
    ok( !$is_used, 'Returns false with missing password' );
249
250
    $is_used = Koha::PatronPasswordHistories->has_used_password( { password => 'test' } );
251
    ok( !$is_used, 'Returns false with missing borrowernumber' );
252
253
    # Test with non-existent patron
254
    $is_used = Koha::PatronPasswordHistories->has_used_password(
255
        {
256
            borrowernumber => 99999,
257
            password       => 'test_password'
258
        }
259
    );
260
    ok( !$is_used, 'Returns false for non-existent patron' );
261
262
    $schema->storage->txn_rollback;
263
};
(-)a/t/db_dependent/Koha/PatronPasswordHistory.t (-1 / +203 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Test::More tests => 4;
21
use Test::NoWarnings;
22
23
use t::lib::TestBuilder;
24
use t::lib::Mocks;
25
26
use Koha::Database;
27
use Koha::PatronPasswordHistory;
28
use Koha::PatronPasswordHistories;
29
30
my $schema  = Koha::Database->new->schema;
31
my $builder = t::lib::TestBuilder->new;
32
33
subtest 'basic functionality' => sub {
34
    plan tests => 3;
35
36
    $schema->storage->txn_begin;
37
38
    my $category = $builder->build_object(
39
        {
40
            class => 'Koha::Patron::Categories',
41
            value => { password_history_count => 3 }
42
        }
43
    );
44
    my $patron = $builder->build_object(
45
        {
46
            class => 'Koha::Patrons',
47
            value => { categorycode => $category->categorycode }
48
        }
49
    );
50
51
    my $password_history = Koha::PatronPasswordHistory->new(
52
        {
53
            borrowernumber => $patron->borrowernumber,
54
            password       => 'plaintext_password'
55
        }
56
    );
57
58
    ok( $password_history, 'PatronPasswordHistory object created' );
59
    isa_ok( $password_history, 'Koha::PatronPasswordHistory' );
60
61
    $password_history->store;
62
    ok( $password_history->in_storage, 'PatronPasswordHistory stored successfully' );
63
64
    $schema->storage->txn_rollback;
65
};
66
67
subtest 'store password' => sub {
68
    plan tests => 1;
69
70
    $schema->storage->txn_begin;
71
72
    my $category = $builder->build_object(
73
        {
74
            class => 'Koha::Patron::Categories',
75
            value => { password_history_count => 3 }
76
        }
77
    );
78
    my $patron = $builder->build_object(
79
        {
80
            class => 'Koha::Patrons',
81
            value => { categorycode => $category->categorycode }
82
        }
83
    );
84
85
    my $plain_password   = 'test_password_123';
86
    my $hashed_password  = Koha::AuthUtils::hash_password($plain_password);
87
    my $password_history = Koha::PatronPasswordHistory->new(
88
        {
89
            borrowernumber => $patron->borrowernumber,
90
            password       => $hashed_password
91
        }
92
    );
93
94
    $password_history->store;
95
96
    is( $password_history->password, $hashed_password, 'Password is stored as provided' );
97
98
    $schema->storage->txn_rollback;
99
};
100
101
subtest 'cleanup of old history entries' => sub {
102
    plan tests => 5;
103
104
    $schema->storage->txn_begin;
105
106
    my $category = $builder->build_object(
107
        {
108
            class => 'Koha::Patron::Categories',
109
            value => { password_history_count => 3 }
110
        }
111
    );
112
    my $patron = $builder->build_object(
113
        {
114
            class => 'Koha::Patrons',
115
            value => { categorycode => $category->categorycode }
116
        }
117
    );
118
119
    # Create multiple password history entries
120
    for my $i ( 1 .. 5 ) {
121
        my $password_history = Koha::PatronPasswordHistory->new(
122
            {
123
                borrowernumber => $patron->borrowernumber,
124
                password       => "password_$i"
125
            }
126
        );
127
        $password_history->store;
128
        sleep(1);    # Ensure different created_on timestamps
129
    }
130
131
    my $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
132
133
    # With password_history_count = 3, we should keep 2 historical entries (3 - 1 for current)
134
    is( $count, 2, 'Old password history entries cleaned up correctly (kept 2 for history_count 3)' );
135
136
    # Test with different category setting - add more entries to test the limit
137
    $category->password_history_count(4)->store;
138
139
    # Add more password entries to exceed the limit
140
    for my $i ( 6 .. 10 ) {
141
        my $password_history = Koha::PatronPasswordHistory->new(
142
            {
143
                borrowernumber => $patron->borrowernumber,
144
                password       => "password_$i"
145
            }
146
        );
147
        $password_history->store;
148
        sleep(1);
149
    }
150
151
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
152
153
    # With password_history_count = 4, we should keep 3 historical entries (4 - 1 for current)
154
    is( $count, 3, 'Cleanup respects updated category password_history_count and does not exceed limit' );
155
156
    # Test with history_count = 1 (should keep 1 historical entry)
157
    $category->password_history_count(1)->store;
158
159
    my $password_history2 = Koha::PatronPasswordHistory->new(
160
        {
161
            borrowernumber => $patron->borrowernumber,
162
            password       => "password_7"
163
        }
164
    );
165
    $password_history2->store;
166
167
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
168
169
    is( $count, 1, 'With history_count=1, keeps exactly 1 entry' );
170
171
    # Test with history_count = 0 (should delete all)
172
    $category->password_history_count(0)->store;
173
174
    my $password_history3 = Koha::PatronPasswordHistory->new(
175
        {
176
            borrowernumber => $patron->borrowernumber,
177
            password       => "password_8"
178
        }
179
    );
180
    $password_history3->store;
181
182
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
183
184
    is( $count, 0, 'With history_count=0, deletes all entries' );
185
186
    # Test fallback to system preference
187
    $category->password_history_count(undef)->store;
188
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 );
189
190
    my $password_history4 = Koha::PatronPasswordHistory->new(
191
        {
192
            borrowernumber => $patron->borrowernumber,
193
            password       => "password_9"
194
        }
195
    );
196
    $password_history4->store;
197
198
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
199
200
    is( $count, 1, 'Falls back to system preference when category setting is null' );
201
202
    $schema->storage->txn_rollback;
203
};

Return to bug 40824