summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla.pm20
-rw-r--r--Bugzilla/DB/Schema.pm4
-rw-r--r--Bugzilla/User.pm48
-rwxr-xr-xeditusers.cgi6
-rwxr-xr-xreset_password.cgi72
-rw-r--r--skins/standard/admin.css4
-rw-r--r--template/en/default/account/reset-password.html.tmpl210
-rw-r--r--template/en/default/admin/users/edit.html.tmpl12
-rw-r--r--template/en/default/admin/users/userdata.html.tmpl37
-rw-r--r--template/en/default/global/messages.html.tmpl4
-rw-r--r--template/en/default/global/user-error.html.tmpl4
-rwxr-xr-xtoken.cgi25
12 files changed, 401 insertions, 45 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 8a0ff2fd7..fa95128d1 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -367,8 +367,26 @@ sub login {
}
my $authenticated_user = $authorizer->login($type);
-
+
# At this point, we now know if a real person is logged in.
+
+ # Check if a password reset is required
+ if ($authenticated_user->password_change_required) {
+ # We cannot show the password reset UI for API calls, so treat those as
+ # a disabled account.
+ if (i_am_webservice()) {
+ ThrowUserError("account_disabled", { disabled_reason => $authenticated_user->password_change_reason });
+ }
+
+ # only allow the reset-password and token pages to handle requests
+ # (tokens handles the 'forgot password' process)
+ # otherwise redirect user to the reset-password page.
+ if ($ENV{SCRIPT_NAME} !~ m#/(?:reset_password|token)\.cgi$#) {
+ print Bugzilla->cgi->redirect('reset_password.cgi');
+ exit;
+ }
+ }
+
# We must now check to see if an sudo session is in progress.
# For a session to be in progress, the following must be true:
# 1: There must be a logged in user
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index a9dfa6d8e..abe52be90 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -947,8 +947,8 @@ use constant ABSTRACT_SCHEMA => {
mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
extern_id => {TYPE => 'varchar(64)'},
- is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1,
- DEFAULT => 'TRUE'},
+ is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'TRUE'},
last_seen_date => {TYPE => 'DATETIME'},
password_change_required => { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' },
password_change_reason => { TYPE => 'varchar(64)' },
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 9bfb4046f..4a0c2166d 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -106,6 +106,8 @@ sub DB_COLUMNS {
'profiles.extern_id',
'profiles.is_enabled',
$dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
+ 'profiles.password_change_required',
+ 'profiles.password_change_reason',
),
}
@@ -114,13 +116,15 @@ use constant ID_FIELD => 'userid';
use constant LIST_ORDER => NAME_FIELD;
use constant VALIDATORS => {
- cryptpassword => \&_check_password,
- disable_mail => \&_check_disable_mail,
- disabledtext => \&_check_disabledtext,
- login_name => \&check_login_name_for_creation,
- realname => \&_check_realname,
- extern_id => \&_check_extern_id,
- is_enabled => \&_check_is_enabled,
+ cryptpassword => \&_check_password,
+ disable_mail => \&_check_disable_mail,
+ disabledtext => \&_check_disabledtext,
+ login_name => \&check_login_name_for_creation,
+ realname => \&_check_realname,
+ extern_id => \&_check_extern_id,
+ is_enabled => \&_check_is_enabled,
+ password_change_required => \&Bugzilla::Object::check_boolean,
+ password_change_reason => \&_check_password_change_reason,
};
sub UPDATE_COLUMNS {
@@ -132,13 +136,16 @@ sub UPDATE_COLUMNS {
realname
extern_id
is_enabled
+ password_change_required
+ password_change_reason
);
push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
return @cols;
};
use constant VALIDATOR_DEPENDENCIES => {
- is_enabled => ['disabledtext'],
+ is_enabled => [ 'disabledtext' ],
+ password_change_reason => [ 'password_change_required' ],
};
use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
@@ -343,13 +350,22 @@ sub _check_is_enabled {
return $disabledtext ? 0 : 1;
}
+sub _check_password_change_reason {
+ my ($self, $value) = @_;
+ return $self->password_change_required
+ ? trim($_[1]) || ''
+ : '';
+}
+
################################################################################
# Mutators
################################################################################
-sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
-sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
-sub set_extern_id { $_[0]->set('extern_id', $_[1]); }
+sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
+sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
+sub set_extern_id { $_[0]->set('extern_id', $_[1]); }
+sub set_password_change_required { $_[0]->set('password_change_required', $_[1]); }
+sub set_password_change_reason { $_[0]->set('password_change_reason', $_[1]); }
sub set_login {
my ($self, $login) = @_;
@@ -364,7 +380,12 @@ sub set_name {
delete $self->{identity};
}
-sub set_password { $_[0]->set('cryptpassword', $_[1]); }
+sub set_password {
+ my ($self, $password) = @_;
+ $self->set('cryptpassword', $password);
+ $self->set('password_change_required', 0);
+ $self->set('password_change_reason', '');
+}
sub set_disabledtext {
my ($self, $text) = @_;
@@ -514,6 +535,9 @@ sub showmybugslink { $_[0]->{showmybugslink}; }
sub email_disabled { $_[0]->{disable_mail} || !$_[0]->{is_enabled}; }
sub email_enabled { !$_[0]->email_disabled; }
sub last_seen_date { $_[0]->{last_seen_date}; }
+sub password_change_required { $_[0]->{password_change_required}; }
+sub password_change_reason { $_[0]->{password_change_reason}; }
+
sub cryptpassword {
my $self = shift;
# We don't store it because we never want it in the object (we
diff --git a/editusers.cgi b/editusers.cgi
index a55fd04a7..bb23279ff 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -264,6 +264,12 @@ if ($action eq 'search') {
$otherUser->set_disable_mail($cgi->param('disable_mail'));
$otherUser->set_extern_id($cgi->param('extern_id'))
if defined($cgi->param('extern_id'));
+ $otherUser->set_password_change_required($cgi->param('password_change_required'));
+ $otherUser->set_password_change_reason(
+ $otherUser->password_change_required
+ ? $cgi->param('password_change_reason')
+ : ''
+ );
$changes = $otherUser->update();
}
diff --git a/reset_password.cgi b/reset_password.cgi
new file mode 100755
index 000000000..f784afb81
--- /dev/null
+++ b/reset_password.cgi
@@ -0,0 +1,72 @@
+#!/usr/bin/perl -wT
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Token;
+use Bugzilla::User qw( validate_password );
+use Bugzilla::Util qw( bz_crypt );
+
+my $cgi = Bugzilla->cgi;
+my $user = Bugzilla->login(LOGIN_REQUIRED);
+my $template = Bugzilla->template;
+my $dbh = Bugzilla->dbh;
+
+if ($cgi->param('do_save')) {
+ my $token = $cgi->param('token');
+ check_token_data($token, 'reset_password');
+
+ my $old_password = $cgi->param('old_password') // '';
+ my $password_1 = $cgi->param('new_password1') // '';
+ my $password_2 = $cgi->param('new_password2') // '';
+
+ # make sure passwords never show up in the UI
+ foreach my $field (qw( old_password new_password1 new_password2 )) {
+ $cgi->delete($field);
+ }
+
+ # validation
+ my $old_crypt_password = $user->cryptpassword;
+ if (bz_crypt($old_password, $old_crypt_password) ne $old_crypt_password) {
+ ThrowUserError('old_password_incorrect');
+ }
+ if ($password_1 eq '' || $password_2 eq '') {
+ ThrowUserError('new_password_missing');
+ }
+ if ($old_password eq $password_1) {
+ ThrowUserError('new_password_same');
+ }
+ validate_password($password_1, $password_2);
+
+ # update
+ $dbh->bz_start_transaction;
+ $user->set_password($password_1);
+ $user->update({ keep_session => 1, keep_tokens => 1 });
+ Bugzilla->logout(LOGOUT_KEEP_CURRENT);
+ delete_token($token);
+ $dbh->bz_commit_transaction;
+
+ # done
+ print $cgi->header();
+ $template->process('index.html.tmpl', { message => 'password_changed' })
+ || ThrowTemplateError($template->error());
+}
+
+else {
+ my $token = issue_session_token('reset_password');
+
+ print $cgi->header();
+ $template->process('account/reset-password.html.tmpl', { token => $token })
+ || ThrowTemplateError($template->error());
+}
diff --git a/skins/standard/admin.css b/skins/standard/admin.css
index 7782086a4..a8fabb645 100644
--- a/skins/standard/admin.css
+++ b/skins/standard/admin.css
@@ -179,6 +179,10 @@ th.title {
cursor: default;
}
+input[disabled] {
+ background-color: transparent !important;
+}
+
/* User Preferences Page */
#prefmain {
diff --git a/template/en/default/account/reset-password.html.tmpl b/template/en/default/account/reset-password.html.tmpl
new file mode 100644
index 000000000..2b1d297dc
--- /dev/null
+++ b/template/en/default/account/reset-password.html.tmpl
@@ -0,0 +1,210 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% inline_style = BLOCK %]
+
+.field-hr, .field-row {
+ clear: both;
+}
+
+.field-row {
+ height: 2.5em;
+}
+
+.field-name {
+ text-align: right;
+ width: 150px;
+ float: left;
+ padding-top: 2px;
+ font-weight: bold;
+}
+
+.field-value {
+ margin-left: 160px;
+}
+
+#errors, #complexity_rules {
+ margin-left: 160px;
+ max-width: 500px;
+}
+
+#errors {
+ color: #dd4848;
+ position: absolute;
+ margin-left: 500px;
+}
+
+#errors ul {
+ padding: 0;
+ margin: 0;
+}
+
+#complexity_rules {
+ margin-bottom: 50px;
+}
+
+[% END %]
+
+[% inline_js = BLOCK %]
+$(function() {
+
+ $('#old_password, #new_password1, #new_password2')
+ .keyup(function() {
+ var errors = [];
+ var old = $('#old_password').val();
+ var new1 = $('#new_password1').val();
+ var new2 = $('#new_password2').val();
+
+ if (old === '') {
+ errors.push('Missing current password');
+ }
+ if (new1 === '' || new2 === '') {
+ errors.push('Missing new password');
+ }
+ else if (new1 !== new2) {
+ errors.push('New passwords do not match');
+ }
+ else if (new1 === old) {
+ errors.push('Your new password must be different from your old password');
+ }
+ else if (new1.length < [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %]) {
+ errors.push('Your password must be at least [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] long');
+ }
+ else {
+ var complexity_fn;
+ [% SWITCH Param('password_complexity') %]
+ [% CASE 'no_constraints' %]
+ complexity_fn = function() {};
+ [% CASE 'mixed_letters' %]
+ complexity_fn = function(pass, errors) {
+ if (
+ pass.search(/[a-z]/) == -1 ||
+ pass.search(/[A-Z]/) == -1
+ ) {
+ errors.push('New password is not complex enough');
+ }
+ };
+ [% CASE 'letters_numbers' %]
+ complexity_fn = function(pass, errors) {
+ if (
+ pass.search(/[a-z]/) == -1 ||
+ pass.search(/[A-Z]/) == -1 ||
+ pass.search(/[0-9]/) == -1
+ ) {
+ errors.push('New password is not complex enough');
+ }
+ };
+ [% CASE 'letters_numbers_specialchars' %]
+ complexity_fn = function(pass, errors) {
+ if (
+ pass.search(/[a-z]/) == -1 ||
+ pass.search(/[A-Z]/) == -1 ||
+ pass.search(/[0-9]/) == -1 ||
+ pass.search(/\W/) == -1
+ ) {
+ errors.push('New password is not complex enough');
+ }
+ };
+ [% END %]
+ complexity_fn(new1, errors);
+ }
+
+ $('#submit').attr('disabled', errors.length > 0);
+ if ((old !== '' || new1 !== '' || new2 !== '') && errors.length) {
+ $('#errors').html('<ul><li>' + errors.join('</li><li>') + '</li></ul>');
+ }
+ else {
+ $('#errors').html('');
+ }
+ })
+ .keyup();
+
+ $('#forgot_password')
+ .click(function(event) {
+ event.preventDefault();
+ $('#forgot-form').submit();
+ });
+});
+
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Password change required"
+ style = inline_style
+ javascript = inline_js
+%]
+
+<h1>Password Reset</h1>
+
+<p>
+ [% user.password_change_reason || "You are required to update your password." FILTER html %]
+</p>
+
+<form method="POST" action="reset_password.cgi">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+<input type="hidden" name="do_save" value="1">
+
+<div id="password-reset">
+ <div class="field-hr">&nbsp;</div>
+ <div class="field-row">
+ <div class="field-name">Email</div>
+ <div class="field-value">
+ [% user.login FILTER html %]
+ </div>
+ </div>
+ <div class="field-row">
+ <div class="field-name">Current Password</div>
+ <div class="field-value">
+ <input type="password" name="old_password" id="old_password" size="30">
+ </div>
+ </div>
+ <div class="field-hr">&nbsp;</div>
+ <div id="errors"></div>
+ <div class="field-row">
+ <div class="field-name">New Password</div>
+ <div class="field-value">
+ <input type="password" name="new_password1" id="new_password1" size="30">
+ </div>
+ </div>
+ <div class="field-row">
+ <div class="field-name">New Password</div>
+ <div class="field-value">
+ <input type="password" name="new_password2" id="new_password2" size="30">
+ (again)
+ </div>
+ </div>
+ <div class="field-hr">&nbsp;</div>
+ <div class="field-row">
+ <div class="field-value">
+ <input type="submit" id="submit" value="Update Password">
+ <a id="forgot_password" href="#">Forgot Password</a>
+ </div>
+ </div>
+</div>
+
+</form>
+
+<p id="complexity_rules">
+ Your password must be a minimum of [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] characters long
+ [% SWITCH Param('password_complexity') %]
+ [% CASE 'mixed_letters' %]
+ and must contain at least one UPPER and one lowercase letter
+ [% CASE 'letters_numbers' %]
+ and must contain at least one UPPER and one lowercase letter and a number
+ [% CASE 'letters_numbers_specialchars' %]
+ and must contain at least one letter, a number and a special character
+ [% END ~%].
+</p>
+
+<form action="token.cgi" method="post" id="forgot-form">
+ <input type="hidden" name="loginname" value="[% user.login FILTER html %]">
+ <input type="hidden" name="a" value="reqpw">
+ <input type="hidden" name="token" value="[% issue_hash_token(['reqpw']) FILTER html %]">
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/admin/users/edit.html.tmpl b/template/en/default/admin/users/edit.html.tmpl
index a5ab5cf03..fbf283574 100644
--- a/template/en/default/admin/users/edit.html.tmpl
+++ b/template/en/default/admin/users/edit.html.tmpl
@@ -38,6 +38,18 @@ $(function() {
}
})
.keyup();
+
+ $('#password_change_required')
+ .change(function() {
+ $('#password').prop('disabled', $(this).is(':checked'));
+ if ($(this).is(':checked')) {
+ $('#password_change_reason_container').show();
+ }
+ else {
+ $('#password_change_reason_container').hide();
+ }
+ })
+ .change();
});
[% END %]
diff --git a/template/en/default/admin/users/userdata.html.tmpl b/template/en/default/admin/users/userdata.html.tmpl
index 63f69cfe9..ebe7451e4 100644
--- a/template/en/default/admin/users/userdata.html.tmpl
+++ b/template/en/default/admin/users/userdata.html.tmpl
@@ -64,24 +64,37 @@
</td>
</tr>
-[%# XXX This condition (can_change_password) will cause a problem
- # if we ever have a login system that can create accounts through
- # createaccount.cgi but can't change passwords.
- #%]
-
[% IF editusers %]
- [% IF user.authorizer.can_change_password %]
<tr>
- <th><label for="password">Password:</label></th>
+ <th>
+ <label for="password">
+ [% IF editform %]
+ [% otheruser.cryptpassword == '*' ? "Set" : "Update" =%]
+ [% END %]
+ Password:
+ </label>
+ </th>
<td>
- <input type="password" size="16" name="password" id="password"
- value="" autocomplete="off">
- [% IF editform %]<br>
- (Enter new password to change.)
+ <input type="password" size="16" name="password" id="password" value="" autocomplete="off">
+ [%# if a user's cryptpassword is '*' it means they use an auth provider
+ # such as github, or you can't log in with that account. in either case
+ # forcing a password reset isn't valid %]
+ [% IF otheruser.cryptpassword != '*' && editform %]
+ <br>
+ <input type="checkbox" name="password_change_required" id="password_change_required" maxlength="64"
+ [% " checked" IF otheruser.password_change_required %]>
+ <label for="password_change_required">User must change their password immediately</label>
+ <div id="password_change_reason_container">
+ Password change reason (will be displayed to the user):<br>
+ <input type="text" size="64"
+ name="password_change_reason" id="password_change_reason"
+ value="[% otheruser.password_change_reason || "You are required to change your password." FILTER html %]"
+ >
+ </div>
[% END %]
</td>
</tr>
- [% END %]
+
<tr>
<th><label for="disable_mail">[% terms.Bug %]mail Disabled:</label></th>
<td>
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index 0f408842f..4cefe2a3f 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -70,6 +70,10 @@
[% ELSE %]
[% terms.Bug %]mail has been enabled.
[% END %]
+ [% ELSIF field == 'password_change_required' %]
+ The user [% otheruser.password_change_required ? "must" : "no longer needs to" %] update their password.
+ [% ELSIF field == 'password_change_reason' %]
+ The password change reason has been modified.
[% END %]
</li>
[% END %]
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 814a02c13..98076ce1c 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -1352,6 +1352,10 @@
[% title = "New Password Missing" %]
You must enter a new password.
+ [% ELSIF error == "new_password_same" %]
+ [% title = "Password Unchanged" %]
+ Your new password cannot be the same as your old password.
+
[% ELSIF error == "no_axes_defined" %]
[% title = "No Axes Defined" %]
[% docslinks = {'reporting.html' => 'Reporting'} %]
diff --git a/token.cgi b/token.cgi
index e87e24fab..a326b2f32 100755
--- a/token.cgi
+++ b/token.cgi
@@ -212,25 +212,14 @@ sub changePassword {
my ($token, $password) = @_;
my $dbh = Bugzilla->dbh;
- # Create a crypted version of the new password
- my $cryptedpassword = bz_crypt($password);
-
- # Get the user's ID from the tokens table.
- my ($userid) = $dbh->selectrow_array('SELECT userid FROM tokens
- WHERE token = ?', undef, $token);
-
- # Update the user's password in the profiles table and delete the token
- # from the tokens table.
- $dbh->bz_start_transaction();
- $dbh->do(q{UPDATE profiles
- SET cryptpassword = ?
- WHERE userid = ?},
- undef, ($cryptedpassword, $userid) );
- Bugzilla->memcached->clear({ table => 'profiles', id => $userid });
- $dbh->do('DELETE FROM tokens WHERE token = ?', undef, $token);
- $dbh->bz_commit_transaction();
+ my ($user_id) = $dbh->selectrow_array('SELECT userid FROM tokens WHERE token = ?', undef, $token);
+ my $user = Bugzilla::User->check({ id => $user_id });
+ $user->set_password($password);
+ $user->update();
+ delete_token($token);
+ $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'password'", undef, $user_id);
- Bugzilla->logout_user_by_id($userid);
+ Bugzilla->logout_user_by_id($user_id);
$vars->{'message'} = "password_changed";