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

(-)a/Koha/AuthUtils.pm (-6 / +42 lines)
Lines 22-31 use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); Link Here
22
use Encode qw( encode is_utf8 );
22
use Encode qw( encode is_utf8 );
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 random_regex );
26
use Koha::Exceptions::Password;
26
use Koha::Exceptions::Password;
27
27
28
use C4::Context;
28
use C4::Context;
29
use Koha::Patron::Categories;
29
30
30
use base 'Exporter';
31
use base 'Exporter';
31
32
Lines 154-168 sub is_password_valid { Link Here
154
    }
155
    }
155
    my $minPasswordLength = $category->effective_min_password_length;
156
    my $minPasswordLength = $category->effective_min_password_length;
156
    $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
157
    $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
157
    if ( length($password) < $minPasswordLength ) {
158
    my $passwordpolicy = $category->passwordpolicy;
159
160
    if ($passwordpolicy) {
161
        if ($passwordpolicy eq "complex") {
162
            unless ($password =~ /[0-9]/
163
                && $password =~ /[a-zåäö]/
164
                && $password =~ /[A-ZÅÄÖ]/
165
                && $password =~ /[\|\[\]\{\}!@#\$%\^&\*\(\)_\-\+\?]/
166
                && length($password) >= $minPasswordLength ) {
167
                return (0, "complex_policy_mismatch");
168
            }
169
        }
170
        elsif ($passwordpolicy eq "alphanumeric") {
171
            unless ($password =~ /[0-9]/
172
                && $password =~ /[a-zA-ZöäåÖÄÅ]/
173
                && $password !~ /\W/
174
                && $password !~ /[_-]/
175
                && length($password) >= $minPasswordLength ) {
176
                return (0, "alpha_policy_mismatch");
177
            }
178
        }
179
        else {
180
            if ($password !~ /^[0-9]+$/ || length($password) < $minPasswordLength) {
181
                return (0, "simple_policy_mismatch");
182
            }
183
        }
184
    } 
185
    elsif ( length($password) < $minPasswordLength ) {
158
        return ( 0, 'too_short' );
186
        return ( 0, 'too_short' );
159
    }
187
    }
160
    elsif ( $category->effective_require_strong_password ) {
188
    elsif ( $category->effective_require_strong_password ) {
161
        return ( 0, 'too_weak' )
189
        return ( 0, 'too_weak' )
162
          if $password !~ m|(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{$minPasswordLength,}|;
190
          if $password !~ m|(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{$minPasswordLength,}|;   
163
    }
191
    }
164
    return ( 0, 'has_whitespaces' ) if $password =~ m[^\s|\s$];
192
    return ( 0, 'has_whitespaces' ) if $password =~ m[^\s|\s$];
165
    return ( 1, undef );
193
    return 1;
166
}
194
}
167
195
168
=head2 generate_password
196
=head2 generate_password
Lines 180-195 sub generate_password { Link Here
180
    }
208
    }
181
    my $minPasswordLength = $category->effective_min_password_length;
209
    my $minPasswordLength = $category->effective_min_password_length;
182
    $minPasswordLength = 8 if not $minPasswordLength or $minPasswordLength < 8;
210
    $minPasswordLength = 8 if not $minPasswordLength or $minPasswordLength < 8;
211
    my $passwordpolicy = $category->passwordpolicy;
183
212
184
    my ( $password, $is_valid );
213
    my ( $password, $is_valid );
185
    do {
214
    do {
186
        $password = random_string('.' x $minPasswordLength );
215
        if (!$passwordpolicy || $passwordpolicy eq "complex") {
216
            $password = random_string('.' x $minPasswordLength);
217
        } else {
218
            if ($passwordpolicy eq "alphanumeric") {
219
                $password = random_regex('[a-zA-Z0-9]' x $minPasswordLength);
220
            } else {
221
                $password = random_regex('[0-9]' x $minPasswordLength);
222
            }
223
        }
187
        ( $is_valid, undef ) = is_password_valid( $password, $category );
224
        ( $is_valid, undef ) = is_password_valid( $password, $category );
188
    } while not $is_valid;
225
    } while not $is_valid;
189
    return $password;
226
    return $password;
190
}
227
}
191
228
192
193
=head2 get_script_name
229
=head2 get_script_name
194
230
195
This returns the correct script name, for use in redirecting back to the correct page after showing
231
This returns the correct script name, for use in redirecting back to the correct page after showing
(-)a/Koha/Exceptions/Password.pm (-1 / +13 lines)
Lines 46-52 use Exception::Class ( Link Here
46
    'Koha::Exceptions::Password::NoCategoryProvided' => {
46
    'Koha::Exceptions::Password::NoCategoryProvided' => {
47
        isa => 'Koha::Exceptions::Password',
47
        isa => 'Koha::Exceptions::Password',
48
        description => 'You must provide a patron\'s category to validate password\'s strength and length'
48
        description => 'You must provide a patron\'s category to validate password\'s strength and length'
49
    }
49
    },
50
    'Koha::Exceptions::Password::SimplePolicy' => {
51
        isa => 'Koha::Exceptions::Password',
52
        description => 'Password does not match simplenumeric passwordpolicy',
53
    },
54
    'Koha::Exceptions::Password::AlphaPolicy' => {
55
        isa => 'Koha::Exceptions::Password',
56
        description => 'Password does not match alphanumeric passwordpolicy',
57
    },
58
    'Koha::Exceptions::Password::ComplexPolicy' => {
59
        isa => 'Koha::Exceptions::Password',
60
        description => 'Password does not match complex passwordpolicy',
61
    },
50
);
62
);
51
63
52
sub full_message {
64
sub full_message {
(-)a/Koha/Patron.pm (-1 / +15 lines)
Lines 734-739 Exceptions are thrown if the password is not good enough. Link Here
734
734
735
=item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
735
=item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
736
736
737
=item Koha::Exceptions::Password::SimplePolicy
738
739
=item Koha::Exceptions::Password::AlphaPolicy
740
741
=item Koha::Exceptions::Password::ComplexPolicy
742
737
=back
743
=back
738
744
739
=cut
745
=cut
Lines 761-766 sub set_password { Link Here
761
            elsif ( $error eq 'too_weak' ) {
767
            elsif ( $error eq 'too_weak' ) {
762
                Koha::Exceptions::Password::TooWeak->throw();
768
                Koha::Exceptions::Password::TooWeak->throw();
763
            }
769
            }
770
            elsif ( $error eq 'simple_policy_mismatch' ) {
771
                Koha::Exceptions::Password::SimplePolicy->throw();
772
            }
773
            elsif ( $error eq 'alpha_policy_mismatch' ) {
774
                Koha::Exceptions::Password::AlphaPolicy->throw();
775
            }
776
            elsif ( $error eq 'complex_policy_mismatch' ) {
777
                Koha::Exceptions::Password::ComplexPolicy->throw();
778
            }
764
        }
779
        }
765
    }
780
    }
766
781
Lines 801-807 sub set_password { Link Here
801
    return $self;
816
    return $self;
802
}
817
}
803
818
804
805
=head3 renew_account
819
=head3 renew_account
806
820
807
my $new_expiry_date = $patron->renew_account
821
my $new_expiry_date = $patron->renew_account
(-)a/Koha/Schema/Result/Category.pm (+7 lines)
Lines 142-147 __PACKAGE__->table("categories"); Link Here
142
142
143
  data_type: 'tinyint'
143
  data_type: 'tinyint'
144
  is_nullable: 1
144
  is_nullable: 1
145
=head2 passwordpolicy
146
147
  data_type: 'varchar'
148
  is_nullable: 1
149
  size: 40
145
150
146
=cut
151
=cut
147
152
Lines 203-208 __PACKAGE__->add_columns( Link Here
203
  { data_type => "smallint", is_nullable => 1 },
208
  { data_type => "smallint", is_nullable => 1 },
204
  "require_strong_password",
209
  "require_strong_password",
205
  { data_type => "tinyint", is_nullable => 1 },
210
  { data_type => "tinyint", is_nullable => 1 },
211
  "passwordpolicy",
212
  { data_type => "varchar", is_nullable => 1, size => 40 },
206
);
213
);
207
214
208
=head1 PRIMARY KEY
215
=head1 PRIMARY KEY
(-)a/admin/categories.pl (+3 lines)
Lines 96-101 elsif ( $op eq 'add_validate' ) { Link Here
96
    my $change_password = $input->param('change_password');
96
    my $change_password = $input->param('change_password');
97
    my $min_password_length = $input->param('min_password_length');
97
    my $min_password_length = $input->param('min_password_length');
98
    my $require_strong_password = $input->param('require_strong_password');
98
    my $require_strong_password = $input->param('require_strong_password');
99
    my $selectedpasswordpolicy  = $input->param('passwordpolicy');
99
    my @branches = grep { $_ ne q{} } $input->multi_param('branches');
100
    my @branches = grep { $_ ne q{} } $input->multi_param('branches');
100
101
101
    $reset_password = undef if $reset_password eq -1;
102
    $reset_password = undef if $reset_password eq -1;
Lines 135-140 elsif ( $op eq 'add_validate' ) { Link Here
135
        $category->change_password($change_password);
136
        $category->change_password($change_password);
136
        $category->min_password_length($min_password_length);
137
        $category->min_password_length($min_password_length);
137
        $category->require_strong_password($require_strong_password);
138
        $category->require_strong_password($require_strong_password);
139
        $category->passwordpolicy($selectedpasswordpolicy);
138
        eval {
140
        eval {
139
            $category->store;
141
            $category->store;
140
            $category->replace_branch_limitations( \@branches );
142
            $category->replace_branch_limitations( \@branches );
Lines 165-170 elsif ( $op eq 'add_validate' ) { Link Here
165
            change_password => $change_password,
167
            change_password => $change_password,
166
            min_password_length => $min_password_length,
168
            min_password_length => $min_password_length,
167
            require_strong_password => $require_strong_password,
169
            require_strong_password => $require_strong_password,
170
            passwordpolicy => $selectedpasswordpolicy,
168
        });
171
        });
169
        eval {
172
        eval {
170
            $category->store;
173
            $category->store;
(-)a/installer/data/mysql/atomicupdate/Bug-12617-Koha-should-let-admins-to-configure-automatically-generated-password.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX';  # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do("ALTER TABLE categories ADD COLUMN passwordpolicy VARCHAR(40) DEFAULT NULL AFTER require_strong_password");
4
5
    # Always end with this (adjust the bug info)
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 12617 - Koha should let admins to configure automatically generated password complexity/difficulty)\n";
8
}
(-)a/installer/data/mysql/kohastructure.sql (+1 lines)
Lines 329-334 CREATE TABLE `categories` ( -- this table shows information related to Koha patr Link Here
329
  `change_password` TINYINT(1) NULL DEFAULT NULL, -- if patrons of this category can change their passwords in the OAPC
329
  `change_password` TINYINT(1) NULL DEFAULT NULL, -- if patrons of this category can change their passwords in the OAPC
330
  `min_password_length` smallint(6) NULL DEFAULT NULL, -- set minimum password length for patrons in this category
330
  `min_password_length` smallint(6) NULL DEFAULT NULL, -- set minimum password length for patrons in this category
331
  `require_strong_password` TINYINT(1) NULL DEFAULT NULL, -- set required password strength for patrons in this category
331
  `require_strong_password` TINYINT(1) NULL DEFAULT NULL, -- set required password strength for patrons in this category
332
  `passwordpolicy` varchar(40) default NULL, -- which password policy patron category uses
332
  PRIMARY KEY  (`categorycode`),
333
  PRIMARY KEY  (`categorycode`),
333
  UNIQUE KEY `categorycode` (`categorycode`)
334
  UNIQUE KEY `categorycode` (`categorycode`)
334
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
335
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
(-)a/installer/data/mysql/sysprefs.sql (+2 lines)
Lines 329-334 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
329
('MaxTotalSuggestions','',NULL,'Number of total suggestions used for time limit with NumberOfSuggestionDays','Free'),
329
('MaxTotalSuggestions','',NULL,'Number of total suggestions used for time limit with NumberOfSuggestionDays','Free'),
330
('MembershipExpiryDaysNotice','',NULL,'Send an account expiration notice that a patron\'s card is about to expire after','Integer'),
330
('MembershipExpiryDaysNotice','',NULL,'Send an account expiration notice that a patron\'s card is about to expire after','Integer'),
331
('MergeReportFields','',NULL,'Displayed fields for deleted MARC records after merge','Free'),
331
('MergeReportFields','',NULL,'Displayed fields for deleted MARC records after merge','Free'),
332
('minAlnumPasswordLength', '10', null, 'Specify the minimum length for alphanumeric passwords', 'free')
333
('minComplexPasswordLength', '10', null, 'Specify the minimum length for complex passwords', 'free')
332
('minPasswordLength','8',NULL,'Specify the minimum length of a patron/staff password','free'),
334
('minPasswordLength','8',NULL,'Specify the minimum length of a patron/staff password','free'),
333
('NewItemsDefaultLocation','','','If set, all new items will have a location of the given Location Code ( Authorized Value type LOC )',''),
335
('NewItemsDefaultLocation','','','If set, all new items will have a location of the given Location Code ( Authorized Value type LOC )',''),
334
('NewsAuthorDisplay','none','none|opac|staff|both','Display the author name for news items.','Choice'),
336
('NewsAuthorDisplay','none','none|opac|staff|both','Display the author name for news items.','Choice'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt (+15 lines)
Lines 266-271 Link Here
266
                      [% END %]
266
                      [% END %]
267
                    </select>
267
                    </select>
268
                </li>
268
                </li>
269
                <li>
270
                    <label for="password-policy">Category password policy:</label>
271
                    <select name="passwordpolicy" id="password-policy">
272
                        [% UNLESS category %]<option value="" selected="selected"></option>[% ELSE %]<option value=""></option>[% END %]
273
                        [% IF category and category.passwordpolicy == 'complex' %]<option value="complex" selected="selected">Complex</option>[% ELSE %]<option value="complex">Complex</option>[% END %]
274
                        [% IF category and category.passwordpolicy == 'alphanumeric' %]<option value="alphanumeric" selected="selected">Alphanumeric</option>[% ELSE %]<option value="alphanumeric">Alphanumeric</option>[% END %]
275
                        [% IF category and category.passwordpolicy == 'simplenumeric' %]<option value="simplenumeric" selected="selected">Numbers only</option>[% ELSE %]<option value="simplenumeric">Numbers only</option>[% END %]
276
                    </select>
277
                    <span>
278
                        Selecting a password policy for a category affects both automatically created suggested passwords and enfo$
279
                        of rules.
280
                    </span>
281
                </li>
269
                <li><label for="block_expired">Block expired patrons:</label>
282
                <li><label for="block_expired">Block expired patrons:</label>
270
                    <select name="BlockExpiredPatronOpacActions" id="block_expired">
283
                    <select name="BlockExpiredPatronOpacActions" id="block_expired">
271
                        [% IF not category or category.BlockExpiredPatronOpacActions == -1%]
284
                        [% IF not category or category.BlockExpiredPatronOpacActions == -1%]
Lines 456-461 Link Here
456
                    <th scope="col">Messaging</th>
469
                    <th scope="col">Messaging</th>
457
                    [% END %]
470
                    [% END %]
458
                    <th scope="col">Library limitations</th>
471
                    <th scope="col">Library limitations</th>
472
                    <th scope="col">Password policy</th>
459
                    [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
473
                    [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
460
                    <th scope="col">Check previous checkout?</th>
474
                    <th scope="col">Check previous checkout?</th>
461
                    [% END %]
475
                    [% END %]
Lines 551-556 Link Here
551
                                No limitation
565
                                No limitation
552
                            [% END %]
566
                            [% END %]
553
                        </td>
567
                        </td>
568
                        <td>[% category.passwordpolicy %]</td>
554
                        [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
569
                        [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
555
                          <td>
570
                          <td>
556
                              [% SWITCH category.checkprevcheckout %]
571
                              [% SWITCH category.checkprevcheckout %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref (-1 / +11 lines)
Lines 313-322 Patrons: Link Here
313
         - "days.<br>IMPORTANT: No action is performed when these delays are empty (no text). But a zero value ('0') is interpreted as no delay (do it now)! The actions are performed by the cleanup database cron job."
313
         - "days.<br>IMPORTANT: No action is performed when these delays are empty (no text). But a zero value ('0') is interpreted as no delay (do it now)! The actions are performed by the cleanup database cron job."
314
    Security:
314
    Security:
315
     -
315
     -
316
         - Login passwords for staff and patrons must be at least
316
         - Login passwords for simplenumeric policy must be at least
317
         - pref: minPasswordLength
317
         - pref: minPasswordLength
318
           class: integer
318
           class: integer
319
         - characters long.
319
         - characters long.
320
     -
321
         - Login passwords for alphanumeric policy must be at least
322
         - pref: minAlnumPasswordLength
323
           class: integer
324
         - characters long.
325
     -
326
         - Login passwords for complex policy must be at least
327
         - pref: minComplexPasswordLength
328
           class: integer
329
         - characters long.
320
     -
330
     -
321
         - pref: RequireStrongPassword
331
         - pref: RequireStrongPassword
322
           choices:
332
           choices:
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-password.tt (-5 / +24 lines)
Lines 51-59 Link Here
51
		[% IF ( NOPERMISSION ) %]
51
		[% IF ( NOPERMISSION ) %]
52
		<li>You do not have permission to edit this patron's login information.</li>
52
		<li>You do not have permission to edit this patron's login information.</li>
53
		[% END %]
53
		[% END %]
54
		[% IF ( NOMATCH ) %]
54
		[% IF ( ERROR_password_mismatch )%]
55
		<li><strong>The passwords entered do not match</strong>. Please re-enter the new password.</li>
55
		<li id="ERROR_password_mismatch"><strong>The passwords entered do not match</strong>. Please re-enter the new password.</li>
56
		[% END %]
56
		[% END %]
57
        [% IF ( ERROR_complex_policy_mismatch ) %]
58
        <li id="ERROR_policy_mismatch"><strong>Password policy: password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</strong> Please re-enter the new password.</li>
59
        [% END %]
60
        [% IF ( ERROR_alpha_policy_mismatch ) %]
61
        <li id="ERROR_policy_mismatch"><strong>Password policy: password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</strong> Please re-enter the new password.</li>
62
        [% END %]
63
        [% IF ( ERROR_simple_policy_mismatch ) %]
64
        <li id="ERROR_policy_mismatch"><strong>Password policy: password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</strong> Please re-enter the new password.</li>
65
        [% END %]
57
		</ul>
66
		</ul>
58
		</div>
67
		</div>
59
	[% END %]
68
	[% END %]
Lines 102-110 Link Here
102
    [% INCLUDE 'str/members-menu.inc' %]
111
    [% INCLUDE 'str/members-menu.inc' %]
103
    [% Asset.js("js/members-menu.js") | $raw %]
112
    [% Asset.js("js/members-menu.js") | $raw %]
104
    <script>
113
    <script>
105
        function generate_password() {
114
        function generate_password(password_policy) {
106
            // Always generate a strong password
115
            // Follow password policy when generating password
107
            var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
116
            var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
117
            if ( password_policy == 'complex' ){
118
                chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ|[]{}!@#$%^&*()_-+?';
119
            } else if ( password_policy == 'simplenumeric'){
120
                chars = '0123456789';
121
            }
108
            var length = [% patron.category.effective_min_password_length | html %];
122
            var length = [% patron.category.effective_min_password_length | html %];
109
            if ( length < 8 ) length = 8;
123
            if ( length < 8 ) length = 8;
110
            var password='';
124
            var password='';
Lines 117-125 Link Here
117
            $("body").on('click', "#fillrandom",function(e) {
131
            $("body").on('click', "#fillrandom",function(e) {
118
                e.preventDefault();
132
                e.preventDefault();
119
                var password = '';
133
                var password = '';
134
                var password_policy = '[% password_policy | html %]';
135
136
                // Change password pattern to match password policy
120
                var pattern_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{[% patron.category.effective_min_password_length | html %],}/;
137
                var pattern_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{[% patron.category.effective_min_password_length | html %],}/;
138
                if (password_policy == 'simplenumeric') pattern_regex = /(?=.*\d).{[% patron.category.effective_min_password_length | html %],}/;
139
                if (password_policy == 'complex') pattern_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[|[]{}!@#$%^&*()_-+?]).{[% patron.category.effective_min_password_length | html %],}/;
121
                while ( ! pattern_regex.test( password ) ) {
140
                while ( ! pattern_regex.test( password ) ) {
122
                    password = generate_password();
141
                    password = generate_password(password_policy);
123
                }
142
                }
124
                $("#newpassword").val(password);
143
                $("#newpassword").val(password);
125
                $("#newpassword").attr('type', 'text');
144
                $("#newpassword").attr('type', 'text');
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt (-2 / +11 lines)
Lines 172-177 legend:hover { Link Here
172
                                    [% IF ERROR_bad_email_alternative %]
172
                                    [% IF ERROR_bad_email_alternative %]
173
                                        <li id="ERROR_bad_email_alternative">The alternative email is invalid.</li>
173
                                        <li id="ERROR_bad_email_alternative">The alternative email is invalid.</li>
174
                                    [% END %]
174
                                    [% END %]
175
                                    [% IF ( ERROR_complex_policy_mismatch ) %]
176
                                    <li id="ERROR_policy_mismatch">Password policy: password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</li>
177
                                    [% END %]
178
                                    [% IF ( ERROR_alpha_policy_mismatch ) %]
179
                                    <li id="ERROR_policy_mismatch">Password policy: password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</li>
180
                                    [% END %]
181
                                    [% IF ( ERROR_simple_policy_mismatch ) %]
182
                                    <li id="ERROR_policy_mismatch">Password policy: password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</li>
183
                                    [% END %]
175
                                </ul>
184
                                </ul>
176
                            </div>
185
                            </div>
177
                        [% END %]
186
                        [% END %]
Lines 830-838 legend:hover { Link Here
830
                                                            [% IF ( typeloo.typename_X ) %]<optgroup label="Statistical">[% END %]
839
                                                            [% IF ( typeloo.typename_X ) %]<optgroup label="Statistical">[% END %]
831
                                                        [% END %]
840
                                                        [% END %]
832
                                                        [% IF ( categoryloo.categorycodeselected ) %]
841
                                                        [% IF ( categoryloo.categorycodeselected ) %]
833
                                                            <option value="[% categoryloo.categorycode | html %]" selected="selected" data-pwd-length="[% categoryloo.effective_min_password_length | html %]" data-pwd-strong="[% categoryloo.effective_require_strong_password | html %]" data-typename="[% typeloo.typename | html %]">[% categoryloo.categoryname | html %]</option>
842
                                                            <option value="[% categoryloo.categorycode | html %]" selected="selected" data-pwd-length="[% categoryloo.effective_min_password_length | html %]" data-pwd-strong="[% categoryloo.effective_require_strong_password | html %]" data-pwd-policy="[% categoryloo.passwordpolicy | html %]" data-typename="[% typeloo.typename | html %]">[% categoryloo.categoryname | html %]</option>
834
                                                        [% ELSE %]
843
                                                        [% ELSE %]
835
                                                            <option value="[% categoryloo.categorycode | html %]" data-pwd-length="[% categoryloo.effective_min_password_length | html %]" data-pwd-strong="[% categoryloo.effective_require_strong_password | html %]" data-typename="[% typeloo.typename | html %]">[% categoryloo.categoryname | html %]</option>
844
                                                            <option value="[% categoryloo.categorycode | html %]" data-pwd-length="[% categoryloo.effective_min_password_length | html %]" data-pwd-strong="[% categoryloo.effective_require_strong_password | html %]" data-pwd-policy="[% categoryloo.passwordpolicy | html %]" data-typename="[% typeloo.typename | html %]">[% categoryloo.categoryname | html %]</option>
836
                                                        [% END %]
845
                                                        [% END %]
837
                                                        [% IF ( loop.last ) %]
846
                                                        [% IF ( loop.last ) %]
838
                                                            </optgroup>
847
                                                            </optgroup>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt (-4 / +33 lines)
Lines 90-95 Link Here
90
                                [% IF field == "password_has_whitespaces" %]
90
                                [% IF field == "password_has_whitespaces" %]
91
                                    <li>Password must not contain leading or trailing whitespaces.</li>
91
                                    <li>Password must not contain leading or trailing whitespaces.</li>
92
                                [% END %]
92
                                [% END %]
93
                                [% IF field == "complex_policy_mismatch" %]
94
                                    <li>Password policy: password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</li>
95
                                [% END %]
96
                                [% IF field == "alpha_policy_mismatch" %]
97
                                    <li>Password policy: password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</li>
98
                                [% END %]
99
                                [% IF field == "simple_policy_mismatch" %]
100
                                    <li>Password policy: password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</li>
101
                                [% END %]
93
                                [% IF field == "duplicate_email" %]
102
                                [% IF field == "duplicate_email" %]
94
                                    <li>This email address already exists in our database.</li>
103
                                    <li>This email address already exists in our database.</li>
95
                                [% END %]
104
                                [% END %]
Lines 252-260 Link Here
252
                                            <select id="borrower_categorycode" name="borrower_categorycode">
261
                                            <select id="borrower_categorycode" name="borrower_categorycode">
253
                                                [% FOREACH c IN Categories.all() %]
262
                                                [% FOREACH c IN Categories.all() %]
254
                                                    [% IF c.categorycode == Koha.Preference('PatronSelfRegistrationDefaultCategory') %]
263
                                                    [% IF c.categorycode == Koha.Preference('PatronSelfRegistrationDefaultCategory') %]
255
                                                        <option value="[% c.categorycode | html %]" data-pwd-length="[% c.effective_min_password_length | html %]" data-pwd-strong="[% c.effective_require_strong_password | html %]" selected="selected">[% c.description | html %]</option>
264
                                                        <option value="[% c.categorycode | html %]" data-pwd-length="[% c.effective_min_password_length | html %]" data-pwd-strong="[% c.effective_require_strong_password | html %]" data-pwd-policy="[% c.passwordpolicy | html %]" selected="selected">[% c.description | html %]</option>
256
                                                    [% ELSE %]
265
                                                    [% ELSE %]
257
                                                        <option value="[% c.categorycode | html %]" data-pwd-length="[% c.effective_min_password_length | html %]" data-pwd-strong="[% c.effective_require_strong_password | html %]">[% c.description | html %]</option>
266
                                                        <option value="[% c.categorycode | html %]" data-pwd-length="[% c.effective_min_password_length | html %]" data-pwd-strong="[% c.effective_require_strong_password | html %]" data-pwd-policy="[% c.passwordpolicy | html %]">[% c.description | html %]</option>
258
                                                    [% END %]
267
                                                    [% END %]
259
                                                [% END %]
268
                                                [% END %]
260
                                            </select>
269
                                            </select>
Lines 869-874 Link Here
869
                            [% IF patron %]
878
                            [% IF patron %]
870
                                [% IF ( patron.category.effective_require_strong_password ) %]
879
                                [% IF ( patron.category.effective_require_strong_password ) %]
871
                                    <p>Your password must contain at least [% patron.category.effective_min_password_length | html %] characters, including UPPERCASE, lowercase and numbers.</p>
880
                                    <p>Your password must contain at least [% patron.category.effective_min_password_length | html %] characters, including UPPERCASE, lowercase and numbers.</p>
881
                                [% ELSIF ( passwordpolicy == 'complex') %]
882
                                    <p>Your password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</p>
883
                                [% ELSIF ( passwordpolicy == 'alphanumeric') %]
884
                                    <p>Your password must contain both numbers and non-special characters and must be at least [% patron.category.effective_min_password_length | html %] characters long.</p>
885
                                [% ELSIF ( passwordpolicy == 'simplenumeric') %]
886
                                    <p>Your password can only contain digits 0-9 and must be at least [% patron.category.effective_min_password_length | html %] characters long.</p>
872
                                [% ELSE %]
887
                                [% ELSE %]
873
                                    <p>Your password must be at least [% patron.category.effective_min_password_length | html %] characters long.</p>
888
                                    <p>Your password must be at least [% patron.category.effective_min_password_length | html %] characters long.</p>
874
                                [% END %]
889
                                [% END %]
Lines 1185-1195 Link Here
1185
    [% UNLESS patron %]
1200
    [% UNLESS patron %]
1186
        var PWD_STRONG_MSG = _("Password must contain at least %s characters, including UPPERCASE, lowercase and numbers");
1201
        var PWD_STRONG_MSG = _("Password must contain at least %s characters, including UPPERCASE, lowercase and numbers");
1187
        var PWD_WEAK_MSG = _("Password must contain at least %s characters");
1202
        var PWD_WEAK_MSG = _("Password must contain at least %s characters");
1188
        $(document).ready(function() {
1203
        var PWD_COMPLEX_MSG = _("Password must contain numbers, lower and uppercase characters and special characters and must be at least %s characters long.");
1204
        var PWD_ALPHA_MSG = _("Password must contain both numbers and non-special characters and must be at least %s characters long.");
1205
        var PWD_SIMPLE_MSG = _("Password can only contain digits 0-9 and must be at least %s characters long.");           
1206
        $(document).ready(function() {    
1189
            var setPwdMessage = function() {
1207
            var setPwdMessage = function() {
1190
                var require_strong = $('select#borrower_categorycode option:selected').data('pwdStrong');
1208
                var require_strong = $('select#borrower_categorycode option:selected').data('pwdStrong');
1191
                var min_lenght = $('select#borrower_categorycode option:selected').data('pwdLength');
1209
                var min_lenght = $('select#borrower_categorycode option:selected').data('pwdLength');
1192
                $('#password_alert').html((require_strong?PWD_STRONG_MSG:PWD_WEAK_MSG).format(min_lenght));
1210
                var passwordpolicy = $('select#borrower_categorycode option:selected').data('pwdPolicy');
1211
                if(passwordpolicy){
1212
                    if(passwordpolicy == 'complex'){
1213
                        $('#password_alert').html((PWD_COMPLEX_MSG).format(min_lenght));
1214
                    }else if(passwordpolicy == 'alphanumeric'){
1215
                        $('#password_alert').html((PWD_ALPHA_MSG).format(min_lenght));
1216
                    }else{
1217
                        $('#password_alert').html((PWD_SIMPLE_MSG).format(min_lenght));
1218
                    }
1219
                }else{
1220
                    $('#password_alert').html((require_strong?PWD_STRONG_MSG:PWD_WEAK_MSG).format(min_lenght));
1221
                }
1193
            };
1222
            };
1194
            setPwdMessage();
1223
            setPwdMessage();
1195
            $('select#borrower_categorycode').change(setPwdMessage);
1224
            $('select#borrower_categorycode').change(setPwdMessage);
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-passwd.tt (-1 / +15 lines)
Lines 43-49 Link Here
43
                                [% IF password_has_whitespaces %]
43
                                [% IF password_has_whitespaces %]
44
                                    Password must not contain leading or trailing whitespaces.
44
                                    Password must not contain leading or trailing whitespaces.
45
                                [% END %]
45
                                [% END %]
46
46
                                [% IF ( complex_policy_mismatch ) %]
47
                                    Password policy: password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.
48
                                [% END %]
49
                                [% IF ( alpha_policy_mismatch ) %]
50
                                    Password policy: password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.
51
                                [% END %]
52
                                [% IF ( simple_policy_mismatch ) %]
53
                                    Password policy: password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.
54
                                [% END %]
47
                                [% IF ( WrongPass ) %]
55
                                [% IF ( WrongPass ) %]
48
                                Your current password was entered incorrectly.  If this problem persists, please ask a librarian to reset your password for you.
56
                                Your current password was entered incorrectly.  If this problem persists, please ask a librarian to reset your password for you.
49
                                [% END %]
57
                                [% END %]
Lines 59-64 Link Here
59
                                <fieldset>
67
                                <fieldset>
60
                                    [% IF ( logged_in_user.category.effective_require_strong_password ) %]
68
                                    [% IF ( logged_in_user.category.effective_require_strong_password ) %]
61
                                        <div class="alert alert-info">Your password must contain at least [% logged_in_user.category.effective_min_password_length | html %] characters, including UPPERCASE, lowercase and numbers.</div>
69
                                        <div class="alert alert-info">Your password must contain at least [% logged_in_user.category.effective_min_password_length | html %] characters, including UPPERCASE, lowercase and numbers.</div>
70
                                    [% ELSIF ( password_policy == 'complex') %]
71
                                        <div class="alert alert-info">Your password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</div>
72
                                    [% ELSIF ( password_policy == 'alphanumeric') %]
73
                                       <div class="alert alert-info">Your password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</div>
74
                                    [% ELSIF ( password_policy == 'simplenumeric') %]
75
                                        <div class="alert alert-info">Your password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</div>
62
                                    [% ELSE %]
76
                                    [% ELSE %]
63
                                        <div class="alert alert-info">Your password must be at least [% logged_in_user.category.effective_min_password_length | html %] characters long.</div>
77
                                        <div class="alert alert-info">Your password must be at least [% logged_in_user.category.effective_min_password_length | html %] characters long.</div>
64
                                    [% END %]
78
                                    [% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt (+12 lines)
Lines 80-85 Link Here
80
                    [% ELSIF (errLinkNotValid) %]
80
                    [% ELSIF (errLinkNotValid) %]
81
                        The link you clicked is either invalid, or expired.
81
                        The link you clicked is either invalid, or expired.
82
                        <br/>Be sure you used the link from the email, or contact library staff for assistance.
82
                        <br/>Be sure you used the link from the email, or contact library staff for assistance.
83
                    [% ELSIF ( complex_policy_mismatch ) %]
84
                        <li>Password policy: password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</li>
85
                    [% ELSIF ( alpha_policy_mismatch ) %]
86
                        <li>Password policy: password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</li>
87
                    [% ELSIF ( simple_policy_mismatch ) %]
88
                        <li>Password policy: password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</li>
83
                    [% END %]
89
                    [% END %]
84
                    </p>
90
                    </p>
85
                    <p>Please contact the library if you need further assistance.</p>
91
                    <p>Please contact the library if you need further assistance.</p>
Lines 109-114 Link Here
109
                        <fieldset>
115
                        <fieldset>
110
                            [% IF ( RequireStrongPassword ) %]
116
                            [% IF ( RequireStrongPassword ) %]
111
                                <div class="alert alert-info">Your password must contain at least [% minPasswordLength | html %] characters, including UPPERCASE, lowercase and numbers.</div>
117
                                <div class="alert alert-info">Your password must contain at least [% minPasswordLength | html %] characters, including UPPERCASE, lowercase and numbers.</div>
118
                            [% ELSIF ( password_policy == 'complex') %]
119
                                <div class="alert alert-info">Your password must contain numbers, lower and uppercase characters and special characters and must be at least [% minPasswordLength | html %] characters long.</div>
120
                            [% ELSIF ( password_policy == 'alphanumeric') %]
121
                                <div class="alert alert-info">Your password must contain both numbers and non-special characters and must be at least [% minPasswordLength | html %] characters long.</div>
122
                            [% ELSIF ( password_policy == 'simplenumeric') %]
123
                                <div class="alert alert-info">Your password can only contain digits 0-9 and must be at least [% minPasswordLength | html %] characters long.</div>
112
                            [% ELSE %]
124
                            [% ELSE %]
113
                                <div class="alert alert-info">Your password must be at least [% minPasswordLength | html %] characters long.</div>
125
                                <div class="alert alert-info">Your password must be at least [% minPasswordLength | html %] characters long.</div>
114
                            [% END %]
126
                            [% END %]
(-)a/members/member-password.pl (-1 / +14 lines)
Lines 52-61 output_and_exit_if_error( $input, $cookie, $template, { module => 'members', log Link Here
52
52
53
my $category_type = $patron->category->category_type;
53
my $category_type = $patron->category->category_type;
54
54
55
my $passwordpolicy = $patron->category->passwordpolicy;
56
my $minPasswordLength = $patron->category->effective_min_password_length;
57
55
if ( ( $patron_id ne $loggedinuser ) && ( $category_type eq 'S' ) ) {
58
if ( ( $patron_id ne $loggedinuser ) && ( $category_type eq 'S' ) ) {
56
    push( @errors, 'NOPERMISSION' )
59
    push( @errors, 'NOPERMISSION' )
57
      unless ( $staffflags->{'superlibrarian'} || $staffflags->{'staffaccess'} );
60
      unless ( $staffflags->{'superlibrarian'} || $staffflags->{'staffaccess'} );
58
59
    # need superlibrarian for koha-conf.xml fakeuser.
61
    # need superlibrarian for koha-conf.xml fakeuser.
60
}
62
}
61
63
Lines 94-99 if ( $newpassword and not @errors) { Link Here
94
        elsif ( $_->isa('Koha::Exceptions::Password::Plugin') ) {
96
        elsif ( $_->isa('Koha::Exceptions::Password::Plugin') ) {
95
            push @errors, 'ERROR_from_plugin';
97
            push @errors, 'ERROR_from_plugin';
96
        }
98
        }
99
        elsif ( $_->isa('Koha::Exceptions::Password::SimplePolicy') ) {
100
            push @errors, 'ERROR_simple_policy_mismatch';
101
        }
102
        elsif ( $_->isa('Koha::Exceptions::Password::AlphaPolicy') ) {
103
            push @errors, 'ERROR_alpha_policy_mismatch';
104
        }
105
        elsif ( $_->isa('Koha::Exceptions::Password::ComplexPolicy') ) {
106
            push @errors, 'ERROR_complex_policy_mismatch';
107
        }
97
        else {
108
        else {
98
            push( @errors, 'BADUSERID' );
109
            push( @errors, 'BADUSERID' );
99
        }
110
        }
Lines 104-109 $template->param( Link Here
104
    patron      => $patron,
115
    patron      => $patron,
105
    destination => $destination,
116
    destination => $destination,
106
    csrf_token  => Koha::Token->new->generate_csrf({ session_id => scalar $input->cookie('CGISESSID'), }),
117
    csrf_token  => Koha::Token->new->generate_csrf({ session_id => scalar $input->cookie('CGISESSID'), }),
118
    password_policy => $passwordpolicy,
119
    minPasswordLength => $minPasswordLength,
107
);
120
);
108
121
109
if ( scalar(@errors) ) {
122
if ( scalar(@errors) ) {
(-)a/members/memberentry.pl (+9 lines)
Lines 184-189 unless ($category_type or !($categorycode)){ Link Here
184
}
184
}
185
$category_type="A" unless $category_type; # FIXME we should display a error message instead of a 500 error !
185
$category_type="A" unless $category_type; # FIXME we should display a error message instead of a 500 error !
186
186
187
my $passwordpolicy = ( $patron ) ? $patron->category->passwordpolicy : Koha::Patron::Categories->find($categorycode)->passwordpolicy;
188
$template->param(password_policy => $passwordpolicy);
189
190
my $minPasswordLength = Koha::Patron::Categories->find($categorycode)->effective_min_password_length;
191
$template->param("minPasswordLength" => $minPasswordLength);
192
187
# if a add or modify is requested => check validity of data.
193
# if a add or modify is requested => check validity of data.
188
%data = %$borrower_data if ($borrower_data);
194
%data = %$borrower_data if ($borrower_data);
189
195
Lines 383-388 if ($op eq 'save' || $op eq 'insert'){ Link Here
383
          push @errors, 'ERROR_password_too_short' if $error eq 'too_short';
389
          push @errors, 'ERROR_password_too_short' if $error eq 'too_short';
384
          push @errors, 'ERROR_password_too_weak' if $error eq 'too_weak';
390
          push @errors, 'ERROR_password_too_weak' if $error eq 'too_weak';
385
          push @errors, 'ERROR_password_has_whitespaces' if $error eq 'has_whitespaces';
391
          push @errors, 'ERROR_password_has_whitespaces' if $error eq 'has_whitespaces';
392
          push @errors, 'ERROR_complex_policy_mismatch' if $error eq 'complex_policy_mismatch';
393
          push @errors, 'ERROR_alpha_policy_mismatch' if $error eq 'alpha_policy_mismatch';
394
          push @errors, 'ERROR_simple_policy_mismatch' if $error eq 'simple_policy_mismatch';
386
      }
395
      }
387
  }
396
  }
388
397
(-)a/opac/opac-memberentry.pl (+14 lines)
Lines 38-43 use Koha::DateUtils; Link Here
38
use Koha::Libraries;
38
use Koha::Libraries;
39
use Koha::Patron::Attribute::Types;
39
use Koha::Patron::Attribute::Types;
40
use Koha::Patron::Attributes;
40
use Koha::Patron::Attributes;
41
use Koha::Patron::Categories;
41
use Koha::Patron::Images;
42
use Koha::Patron::Images;
42
use Koha::Patron::Modification;
43
use Koha::Patron::Modification;
43
use Koha::Patron::Modifications;
44
use Koha::Patron::Modifications;
Lines 117-122 foreach my $attr (@$attributes) { Link Here
117
    }
118
    }
118
}
119
}
119
120
121
my $categorycode = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
122
my $category = Koha::Patron::Categories->find($categorycode);
123
my $passwordpolicy = $category->passwordpolicy;
124
my $minPasswordLength = $category->effective_min_password_length;
125
$template->param(
126
    password_policy => $passwordpolicy,
127
    minPasswordLength => $minPasswordLength
128
);
129
120
if ( $action eq 'create' ) {
130
if ( $action eq 'create' ) {
121
131
122
    my %borrower = ParseCgiForBorrower($cgi);
132
    my %borrower = ParseCgiForBorrower($cgi);
Lines 125-130 if ( $action eq 'create' ) { Link Here
125
135
126
    my @empty_mandatory_fields = (CheckMandatoryFields( \%borrower, $action ), CheckMandatoryAttributes( \%borrower, $attributes ) );
136
    my @empty_mandatory_fields = (CheckMandatoryFields( \%borrower, $action ), CheckMandatoryAttributes( \%borrower, $attributes ) );
127
    my $invalidformfields = CheckForInvalidFields(\%borrower);
137
    my $invalidformfields = CheckForInvalidFields(\%borrower);
138
128
    delete $borrower{'password2'};
139
    delete $borrower{'password2'};
129
    my $cardnumber_error_code;
140
    my $cardnumber_error_code;
130
    if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
141
    if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
Lines 472-477 sub CheckForInvalidFields { Link Here
472
              push @invalidFields, 'password_too_short' if $error eq 'too_short';
483
              push @invalidFields, 'password_too_short' if $error eq 'too_short';
473
              push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
484
              push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
474
              push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
485
              push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
486
              push @invalidFields, 'complex_policy_mismatch' if $error eq 'complex_policy_mismatch';
487
              push @invalidFields, 'alpha_policy_mismatch' if $error eq 'alpha_policy_mismatch';
488
              push @invalidFields, 'simple_policy_mismatch' if $error eq 'simple_policy_mismatch';
475
          }
489
          }
476
    }
490
    }
477
491
(-)a/opac/opac-passwd.pl (+13 lines)
Lines 27-32 use C4::Context; Link Here
27
use C4::Circulation;
27
use C4::Circulation;
28
use C4::Members;
28
use C4::Members;
29
use C4::Output;
29
use C4::Output;
30
use Koha::AuthUtils;
30
use Koha::Patrons;
31
use Koha::Patrons;
31
32
32
use Try::Tiny;
33
use Try::Tiny;
Lines 44-49 my ( $template, $borrowernumber, $cookie ) = get_template_and_user( Link Here
44
);
45
);
45
46
46
my $patron = Koha::Patrons->find( $borrowernumber );
47
my $patron = Koha::Patrons->find( $borrowernumber );
48
my $categorycode = $patron->category->categorycode;
49
my $passwordpolicy = $patron->category->passwordpolicy;
50
my $minPasswordLength = $patron->category->effective_min_password_length;
47
if ( $patron->category->effective_change_password ) {
51
if ( $patron->category->effective_change_password ) {
48
    if (   $query->param('Oldkey')
52
    if (   $query->param('Oldkey')
49
        && $query->param('Newkey')
53
        && $query->param('Newkey')
Lines 60-65 if ( $patron->category->effective_change_password ) { Link Here
60
                $template->param( 'passwords_mismatch'   => '1' );
64
                $template->param( 'passwords_mismatch'   => '1' );
61
            } else {
65
            } else {
62
                try {
66
                try {
67
                    Koha::AuthUtils::is_password_valid( $new_password, $categorycode );
63
                    $patron->set_password({ password => $new_password });
68
                    $patron->set_password({ password => $new_password });
64
                    $template->param( 'password_updated' => '1' );
69
                    $template->param( 'password_updated' => '1' );
65
                    $template->param( 'borrowernumber'   => $borrowernumber );
70
                    $template->param( 'borrowernumber'   => $borrowernumber );
Lines 71-76 if ( $patron->category->effective_change_password ) { Link Here
71
                        if $_->isa('Koha::Exceptions::Password::TooWeak');
76
                        if $_->isa('Koha::Exceptions::Password::TooWeak');
72
                    $error = 'password_has_whitespaces'
77
                    $error = 'password_has_whitespaces'
73
                        if $_->isa('Koha::Exceptions::Password::WhitespaceCharacters');
78
                        if $_->isa('Koha::Exceptions::Password::WhitespaceCharacters');
79
                    $error = 'simple_policy_mismatch'
80
                        if $_->isa('Koha::Exceptions::Password::SimplePolicy');
81
                    $error = 'alpha_policy_mismatch'
82
                        if $_->isa('Koha::Exceptions::Password::AlphaPolicy');
83
                    $error = 'complex_policy_mismatch'
84
                        if $_->isa('Koha::Exceptions::Password::ComplexPolicy');
74
                };
85
                };
75
            }
86
            }
76
        }
87
        }
Lines 106-111 $template->param( Link Here
106
    firstname  => $patron->firstname,
117
    firstname  => $patron->firstname,
107
    surname    => $patron->surname,
118
    surname    => $patron->surname,
108
    passwdview => 1,
119
    passwdview => 1,
120
    password_policy => $passwordpolicy,
121
    minPasswordLength => $minPasswordLength,
109
);
122
);
110
123
111
124
(-)a/opac/opac-password-recovery.pl (-1 / +21 lines)
Lines 7-12 use C4::Auth; Link Here
7
use C4::Koha;
7
use C4::Koha;
8
use C4::Output;
8
use C4::Output;
9
use C4::Context;
9
use C4::Context;
10
use Koha::AuthUtils;
10
use Koha::Patron::Password::Recovery
11
use Koha::Patron::Password::Recovery
11
  qw(SendPasswordRecoveryEmail ValidateBorrowernumber GetValidLinkInfo CompletePasswordRecovery DeleteExpiredPasswordRecovery);
12
  qw(SendPasswordRecoveryEmail ValidateBorrowernumber GetValidLinkInfo CompletePasswordRecovery DeleteExpiredPasswordRecovery);
12
use Koha::Patrons;
13
use Koha::Patrons;
Lines 153-158 if ( $query->param('sendEmail') || $query->param('resendEmail') ) { Link Here
153
elsif ( $query->param('passwordReset') ) {
154
elsif ( $query->param('passwordReset') ) {
154
    ( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey);
155
    ( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey);
155
156
157
    my $patron = Koha::Patrons->find($borrower_number);
158
    my $passwordpolicy = $patron->category->passwordpolicy;
159
    my $minPasswordLength = $patron->category->effective_min_password_length;
160
161
    $template->param(
162
        password_policy => $passwordpolicy,
163
        minPasswordLength => $minPasswordLength,
164
    );
165
156
    my $error;
166
    my $error;
157
    my $min_password_length = C4::Context->preference('minPasswordPreference');
167
    my $min_password_length = C4::Context->preference('minPasswordPreference');
158
    my $require_strong_password = C4::Context->preference('RequireStrongPassword');
168
    my $require_strong_password = C4::Context->preference('RequireStrongPassword');
Lines 183-188 elsif ( $query->param('passwordReset') ) { Link Here
183
            elsif ( $_->isa('Koha::Exceptions::Password::TooWeak') ) {
193
            elsif ( $_->isa('Koha::Exceptions::Password::TooWeak') ) {
184
                $error = 'password_too_weak';
194
                $error = 'password_too_weak';
185
            }
195
            }
196
            elsif ( $_->isa('Koha::Exceptions::Password::SimplePolicy') ) {
197
                $error = 'simple_policy_mismatch';
198
            }
199
            elsif ( $_->isa('Koha::Exceptions::Password::AlphaPolicy') ) {
200
                $error = 'alpha_policy_mismatch';
201
            }
202
            elsif ( $_->isa('Koha::Exceptions::Password::ComplexPolicy') ) {
203
                $error = 'complex_policy_mismatch';
204
            }
186
        };
205
        };
187
    }
206
    }
188
    if ( $error ) {
207
    if ( $error ) {
Lines 215-221 elsif ($uniqueKey) { #reset password form Link Here
215
        errLinkNotValid => $errLinkNotValid,
234
        errLinkNotValid => $errLinkNotValid,
216
        hasError        => ( $errLinkNotValid ? 1 : 0 ),
235
        hasError        => ( $errLinkNotValid ? 1 : 0 ),
217
        minPasswordLength => $borrower->category->effective_min_password_length,
236
        minPasswordLength => $borrower->category->effective_min_password_length,
218
        RequireStrongPassword => $borrower->category->effective_require_strong_password
237
        RequireStrongPassword => $borrower->category->effective_require_strong_password,
238
        password_policy => $borrower->category->passwordpolicy,
219
    );
239
    );
220
}
240
}
221
else {    #password recovery form (to send email)
241
else {    #password recovery form (to send email)
(-)a/t/db_dependent/AuthUtils.t (-6 / +89 lines)
Lines 32-57 $schema->storage->txn_begin; Link Here
32
my $category1 = $builder->build_object(
32
my $category1 = $builder->build_object(
33
    {
33
    {
34
        class => 'Koha::Patron::Categories',
34
        class => 'Koha::Patron::Categories',
35
        value => { min_password_length => 15, require_strong_password => 1 }
35
        value => { min_password_length => 15, require_strong_password => 1, passwordpolicy => '' }
36
    }
36
    }
37
);
37
);
38
my $category2 = $builder->build_object(
38
my $category2 = $builder->build_object(
39
    {
39
    {
40
        class => 'Koha::Patron::Categories',
40
        class => 'Koha::Patron::Categories',
41
        value => { min_password_length => 5, require_strong_password => undef }
41
        value => { min_password_length => 5, require_strong_password => undef, passwordpolicy => '' }
42
    }
42
    }
43
);
43
);
44
my $category3 = $builder->build_object(
44
my $category3 = $builder->build_object(
45
    {
45
    {
46
        class => 'Koha::Patron::Categories',
46
        class => 'Koha::Patron::Categories',
47
        value => { min_password_length => undef, require_strong_password => 1 }
47
        value => { min_password_length => undef, require_strong_password => 1, passwordpolicy => '' }
48
    }
48
    }
49
);
49
);
50
my $category4 = $builder->build_object(
50
my $category4 = $builder->build_object(
51
    {
51
    {
52
        class => 'Koha::Patron::Categories',
52
        class => 'Koha::Patron::Categories',
53
        value =>
53
        value =>
54
          { min_password_length => undef, require_strong_password => undef }
54
          { min_password_length => undef, require_strong_password => undef, passwordpolicy => '' }
55
    }
55
    }
56
);
56
);
57
57
Lines 63-70 my $p_15l_weak = '0123456789abcdf'; Link Here
63
my $p_5l_strong  = 'Abc12';
63
my $p_5l_strong  = 'Abc12';
64
my $p_15l_strong = '0123456789AbCdF';
64
my $p_15l_strong = '0123456789AbCdF';
65
65
66
# Password policy simplenumeric
67
my $category_simple = $builder->build_object({
68
    class => 'Koha::Patron::Categories',
69
    value  => { min_password_length => 4, passwordpolicy => 'simplenumeric' },
70
});
71
72
# Password policy alphanumeric
73
my $category_alpha = $builder->build_object({
74
    class => 'Koha::Patron::Categories',
75
    value  => { min_password_length => 5, passwordpolicy => 'alphanumeric' },
76
});
77
78
# Password policy complex
79
my $category_complex = $builder->build_object({
80
    class => 'Koha::Patron::Categories',
81
    value  => { min_password_length => 6, passwordpolicy => 'complex' },
82
});
83
66
subtest 'is_password_valid for category' => sub {
84
subtest 'is_password_valid for category' => sub {
67
    plan tests => 15;
85
    plan tests => 16;
68
86
69
    my ( $is_valid, $error );
87
    my ( $is_valid, $error );
70
88
Lines 121-130 subtest 'is_password_valid for category' => sub { Link Here
121
    'Koha::Exceptions::Password::NoCategoryProvided',
139
    'Koha::Exceptions::Password::NoCategoryProvided',
122
      'Category should always be provided';
140
      'Category should always be provided';
123
141
142
    subtest 'password policies' => sub {
143
144
        t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
145
        t::lib::Mocks::mock_preference('minPasswordLength', 4);
146
147
        #test simplenumeric password policy
148
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '1234', $category_simple );
149
        is ( $is_valid, 1, 'simplenumeric password should contain only numbers' );
150
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'A123', $category_simple );
151
        is ( $is_valid, 0, 'simplenumeric password should not contain alphabets' );
152
        is($error, 'simple_policy_mismatch', 'error "simple_policy_mismatch" raised');
153
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '!234', $category_simple );
154
        is ( $is_valid, 0, 'simplenumeric password should not contain non-special characters' );
155
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '123', $category_simple );
156
        is ( $is_valid, 0, 'simplenumeric password follows "min_password_length" value' );
157
158
        #test alphanumeric
159
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'A1234', $category_alpha );
160
        is ( $is_valid, 1, 'alphanumeric password should contain both numbers and non-special characters' );
161
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '12345', $category_alpha );
162
        is ( $is_valid, 0, 'alphanumeric password must contain at least one uppercase character' );
163
        is($error, 'alpha_policy_mismatch', 'error "alpha_policy_mismatch" raised');
164
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'A123', $category_alpha );
165
        is ( $is_valid, 0, 'alphanumeric password follows "min_password_length" value');
166
167
        #test complex
168
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'As!123', $category_complex );
169
        is ( $is_valid, 1, 'complex password should contain numbers, lower and uppercase characters and special characters' );
170
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'A12345', $category_complex );
171
        is ( $is_valid, 0, 'complex password must contain numbers, lower and uppercase characters and special characters' );
172
        is($error, 'complex_policy_mismatch', 'error "complex_policy_mismatch" raised');
173
        ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'As!12', $category_complex );
174
        is ( $is_valid, 0, 'complex password follows "min_password_length" value' );
175
    }
124
};
176
};
125
177
126
subtest 'generate_password for category' => sub {
178
subtest 'generate_password for category' => sub {
127
    plan tests => 5;
179
    plan tests => 6;
128
180
129
    my ( $is_valid, $error );
181
    my ( $is_valid, $error );
130
182
Lines 159-164 subtest 'generate_password for category' => sub { Link Here
159
    'Koha::Exceptions::Password::NoCategoryProvided',
211
    'Koha::Exceptions::Password::NoCategoryProvided',
160
      'Category should always be provided';
212
      'Category should always be provided';
161
213
214
    subtest 'generate_password with password policies' => sub {
215
216
        t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
217
        t::lib::Mocks::mock_preference('minPasswordLength', 4);
218
219
        my $all_valid = 1;
220
        
221
        #simplenumeric
222
        for ( 1 .. 10 ) {
223
            my $password = Koha::AuthUtils::generate_password( $category_simple );;
224
            my ( $is_valid, undef ) = Koha::AuthUtils::is_password_valid( $password, $category_simple );
225
            $all_valid = 0 unless $is_valid;
226
        }
227
        is ( $all_valid, 1, 'generate_password should generate valid passwords with simplenumeric policy' );
228
229
        #alphanumeric
230
        for ( 1 .. 10 ) {
231
            my $password = Koha::AuthUtils::generate_password( $category_alpha );
232
            my ( $is_valid, undef ) = Koha::AuthUtils::is_password_valid( $password, $category_alpha );
233
            $all_valid = 0 unless $is_valid;
234
        }
235
        is ( $all_valid, 1, 'generate_password should generate valid passwords with alphanumeric policy' );
236
237
        #complex
238
        for ( 1 .. 10 ) {
239
            my $password = Koha::AuthUtils::generate_password( $category_complex );
240
            my ( $is_valid, undef ) = Koha::AuthUtils::is_password_valid( $password, $category_complex );
241
            $all_valid = 0 unless $is_valid;
242
        }
243
        is ( $all_valid, 1, 'generate_password should generate valid passwords with complex policy' );
244
    }
162
};
245
};
163
246
164
$schema->storage->txn_rollback;
247
$schema->storage->txn_rollback;
(-)a/t/db_dependent/api/v1/patrons_password.t (-2 / +80 lines)
Lines 39-49 my $t = Test::Mojo->new('Koha::REST::V1'); Link Here
39
39
40
subtest 'set() (authorized user tests)' => sub {
40
subtest 'set() (authorized user tests)' => sub {
41
41
42
    plan tests => 21;
42
    plan tests => 22;
43
43
44
    $schema->storage->txn_begin;
44
    $schema->storage->txn_begin;
45
45
46
    my ( $patron, $session ) = create_user_and_session({ authorized => 1 });
46
    my ( $patron, $session ) = create_user_and_session({ authorized => 1 });
47
    $patron->category->update({ passwordpolicy => ''});
47
48
48
    t::lib::Mocks::mock_preference( 'minPasswordLength',     3 );
49
    t::lib::Mocks::mock_preference( 'minPasswordLength',     3 );
49
    t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
50
    t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
Lines 118-123 subtest 'set() (authorized user tests)' => sub { Link Here
118
    $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
119
    $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
119
    $t->request_ok($tx)->status_is(200)->json_is('');
120
    $t->request_ok($tx)->status_is(200)->json_is('');
120
121
122
    subtest 'password policies' => sub {
123
124
      plan tests => 18;
125
126
      t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0);
127
      t::lib::Mocks::mock_preference( 'minPasswordLength', 4 );
128
129
      # simple policy
130
      $patron->category->update({ passwordpolicy => 'simple'});
131
      $new_password = '1234';
132
      $tx
133
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
134
              . $patron->id
135
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
136
137
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
138
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
139
      $t->request_ok($tx)->status_is(200)->json_is('');
140
141
      $new_password = '123A';
142
      $tx
143
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
144
              . $patron->id
145
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
146
147
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
148
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
149
      $t->request_ok($tx)->status_is(400)->json_is({ error => '[Password does not match simplenumeric passwordpolicy]' });
150
151
      # alphanumeric policy
152
      $patron->category->update({ passwordpolicy => 'alphanumeric'});
153
      $new_password = '123A5';
154
      $tx
155
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
156
              . $patron->id
157
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
158
159
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
160
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
161
      $t->request_ok($tx)->status_is(200)->json_is('');
162
163
      $new_password = '12345';
164
      $tx
165
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
166
              . $patron->id
167
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
168
169
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
170
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
171
      $t->request_ok($tx)->status_is(400)->json_is({ error => '[Password does not match alphanumeric passwordpolicy]' });
172
173
      # complex policy
174
      $patron->category->update({ passwordpolicy => 'complex'});
175
      $new_password = 'As!123';
176
      $tx
177
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
178
              . $patron->id
179
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
180
181
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
182
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
183
      $t->request_ok($tx)->status_is(200)->json_is('');
184
185
      $new_password = '123A5';
186
      $tx
187
            = $t->ua->build_tx( POST => "/api/v1/patrons/"
188
              . $patron->id
189
              . "/password" => json => { password => $new_password, password_2 => $new_password } );
190
191
      $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
192
      $tx->req->env( { REMOTE_ADDR => '127.0.0.1' } );
193
      $t->request_ok($tx)->status_is(400)->json_is({ error => '[Password does not match complex passwordpolicy]' });
194
195
196
    };
197
121
    $schema->storage->txn_rollback;
198
    $schema->storage->txn_rollback;
122
};
199
};
123
200
Lines 128-133 subtest 'set_public() (unprivileged user tests)' => sub { Link Here
128
    $schema->storage->txn_begin;
205
    $schema->storage->txn_begin;
129
206
130
    my ( $patron, $session ) = create_user_and_session({ authorized => 0 });
207
    my ( $patron, $session ) = create_user_and_session({ authorized => 0 });
208
    $patron->category->update({ passwordpolicy => ''});
209
131
    my $other_patron = $builder->build_object({ class => 'Koha::Patrons' });
210
    my $other_patron = $builder->build_object({ class => 'Koha::Patrons' });
132
211
133
    # Enable the public API
212
    # Enable the public API
134
- 

Return to bug 12617