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

(-)a/Koha/AuthUtils.pm (-3 / +10 lines)
Lines 23-28 use Encode qw( encode is_utf8 ); Link Here
23
use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt
23
use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt
24
use List::MoreUtils qw/ any /;
24
use List::MoreUtils qw/ any /;
25
use String::Random qw( random_string );
25
use String::Random qw( random_string );
26
use Koha::Exceptions::Password;
26
27
27
use C4::Context;
28
use C4::Context;
28
29
Lines 148-159 otherwise return $is_valid == 0 and $error will contain the error ('too_short' o Link Here
148
149
149
sub is_password_valid {
150
sub is_password_valid {
150
    my ($password, $category) = @_;
151
    my ($password, $category) = @_;
151
    my $minPasswordLength = $category?$category->effective_min_password_length:C4::Context->preference('minPasswordLength');
152
    if(!$category) {
153
        Koha::Exceptions::Password::NoCategoryProvided->throw();
154
    }
155
    my $minPasswordLength = $category->effective_min_password_length;
152
    $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
156
    $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
153
    if ( length($password) < $minPasswordLength ) {
157
    if ( length($password) < $minPasswordLength ) {
154
        return ( 0, 'too_short' );
158
        return ( 0, 'too_short' );
155
    }
159
    }
156
    elsif ( $category?$category->effective_require_strong_password:C4::Context->preference('RequireStrongPassword') ) {
160
    elsif ( $category->effective_require_strong_password ) {
157
        return ( 0, 'too_weak' )
161
        return ( 0, 'too_weak' )
158
          if $password !~ m|(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{$minPasswordLength,}|;
162
          if $password !~ m|(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{$minPasswordLength,}|;
159
    }
163
    }
Lines 171-177 Generate a password according to category's minimum password length and strength Link Here
171
175
172
sub generate_password {
176
sub generate_password {
173
    my ($category) = @_;
177
    my ($category) = @_;
174
    my $minPasswordLength = $category?$category->effective_min_password_length:C4::Context->preference('minPasswordLength');
178
    if(!$category) {
179
        Koha::Exceptions::Password::NoCategoryProvided->throw();
180
    }
181
    my $minPasswordLength = $category->effective_min_password_length;
175
    $minPasswordLength = 8 if not $minPasswordLength or $minPasswordLength < 8;
182
    $minPasswordLength = 8 if not $minPasswordLength or $minPasswordLength < 8;
176
183
177
    my ( $password, $is_valid );
184
    my ( $password, $is_valid );
(-)a/Koha/Exceptions/Password.pm (+4 lines)
Lines 43-48 use Exception::Class ( Link Here
43
        isa => 'Koha::Exceptions::Password',
43
        isa => 'Koha::Exceptions::Password',
44
        description => 'The password was rejected by a plugin'
44
        description => 'The password was rejected by a plugin'
45
    },
45
    },
46
    'Koha::Exceptions::Password::NoCategoryProvided' => {
47
        isa => 'Koha::Exceptions::Password',
48
        description => 'You must provide a patron\'s category to validate password\'s strength and length'
49
    }
46
);
50
);
47
51
48
sub full_message {
52
sub full_message {
(-)a/installer/onboarding.pl (-4 / +8 lines)
Lines 144-151 if ( $step == 3 ) { Link Here
144
        my $secondpassword = $input->param('password2') || '';
144
        my $secondpassword = $input->param('password2') || '';
145
        my $cardnumber     = $input->param('cardnumber');
145
        my $cardnumber     = $input->param('cardnumber');
146
        my $userid         = $input->param('userid');
146
        my $userid         = $input->param('userid');
147
        my $categorycode = $input->param('categorycode_entry');
148
        my $patron_category =
149
          Koha::Patron::Categories->find( $categorycode );
150
151
        my ( $is_valid, $passworderror ) =
152
          Koha::AuthUtils::is_password_valid( $firstpassword,
153
            $patron_category );
147
154
148
        my ( $is_valid, $passworderror) = Koha::AuthUtils::is_password_valid( $firstpassword );
149
155
150
        if ( my $error_code = checkcardnumber($cardnumber) ) {
156
        if ( my $error_code = checkcardnumber($cardnumber) ) {
151
            if ( $error_code == 1 ) {
157
            if ( $error_code == 1 ) {
Lines 171-177 if ( $step == 3 ) { Link Here
171
                firstname    => scalar $input->param('firstname'),
177
                firstname    => scalar $input->param('firstname'),
172
                cardnumber   => scalar $input->param('cardnumber'),
178
                cardnumber   => scalar $input->param('cardnumber'),
173
                branchcode   => scalar $input->param('libraries'),
179
                branchcode   => scalar $input->param('libraries'),
174
                categorycode => scalar $input->param('categorycode_entry'),
180
                categorycode => $categorycode,
175
                userid       => scalar $input->param('userid'),
181
                userid       => scalar $input->param('userid'),
176
                privacy      => "default",
182
                privacy      => "default",
177
                address      => "",
183
                address      => "",
Lines 179-186 if ( $step == 3 ) { Link Here
179
                flags        => 1,    # Will be superlibrarian
185
                flags        => 1,    # Will be superlibrarian
180
            };
186
            };
181
187
182
            my $patron_category =
183
              Koha::Patron::Categories->find( $patron_data->{categorycode} );
184
            $patron_data->{dateexpiry} =
188
            $patron_data->{dateexpiry} =
185
              $patron_category->get_expiry_date( $patron_data->{dateenrolled} );
189
              $patron_category->get_expiry_date( $patron_data->{dateenrolled} );
186
190
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt (-1 / +1 lines)
Lines 231-237 Link Here
231
                </li>
231
                </li>
232
                <li>
232
                <li>
233
                    <label for="min_password_length">Minimum password length:</label>
233
                    <label for="min_password_length">Minimum password length:</label>
234
                    <input id="min_password_length" type="number" name="min_password_length" value="[% category.min_password_length | html %]"/>
234
                    <input id="min_password_length" type="text" inputmode="numeric" pattern="[0-9]*" name="min_password_length" value="[% category.min_password_length | html %]"/>
235
                    <span>Leave blank to use system default ([% Koha.Preference('minPasswordLength') | html %])</span>
235
                    <span>Leave blank to use system default ([% Koha.Preference('minPasswordLength') | html %])</span>
236
                </li>
236
                </li>
237
                <li class="pwd_setting_wrapper">
237
                <li class="pwd_setting_wrapper">
(-)a/misc/admin/set_password.pl (-8 / +5 lines)
Lines 44-49 unless ( $userid or $patron_id or $cardnumber ) { Link Here
44
    pod2usage("cardnumber is mandatory")   unless $cardnumber;
44
    pod2usage("cardnumber is mandatory")   unless $cardnumber;
45
}
45
}
46
46
47
unless ($password) {
48
    my $generator  = String::Random->new( rand_gen => \&alt_rand );
49
    $password      = $generator->randregex('[A-Za-z][A-Za-z0-9_]{6}.[A-Za-z][A-Za-z0-9_]{6}\d');
50
}
51
47
my $filter;
52
my $filter;
48
53
49
if ( $userid ) {
54
if ( $userid ) {
Lines 65-78 unless ( $patrons->count > 0 ) { Link Here
65
}
70
}
66
71
67
my $patron = $patrons->next;
72
my $patron = $patrons->next;
68
69
unless ($password) {
70
    my $generator  = String::Random->new( rand_gen => \&alt_rand );
71
    my $n = $patron->category->effective_min_password_length;
72
    $n = $n<6?6:$n;
73
    $password      = $generator->randregex('[A-Za-z][A-Za-z0-9_]{6}.[A-Za-z][A-Za-z0-9_]{'.$n.'}\d');
74
}
75
76
$patron->set_password({ password => $password, skip_validation => 1 });
73
$patron->set_password({ password => $password, skip_validation => 1 });
77
74
78
print $patron->userid . " " . $password . "\n";
75
print $patron->userid . " " . $password . "\n";
(-)a/t/AuthUtils.t (-73 lines)
Lines 1-73 Link Here
1
# This file is part of Koha.
2
#
3
# Copyright (C) 2013 Equinox Software, Inc.
4
# Copyright 2017 Koha Development Team
5
#
6
# Koha is free software; you can redistribute it and/or modify it
7
# under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# Koha is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with Koha; if not, see <http://www.gnu.org/licenses>.
18
19
use Modern::Perl;
20
use Test::More tests => 3;
21
22
use t::lib::Mocks;
23
use Koha::AuthUtils qw/hash_password/;
24
25
my $hash1 = hash_password('password');
26
my $hash2 = hash_password('password');
27
28
ok($hash1 ne $hash2, 'random salts used when generating password hash');
29
30
subtest 'is_password_valid' => sub {
31
    plan tests => 12;
32
33
    my ( $is_valid, $error );
34
35
    t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
36
    t::lib::Mocks::mock_preference('minPasswordLength', 0);
37
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '12' );
38
    is( $is_valid, 0, 'min password size should be 3' );
39
    is( $error, 'too_short', 'min password size should be 3' );
40
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( ' 123' );
41
    is( $is_valid, 0, 'password should not contain leading spaces' );
42
    is( $error, 'has_whitespaces', 'password should not contain leading spaces' );
43
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '123 ' );
44
    is( $is_valid, 0, 'password should not contain trailing spaces' );
45
    is( $error, 'has_whitespaces', 'password should not contain trailing spaces' );
46
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '123' );
47
    is( $is_valid, 1, 'min password size should be 3' );
48
49
    t::lib::Mocks::mock_preference('RequireStrongPassword', 1);
50
    t::lib::Mocks::mock_preference('minPasswordLength', 8);
51
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '12345678' );
52
    is( $is_valid, 0, 'password should be strong' );
53
    is( $error, 'too_weak', 'password should be strong' );
54
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'abcd1234' );
55
    is( $is_valid, 0, 'strong password should contain uppercase' );
56
    is( $error, 'too_weak', 'strong password should contain uppercase' );
57
58
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'abcD1234' );
59
    is( $is_valid, 1, 'strong password should contain uppercase' );
60
};
61
62
subtest 'generate_password' => sub {
63
    plan tests => 1;
64
    t::lib::Mocks::mock_preference('RequireStrongPassword', 1);
65
    t::lib::Mocks::mock_preference('minPasswordLength', 8);
66
    my $all_valid = 1;
67
    for ( 1 .. 10 ) {
68
        my $password = Koha::AuthUtils::generate_password;
69
        my ( $is_valid, undef ) = Koha::AuthUtils::is_password_valid( $password );
70
        $all_valid = 0 unless $is_valid;
71
    }
72
    is ( $all_valid, 1, 'generate_password should generate valid passwords' );
73
};
(-)a/t/db_dependent/AuthUtils.t (-46 / +95 lines)
Lines 18-23 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 2;
20
use Test::More tests => 2;
21
use Test::Exception;
21
use Test::MockModule;
22
use Test::MockModule;
22
use t::lib::Mocks;
23
use t::lib::Mocks;
23
use t::lib::TestBuilder;
24
use t::lib::TestBuilder;
Lines 28-114 my $builder = t::lib::TestBuilder->new; Link Here
28
29
29
$schema->storage->txn_begin;
30
$schema->storage->txn_begin;
30
31
31
my $category1 = $builder->build_object({class=>'Koha::Patron::Categories', value=>{min_password_length => 15, require_strong_password => 1}});
32
my $category1 = $builder->build_object(
32
my $category2 = $builder->build_object({class=>'Koha::Patron::Categories', value=>{min_password_length => 5, require_strong_password => undef}});
33
    {
33
my $category3 = $builder->build_object({class=>'Koha::Patron::Categories', value=>{min_password_length => undef, require_strong_password => 1}});
34
        class => 'Koha::Patron::Categories',
34
my $category4 = $builder->build_object({class=>'Koha::Patron::Categories', value=>{min_password_length => undef, require_strong_password => undef}});
35
        value => { min_password_length => 15, require_strong_password => 1 }
35
36
    }
36
my $p_3l_weak = '123';
37
);
37
my $p_3l_strong = '1Ab';
38
my $category2 = $builder->build_object(
38
my $p_5l_weak = 'abcde';
39
    {
39
my $p_15l_weak = '0123456789abcdf';
40
        class => 'Koha::Patron::Categories',
40
my $p_5l_strong = 'Abc12';
41
        value => { min_password_length => 5, require_strong_password => undef }
42
    }
43
);
44
my $category3 = $builder->build_object(
45
    {
46
        class => 'Koha::Patron::Categories',
47
        value => { min_password_length => undef, require_strong_password => 1 }
48
    }
49
);
50
my $category4 = $builder->build_object(
51
    {
52
        class => 'Koha::Patron::Categories',
53
        value =>
54
          { min_password_length => undef, require_strong_password => undef }
55
    }
56
);
57
58
my $p_2l         = '1A';
59
my $p_3l_weak    = '123';
60
my $p_3l_strong  = '1Ab';
61
my $p_5l_weak    = 'abcde';
62
my $p_15l_weak   = '0123456789abcdf';
63
my $p_5l_strong  = 'Abc12';
41
my $p_15l_strong = '0123456789AbCdF';
64
my $p_15l_strong = '0123456789AbCdF';
42
65
43
subtest 'is_password_valid for category' => sub {
66
subtest 'is_password_valid for category' => sub {
44
    plan tests => 12;
67
    plan tests => 15;
45
68
46
    my ( $is_valid, $error );
69
    my ( $is_valid, $error );
47
70
48
    t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
71
    t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
49
    t::lib::Mocks::mock_preference('minPasswordLength', 3);
72
    t::lib::Mocks::mock_preference( 'minPasswordLength',     3 );
50
73
51
    #Category 1 - override=>1, length=>15, strong=>1
74
    #Category 1 - override=>1, length=>15, strong=>1
52
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_5l_strong, $category1 );
75
    ( $is_valid, $error ) =
53
    is($is_valid, 0, 'min password length for this category is 15');
76
      Koha::AuthUtils::is_password_valid( $p_5l_strong, $category1 );
54
    is($error, 'too_short', 'min password length for this category is 15');
77
    is( $is_valid, 0,           'min password length for this category is 15' );
78
    is( $error,    'too_short', 'min password length for this category is 15' );
55
79
56
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_15l_weak, $category1 );
80
    ( $is_valid, $error ) =
57
    is($is_valid, 0, 'password should be strong for this category');
81
      Koha::AuthUtils::is_password_valid( $p_15l_weak, $category1 );
58
    is($error, 'too_weak', 'password should be strong for this category');
82
    is( $is_valid, 0,          'password should be strong for this category' );
83
    is( $error,    'too_weak', 'password should be strong for this category' );
59
84
60
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_15l_strong, $category1 );
85
    ( $is_valid, $error ) =
61
    is($is_valid, 1, 'password should be ok for this category');
86
      Koha::AuthUtils::is_password_valid( $p_15l_strong, $category1 );
87
    is( $is_valid, 1, 'password should be ok for this category' );
62
88
63
    #Category 2 - override=>1, length=>5, strong=>0
89
    #Category 2 - override=>1, length=>5, strong=>0
64
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_3l_strong, $category2 );
90
    ( $is_valid, $error ) =
65
    is($is_valid, 0, 'min password length for this category is 5');
91
      Koha::AuthUtils::is_password_valid( $p_3l_strong, $category2 );
66
    is($error, 'too_short', 'min password length for this category is 5');
92
    is( $is_valid, 0,           'min password length for this category is 5' );
93
    is( $error,    'too_short', 'min password length for this category is 5' );
67
94
68
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_5l_weak, $category2 );
95
    ( $is_valid, $error ) =
69
    is($is_valid, 1, 'password should be ok for this category');
96
      Koha::AuthUtils::is_password_valid( $p_5l_weak, $category2 );
97
    is( $is_valid, 1, 'password should be ok for this category' );
70
98
71
    #Category 3 - override=>0, length=>20, strong=>0
99
    #Category 3 - override=>0, length=>20, strong=>0
72
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_3l_weak, $category3 );
100
    ( $is_valid, $error ) =
73
    is($is_valid, 0, 'password should be strong');
101
      Koha::AuthUtils::is_password_valid( $p_3l_weak, $category3 );
74
    is($error, 'too_weak', 'password should be strong');
102
    is( $is_valid, 0,          'password should be strong' );
103
    is( $error,    'too_weak', 'password should be strong' );
75
104
76
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_3l_strong, $category3 );
105
    ( $is_valid, $error ) =
77
    is($is_valid, 1, 'password should be ok');
106
      Koha::AuthUtils::is_password_valid( $p_3l_strong, $category3 );
107
    is( $is_valid, 1, 'password should be ok' );
78
108
79
    #Category 4 - default settings - override=>undef, length=>undef, strong=>undef
109
    #Category 4 - default settings - override=>undef, length=>undef, strong=>undef
80
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $p_3l_weak, $category4 );
110
    ( $is_valid, $error ) =
81
    is($is_valid, 1, 'password should be ok');
111
      Koha::AuthUtils::is_password_valid( $p_3l_weak, $category4 );
112
    is( $is_valid, 1, 'password should be ok' );
113
114
    t::lib::Mocks::mock_preference( 'minPasswordLength', 0 );
115
    ( $is_valid, $error ) =
116
      Koha::AuthUtils::is_password_valid( $p_2l, $category4 );
117
    is( $is_valid, 0,           '3 is absolute minimum password' );
118
    is( $error,    'too_short', '3 is absolute minimum password' );
119
120
    throws_ok { Koha::AuthUtils::is_password_valid($p_2l); }
121
    'Koha::Exceptions::Password::NoCategoryProvided',
122
      'Category should always be provided';
123
82
};
124
};
83
125
84
subtest 'generate_password for category' => sub {
126
subtest 'generate_password for category' => sub {
85
    plan tests => 4;
127
    plan tests => 5;
86
128
87
    my ( $is_valid, $error );
129
    my ( $is_valid, $error );
88
130
89
    t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
131
    t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
90
    t::lib::Mocks::mock_preference('minPasswordLength', 3);
132
    t::lib::Mocks::mock_preference( 'minPasswordLength',     3 );
91
133
92
    #Category 4
134
    #Category 4
93
    my $password = Koha::AuthUtils::generate_password($category4);
135
    my $password = Koha::AuthUtils::generate_password($category4);
94
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $category4 );
136
    ( $is_valid, $error ) =
95
    is($is_valid, 1, 'password should be ok');
137
      Koha::AuthUtils::is_password_valid( $password, $category4 );
138
    is( $is_valid, 1, 'password should be ok' );
96
139
97
    #Category 3
140
    #Category 3
98
    $password = Koha::AuthUtils::generate_password($category3);
141
    $password = Koha::AuthUtils::generate_password($category3);
99
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $category3 );
142
    ( $is_valid, $error ) =
100
    is($is_valid, 1, 'password should be ok');
143
      Koha::AuthUtils::is_password_valid( $password, $category3 );
144
    is( $is_valid, 1, 'password should be ok' );
101
145
102
    #Category 2
146
    #Category 2
103
    $password = Koha::AuthUtils::generate_password($category2);
147
    $password = Koha::AuthUtils::generate_password($category2);
104
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $category2 );
148
    ( $is_valid, $error ) =
105
    is($is_valid, 1, 'password should be ok');
149
      Koha::AuthUtils::is_password_valid( $password, $category2 );
150
    is( $is_valid, 1, 'password should be ok' );
106
151
107
    #Category 1
152
    #Category 1
108
    $password = Koha::AuthUtils::generate_password($category1);
153
    $password = Koha::AuthUtils::generate_password($category1);
109
    ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $category1 );
154
    ( $is_valid, $error ) =
110
    is($is_valid, 1, 'password should be ok');
155
      Koha::AuthUtils::is_password_valid( $password, $category1 );
156
    is( $is_valid, 1, 'password should be ok' );
157
158
    throws_ok { Koha::AuthUtils::generate_password(); }
159
    'Koha::Exceptions::Password::NoCategoryProvided',
160
      'Category should always be provided';
111
161
112
};
162
};
113
163
114
$schema->storage->txn_rollback;
164
$schema->storage->txn_rollback;
115
- 

Return to bug 23816