Commit 9c26c145 authored by Gregor Anzelj's avatar Gregor Anzelj Committed by Robert Lyon
Browse files

Bug 845263: Password policy

Improve the password policy enforcement and configuration in Mahara.
Have a pre-defined password policy of a minimum of 8 characters with
type "alphanumeric mixed case + symbols".

Also allow site administrators to set the desired password policy in
Site Options > Security Settings. In all locations where password
is set, the password input should also include a password strength
indicator.

Change-Id: I020af58a6cf1635fe295f5434783ce5b6f6daacb
parent f3d614cb
......@@ -5265,7 +5265,6 @@ function create_elasticsearch_triggers() {
}
}
/**
* Return a list of available institution(s) to associate to a group.
*
......@@ -5302,3 +5301,37 @@ function get_institutions_to_associate() {
return $institutions;
}
/**
* Get the password policy for this site
*
* @param bool $parts When true we return an array of the policy parts and when false return the policy string
*/
function get_password_policy($parts = false) {
$policy = !empty(get_config('passwordpolicy')) ? get_config('passwordpolicy') : '8_ulns';
if ($parts) {
return explode('_', $policy);
}
return $policy;
}
/**
* Get the password policy description based on password policy values
*
* @param bool $type When 'error' we return an error description rather than form field description
* When 'user' we use message to user rather than generic message
*/
function get_password_policy_description($type = 'generic') {
list($numbervalue, $formatvalue) = get_password_policy(true);
if ($type == 'error') {
$formatdesc = strtolower(get_string('element.passwordpolicy.' . $formatvalue, 'pieforms'));
$description = get_string('passwordinvalidform1', 'auth.internal', $numbervalue, $formatdesc);
}
else if ($type == 'user') {
$description = get_string('yournewpassword1', 'mahara', $numbervalue, get_string('passworddescription.' . $formatvalue, 'mahara'));
}
else {
$description = get_string('passworddescriptionbase', 'mahara', $numbervalue) . ' ' . get_string('passworddescription.' . $formatvalue, 'mahara');
}
return $description;
}
......@@ -32,11 +32,65 @@
* @return string The HTML for the element
*/
function pieform_element_password(Pieform $form, $element) {/*{{{*/
return '<input type="password"'
$result = '<input type="password"'
. $form->element_attributes($element)
. ' value="' . Pieform::hsc($form->get_value($element)) . '">';
if (isset($element['showstrength']) && $element['showstrength']) {
$id = $form->get_name() . '_' . $element['id'];
$msg1 = get_string('passwordstrength1');
$msg2 = get_string('passwordstrength2');
$msg3 = get_string('passwordstrength3');
$msg4 = get_string('passwordstrength4');
$result .= '<label for="strengthBar"></label>'
. '<div class="form-control progress password-progress">'
. ' <div id="strengthBar" class="progress-bar" role="progressbar" style="width: 0;"></div>'
. '</div>';
$result .= <<<EOJS
<script type="application/javascript">
jQuery('#{$id}').keyup(function() {
var result = zxcvbn(jQuery('#{$id}').val());
var score = result.score;
var bar = jQuery('#strengthBar');
switch (score) {
case 0:
bar.attr('class', 'progress-bar progress-bar-danger').css('width', '1%');
bar.text('');
break;
case 1:
bar.attr('class', 'progress-bar progress-bar-danger').css('width', '25%');
bar.text('{$msg1}');
break;
case 2:
bar.attr('class', 'progress-bar progress-bar-danger').css('width', '50%');
bar.text('{$msg2}');
break;
case 3:
bar.attr('class', 'progress-bar progress-bar-warning').css('width', '75%');
bar.text('{$msg3}');
break;
case 4:
bar.attr('class', 'progress-bar progress-bar-success').css('width', '100%');
bar.text('{$msg4}');
break;
}
});
</script>
EOJS;
}
return $result;
}/*}}}*/
function pieform_element_password_get_headdata() {
$libjs = get_config('wwwroot') . 'js/zxcvbn/zxcvbn.js';
$result = array(
'<script type="application/javascript" src="' . append_version_number($libjs) . '"></script>',
);
return $result;
}
function pieform_element_password_get_value(Pieform $form, $element) {/*{{{*/
if (isset($element['value'])) {
return $element['value'];
......
<?php
/**
*
* @package pieforms
* @subpackage element
* @author Gregor Anzelj <gregor.anzelj@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
/**
* Provides a password policy chooser, in the form of a minimum length dropdown box
* and password type (uppercase, lowercase, numerals, special symbols) dropdown box.
*
* @param Pieform $form The form to render the element for
* @param array $element The element to render
* @return string The HTML for the element
*/
function pieform_element_passwordpolicy(Pieform $form, $element) {/*{{{*/
$name = Pieform::hsc($element['name']);
$min = (isset($element['minlength'])) && intval($element['minlength']) >= 8 ? intval($element['minlength']) : 8;
$max = (isset($element['maxlength'])) && intval($element['maxlength']) > intval($element['minlength']) ? intval($element['maxlength']) : 20;
$global = ($form->get_property('method') == 'get') ? $_GET : $_POST;
// Get the value of the element for rendering.
if (isset($element['value'])) {
$value = $element['value'];
}
else if ($form->is_submitted() && isset($global[$name . '_number']) && isset($global[$name . '_format'])) {
$value = $global[$name . '_number'] . '_' . $global[$name . '_format'];
}
else if (isset($element['defaultvalue'])) {
$value = $element['defaultvalue'];
}
else {
$value = '8_ulns'; // 8 characters - upper and lowercase letters, numbers, symbols
}
list($numbervalue, $formatvalue) = explode('_', $value);
// Number dropdown
$label = get_string('passwordpolicylength', 'admin');
$number = '<label for="' . $name . '_number" class="accessible-hidden sr-only">' . $label . '</label>';
$number .= '<span class="picker"><select class="form-control select" ';
$number .= 'name="' . $name . '_number" id="' . $name . '_number"' . ' tabindex="' . Pieform::hsc($element['tabindex']) . '"';
if (isset($element['description'])) {
$number .= ' aria-describedby="' . $form->element_descriptors($element) . '"';
}
$number .= ">\n";
for ($i = $min; $i <= $max; $i++) {
$number .= "\t<option value=\"$i\"" . (($numbervalue == $i) ? ' selected="selected"' : '') . '>' . $i . "</option>\n";
}
$number .= "</select></span>\n";
// Format dropdown
$label = get_string('passwordpolicytype', 'admin');
$format = '<label for="' . $name . '_format" class="accessible-hidden sr-only">' . $label . '</label>';
$format .= '<span class="picker"><select class="form-control select" ';
$format .= 'name="' . $name . '_format" id="' . $name . '_format"' . ' tabindex="' . Pieform::hsc($element['tabindex']) . '"';
if (isset($element['description'])) {
$format .= ' aria-describedby="' . $form->element_descriptors($element) . '"';
}
$format .= ">\n";
foreach (pieform_element_passwordpolicy_get_formats() as $f) {
$format .= "\t<option value=\"$f\"" . (($formatvalue == $f) ? ' selected="selected"' : '') . '>'
. $form->i18n('element', 'passwordpolicy', $f, $element) . "</option>\n";
}
$format .= "</select></span>\n";
return $number . $format;
}/*}}}*/
/**
* Gets the value of the date element from the request and converts it into a
* unix timestamp.
*
* @param Pieform $form The form the element is attached to
* @param array $element The element to get the value for
*/
function pieform_element_passwordpolicy_get_value(Pieform $form, $element) {/*{{{*/
$name = Pieform::hsc($element['name']);
$global = ($form->get_property('method') == 'get') ? $_GET : $_POST;
if ($form->is_submitted() && isset($global[$name . '_number']) && isset($global[$name . '_format'])) {
return $global[$name . '_number'] . '_' . $global[$name . '_format'];
}
return '8_ulns';
}/*}}}*/
function pieform_element_passwordpolicy_get_formats() {/*{{{*/
return array(
'ul', // Uppercase and lowercase letters
'uln', // Uppercase and lowercase letters, numbers
'ulns', // Uppercase and lowercase letters, numbers, symbols
);
}/*}}}*/
......@@ -168,6 +168,12 @@ function smarty($javascript = array(), $headers = array(), $pagestrings = array(
if (!isset($langselectform)) {
$langselectform = language_select_form();
}
// Now that password element can set headdata we need to call the login form before smarty_core()
$isloginblockvisible = !$USER->is_logged_in() && !get_config('siteclosedforupgrade')
&& get_config('showloginsideblock');
if ($isloginblockvisible) {
$authgenerateloginform = auth_generate_login_form();
}
$smarty = smarty_core();
$wwwroot = get_config('wwwroot');
......@@ -868,15 +874,13 @@ EOF;
);
}
$isloginblockvisible = !$USER->is_logged_in() && !get_config('siteclosedforupgrade')
&& get_config('showloginsideblock');
if ($isloginblockvisible) {
$sideblocks[] = array(
'name' => 'login',
'weight' => -10,
'id' => 'sb-loginbox',
'data' => array(
'loginform' => auth_generate_login_form(),
'loginform' => $authgenerateloginform,
),
);
}
......
......@@ -39,7 +39,7 @@ class BehatTestingUtil extends TestingUtil {
*/
protected static $sitedefaultinfo = array(
'admin' => array(
'password' => 'Kupuhipa1',
'password' => 'Kupuh1pa!',
'email' => 'admin@test.mahara.org',
),
'sitename' => self::BEHATSITENAME,
......
......@@ -46,8 +46,13 @@ echo $form_tag;
<?php echo $elements[$field]['labelhtml']; ?>
<?php echo $elements[$field]['html']; ?>
<?php if (isset($elements[$field]['description'])) { ?>
<div class="metadata form-group html">
<?php echo $elements[$field]['description']; ?>
</div>
<?php } ?>
<?php if (isset($elements[$field]['error'])) { ?>
<p class="text-danger"><?php echo $elements[$field]['error']; ?></p>
<p class="text-danger"><?php echo $elements[$field]['error']; ?></p>
<?php } ?>
</div>
<?php } ?>
......
......@@ -17,6 +17,16 @@ select {
}
}
.password-progress {
margin-top: 8px;
margin-bottom: 0;
padding: 0 !important;
.step2 & {
width: 100% !important;
}
}
label,
.pseudolabel {
@extend .control-label;
......
......@@ -5,7 +5,7 @@
To see if mahara is secure enough
Scenario:Injecting sql in groups search field
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "My groups" in "Groups" from main menu
And I click on "Create group"
And I set the following fields to these values:
......
......@@ -6,7 +6,7 @@ So I can check the field doesn't error
Scenario: sql injection attempt on search field
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
When I set the following fields to these values:
| Search users | 'or 1=1;-- |
And I press "Go"
......
......@@ -7,54 +7,49 @@ Feature: Suckypasswords Test increase of array size
Background:
Given the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| UserA | Kupuhipa1 | UserA@example.org | Angela | User | mahara | internal | member |
| Supercool | Kupuh1pa! | Supercool@example.org | Super | Cool | mahara | internal | member |
Scenario: Admin can't change password to anything on suckypasswords list (Bug #844457)
Given I log in as "admin" with password "Kupuhipa1"
Scenario: Admin can't change password to anything not fitting password policy
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Settings" from user menu
And I fill in "Current password" with "Kupuhipa1"
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "abc123"
And I fill in "Confirm password" with "abc123"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I fill in "New password" with "dragon"
And I fill in "Confirm password" with "dragon"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I should see "Password must be at least 8 characters long."
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "administrator"
And I fill in "Confirm password" with "administrator"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I fill in "New password" with "mahara"
And I fill in "Confirm password" with "mahara"
And I should see "Password must contain upper and lowercase letters, numbers, symbols."
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "Admin@123"
And I fill in "Confirm password" with "Admin@123"
And I press "Save"
And I should see "Your password is too easy"
And I log out
Scenario: Student can't change password to anything on suckypasswords list (Bug #844457)
Given I log in as "UserA" with password "Kupuhipa1"
Scenario: Student can't change password to anything not fitting password policy
Given I log in as "Supercool" with password "Kupuh1pa!"
And I choose "Settings" from user menu
And I fill in "Current password" with "Kupuhipa1"
And I fill in "New password" with "abc123"
And I fill in "Confirm password" with "abc123"
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "fastdog"
And I fill in "Confirm password" with "fastdog"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I fill in "New password" with "dragon"
And I fill in "Confirm password" with "dragon"
And I should see "Password must be at least 8 characters long."
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "supercool"
And I fill in "Confirm password" with "supercool"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I should see "Passwords are case sensitive and must be different from your username."
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "administrator"
And I fill in "Confirm password" with "administrator"
And I press "Save"
And I should see "Your password is too easy"
And I fill in "Current password" with "Kupuhipa1"
And I fill in "New password" with "mahara"
And I fill in "Confirm password" with "mahara"
And I should see "Password must contain upper and lowercase letters, numbers, symbols."
And I fill in "Current password" with "Kupuh1pa!"
And I fill in "New password" with "p@ssw0rd"
And I fill in "Confirm password" with "p@ssw0rd"
And I press "Save"
And I should see "Your password is too easy"
And I log out
......@@ -11,11 +11,11 @@ Background:
| instone | Institution One | ON | OFF |
Given the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| UserA | Kupuhipa1 | UserA@example.org | Angela | User | instone | internal | member |
| UserB | Kupuhipa1 | UserB@example.org | Bob | User | instone | internal | member |
| UserA | Kupuh1pa! | UserA@example.org | Angela | User | instone | internal | member |
| UserB | Kupuh1pa! | UserB@example.org | Bob | User | instone | internal | member |
Scenario: Accessing user reports
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "User search" in "Users" from administration menu
And I check "selectusers_2"
And I check "selectusers_3"
......
......@@ -6,7 +6,7 @@ Feature: Create an Institution
Scenario: Creating an institution (selenium test)
# Log in as "Admin" user
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
# Creating an Institution
And I choose "Settings" in "Institutions" from administration menu
......
......@@ -12,17 +12,17 @@ Background:
And the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| StaffA | Kupuhipa1 | StaffA@example.com | Alexei | Staff | instone | internal | staff |
| StaffA | Kupuh1pa! | StaffA@example.com | Alexei | Staff | instone | internal | staff |
Scenario: Admin to add an user (Bug 1703721)
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Add user" in "Users" from administration menu
And I set the following fields to these values:
| First name | Bob |
| Last name | One |
| Email | UserB@example.com |
| Username | instadmin |
| password | Kupuhipa1 |
| password | Kupuh1pa! |
| Institution administrator | 1 |
And I select "Institution One" from "adduser_authinstance"
And I scroll to the top
......@@ -54,14 +54,14 @@ Background:
# Test for logout confirmation
And I should see "You have been logged out successfully"
#login as staff user
Given I log in as "StaffA" with password "Kupuhipa1"
Given I log in as "StaffA" with password "Kupuh1pa!"
And I click on "Show administration menu"
And I should see "Reports" in the "Administration menu" property
And I should not see "Groups" in the "Administration menu" property
#Site admin role already tested in menu_navigation.feature file
Scenario: Create users by csv (Bug 1426983)
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
#Adding 50 Users by csv
And I choose "Add users by CSV" in "Users" from administration menu
And I attach the file "50users_new.csv" to "CSV file"
......@@ -79,10 +79,10 @@ Scenario: Create users by csv (Bug 1426983)
And I should see "Users updated: 20."
And I log out
#Check that we update the fields, password change and email recieved
Given I log in as "user0005" with password "changeme3"
Given I log in as "user0005" with password "cH@ngeme3"
And I should see "You are required to change your password before you can proceed."
And I fill in "New password" with "dragon123"
And I fill in "Confirm password" with "dragon123"
And I fill in "New password" with "dr@Gon123"
And I fill in "Confirm password" with "dr@Gon123"
And I press "Submit"
And I should see "Your new password has been saved"
And I should see "Institution membership confirmation"
......@@ -95,7 +95,7 @@ Scenario: Create users by csv (Bug 1426983)
And the "Occupation" field should contain "Hairdresser"
And I log out
#login back as admin
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "User search" in "Users" from administration menu
# Check that we can delete a user after upload (Bug #1558864)
And I follow "user0005"
......
......@@ -6,7 +6,7 @@ As an admin
So I can make sure I can close/open site or clear cache
Scenario: Closing the site
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Admin home" from administration menu
# Verifying I'm on the right page
And I should see "Register your Mahara site"
......
......@@ -6,15 +6,15 @@ In order to be able to see the (Author's name hidden) link
Background:
Given the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| UserA | Kupuhipa1 | UserA@example.org | Angela | User | mahara | internal | member |
| UserB | Kupuhipa1 | UserB@example.org | Bob | User | mahara | internal | member |
| UserA | Kupuh1pa! | UserA@example.org | Angela | User | mahara | internal | member |
| UserB | Kupuh1pa! | UserB@example.org | Bob | User | mahara | internal | member |
And the following "pages" exist:
| title | description | ownertype | ownername |
| Page UserA_01 | Page 01| user | UserA |
Scenario: Testing that views & collections are collated properly
# Turn on anonymous pages
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I go to "admin/site/options.php"
And I follow "General settings"
And I set the following fields to these values:
......@@ -23,7 +23,7 @@ Scenario: Testing that views & collections are collated properly
And I log out
# Make page anonymous
Given I log in as "UserA" with password "Kupuhipa1"
Given I log in as "UserA" with password "Kupuh1pa!"
And I choose "Pages and collections" in "Portfolio" from main menu
And I follow "Page UserA_01"
And I follow "Edit"
......@@ -36,12 +36,12 @@ Scenario: Testing that views & collections are collated properly
And I press "Save"
And I log out
Given I log in as "UserB" with password "Kupuhipa1"
Given I log in as "UserB" with password "Kupuh1pa!"
And I am on homepage
Then I should see "(Author's name hidden)"
And I log out
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I am on homepage
Then should see "(Author's name hidden)"
And I follow "(Author's name hidden)"
......
......@@ -6,10 +6,10 @@ Feature: Mahara users can change their account settings
Background:
Given the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| UserA | Kupuhipa1 | UserA@example.org | Angela | User | mahara | internal | member |
| UserA | Kupuh1pa! | UserA@example.org | Angela | User | mahara | internal | member |
Scenario: Change notifications
Given I log in as "UserA" with password "Kupuhipa1"
Given I log in as "UserA" with password "Kupuh1pa!"
And I choose "Notifications" in "Settings" from user menu
And I select "Email" from "activity_viewaccess"
When I press "Save"
......
......@@ -5,7 +5,7 @@ As an admin
I need to be able to enable it via a switch
Scenario: Turning the switches on and off on Cookie consent page (Bug 1431569)
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Cookie Consent" in "Configure site" from administration menu
And I set the following fields to these values:
| Enable Cookie Consent | 1 |
......
......@@ -6,7 +6,7 @@ Feature: Creating/Deleting external links from the Links and Resources sideblock
Scenario: Creating and deleting external links (Selenium 1426983)
# Log in as "Admin" user
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
# Entering an external link
And I choose "Menus" in "Configure site" from administration menu
And I select "Logged-in links and resources" from "Edit:"
......@@ -27,7 +27,7 @@ Scenario: Creating and deleting external links (Selenium 1426983)
Scenario: Make sure blogs do not show in site file link options (Bug #1537426)
# Log in as "Admin" user
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
# I create a site journal
And I choose "Journals" in "Configure site" from administration menu
And I follow "Create journal"
......
......@@ -5,7 +5,7 @@ As an admin
So I can search via elasticsearch
Scenario: Check elasticsearch plugin is ready
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Plugin administration" in "Extensions" from administration menu
When I follow "Configuration for search elasticsearch"
Then I should see "Failed to connect to 127.0.0.1 port 9200"
......
......@@ -5,7 +5,7 @@ As an admin
So I can have different settings for each institution
Scenario: Turning switches on and off on Edit Institution page (Bug 1431569)
Given I log in as "admin" with password "Kupuhipa1"
Given I log in as "admin" with password "Kupuh1pa!"
And I choose "Settings" in "Institutions" from administration menu
And I press "Add institution"
And I set the following fields to these values:
......