From 1a20dc936256b10a3744f216f091a70fd441edea Mon Sep 17 00:00:00 2001 From: csteipp Date: Wed, 22 Apr 2015 18:48:48 -0700 Subject: [PATCH] Password validity by policy per group Make password policies defined in a configurable policy, which is defined by group. A user's password policy will be the maximum of each group policy that the user belongs to. Bug: T94774 Change-Id: Iad8e49ffcffed38df6293db0ef31a227d3962003 --- autoload.php | 2 + docs/hooks.txt | 4 + includes/DefaultSettings.php | 59 ++++- includes/Setup.php | 9 + includes/User.php | 33 +-- includes/password/PasswordPolicyChecks.php | 115 +++++++++ includes/password/UserPasswordPolicy.php | 158 ++++++++++++ tests/phpunit/includes/UserTest.php | 25 +- .../password/PasswordPolicyChecksTest.php | 136 ++++++++++ .../password/UserPasswordPolicyTest.php | 234 ++++++++++++++++++ 10 files changed, 744 insertions(+), 31 deletions(-) create mode 100644 includes/password/PasswordPolicyChecks.php create mode 100644 includes/password/UserPasswordPolicy.php create mode 100644 tests/phpunit/includes/password/PasswordPolicyChecksTest.php create mode 100644 tests/phpunit/includes/password/UserPasswordPolicyTest.php diff --git a/autoload.php b/autoload.php index 74c7efda69..9399b0f4bb 100644 --- a/autoload.php +++ b/autoload.php @@ -874,6 +874,7 @@ $wgAutoloadLocalClasses = array( 'Password' => __DIR__ . '/includes/password/Password.php', 'PasswordError' => __DIR__ . '/includes/password/PasswordError.php', 'PasswordFactory' => __DIR__ . '/includes/password/PasswordFactory.php', + 'PasswordPolicyChecks' => __DIR__ . '/includes/password/PasswordPolicyChecks.php', 'PatchSql' => __DIR__ . '/maintenance/patchSql.php', 'PathRouter' => __DIR__ . '/includes/PathRouter.php', 'PathRouterPatternReplacer' => __DIR__ . '/includes/PathRouter.php', @@ -1295,6 +1296,7 @@ $wgAutoloadLocalClasses = array( 'UserMailer' => __DIR__ . '/includes/mail/UserMailer.php', 'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php', 'UserOptions' => __DIR__ . '/maintenance/userOptions.inc', + 'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php', 'UserRightsProxy' => __DIR__ . '/includes/UserRightsProxy.php', 'UsercreateTemplate' => __DIR__ . '/includes/templates/Usercreate.php', 'UserloginTemplate' => __DIR__ . '/includes/templates/Userlogin.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index bf8d164975..fda00f9f83 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -2295,6 +2295,10 @@ run. Use when page save hooks require the presence of custom tables to ensure that tests continue to run properly. &$tables: array of table names +'PasswordPoliciesForUser': Alter the effective password policy for a user. +$user: User object whose policy you are modifying +&$effectivePolicy: Array of policy statements that apply to this user + 'PerformRetroactiveAutoblock': Called before a retroactive autoblock is applied to a user. $block: Block object (which is set to be autoblocking) diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index bd0fbc2073..5c29ff8689 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4250,6 +4250,59 @@ $wgActiveUserDays = 30; * @{ */ +/** + * Password policy for local wiki users. A user's effective policy + * is the superset of all policy statements from the policies for the + * groups where the user is a member. If more than one group policy + * include the same policy statement, the value is the max() of the + * values. Note true > false. The 'default' policy group is required, + * and serves as the minimum policy for all users. New statements can + * be added by appending to $wgPasswordPolicy['checks']. + * Statements: + * - MinimalPasswordLength - minimum length a user can set + * - MinimumPasswordLengthToLogin - passwords shorter than this will + * not be allowed to login, regardless if it is correct. + * - MaximalPasswordLength - maximum length password a user is allowed + * to attempt. Prevents DoS attacks with pbkdf2. + * - PasswordCannotMatchUsername - Password cannot match username to + * - PasswordCannotMatchBlacklist - Username/password combination cannot + * match a specific, hardcoded blacklist. + * @since 1.26 + */ +$wgPasswordPolicy = array( + 'policies' => array( + 'bureaucrat' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'sysop' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'bot' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'default' => array( + 'MinimalPasswordLength' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + ), + 'checks' => array( + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + ), +); + + /** * For compatibility with old installations set to false * @deprecated since 1.24 will be removed in future @@ -4259,8 +4312,9 @@ $wgPasswordSalt = true; /** * Specifies the minimal length of a user password. If set to 0, empty pass- * words are allowed. + * @deprecated since 1.26, use $wgPasswordPolicy's MinimalPasswordLength. */ -$wgMinimalPasswordLength = 1; +$wgMinimalPasswordLength = false; /** * Specifies the maximal length of a user password (T64685). @@ -4271,8 +4325,9 @@ $wgMinimalPasswordLength = 1; * * @warning Unlike other password settings, user with passwords greater than * the maximum will not be able to log in. + * @deprecated since 1.26, use $wgPasswordPolicy's MaximalPasswordLength. */ -$wgMaximalPasswordLength = 4096; +$wgMaximalPasswordLength = false; /** * Specifies if users should be sent to a password-reset form on login, if their diff --git a/includes/Setup.php b/includes/Setup.php index 46712527aa..3fec2ff51e 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -470,6 +470,15 @@ if ( $wgProfileOnly ) { $wgDebugLogFile = ''; } +// Backwards compatibility with old password limits +if ( $wgMinimalPasswordLength !== false ) { + $wgPasswordPolicy['policies']['default']['MinimalPasswordLength'] = $wgMinimalPasswordLength; +} + +if ( $wgMaximalPasswordLength !== false ) { + $wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength; +} + Profiler::instance()->scopedProfileOut( $ps_default ); // Disable MWDebug for command line mode, this prevents MWDebug from eating up diff --git a/includes/User.php b/includes/User.php index bf0326a0cd..bdfa03018b 100644 --- a/includes/User.php +++ b/includes/User.php @@ -844,15 +844,14 @@ class User implements IDBAccessObject { * @since 1.23 */ public function checkPasswordValidity( $password ) { - global $wgMinimalPasswordLength, $wgMaximalPasswordLength, $wgContLang; + global $wgPasswordPolicy; - static $blockedLogins = array( - 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 - 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 + $upp = new UserPasswordPolicy( + $wgPasswordPolicy['policies'], + $wgPasswordPolicy['checks'] ); $status = Status::newGood(); - $result = false; //init $result to false for the internal checks if ( !Hooks::run( 'isValidPassword', array( $password, &$result, $this ) ) ) { @@ -861,28 +860,8 @@ class User implements IDBAccessObject { } if ( $result === false ) { - if ( strlen( $password ) < $wgMinimalPasswordLength ) { - $status->error( 'passwordtooshort', $wgMinimalPasswordLength ); - return $status; - } elseif ( strlen( $password ) > $wgMaximalPasswordLength ) { - // T64685: Password too long, might cause DoS attack - $status->fatal( 'passwordtoolong', $wgMaximalPasswordLength ); - return $status; - } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) { - $status->error( 'password-name-match' ); - return $status; - } elseif ( isset( $blockedLogins[$this->getName()] ) - && $password == $blockedLogins[$this->getName()] - ) { - $status->error( 'password-login-forbidden' ); - return $status; - } else { - //it seems weird returning a Good status here, but this is because of the - //initialization of $result to false above. If the hook is never run or it - //doesn't modify $result, then we will likely get down into this if with - //a valid password. - return $status; - } + $status->merge( $upp->checkUserPassword( $this, $password ) ); + return $status; } elseif ( $result === true ) { return $status; } else { diff --git a/includes/password/PasswordPolicyChecks.php b/includes/password/PasswordPolicyChecks.php new file mode 100644 index 0000000000..eb4a9582dc --- /dev/null +++ b/includes/password/PasswordPolicyChecks.php @@ -0,0 +1,115 @@ + strlen( $password ) ) { + $status->error( 'passwordtooshort', $policyVal ); + } + return $status; + } + + /** + * Check password is longer than minimum, fatal + * @param int $policyVal minimal length + * @param User $user + * @param string $password + * @return Status fatal if $password is shorter than $policyVal + */ + public static function checkMinimumPasswordLengthToLogin( $policyVal, User $user, $password ) { + $status = Status::newGood(); + if ( $policyVal > strlen( $password ) ) { + $status->fatal( 'passwordtooshort', $policyVal ); + } + return $status; + } + + /** + * Check password is shorter than maximum, fatal + * @param int $policyVal maximum length + * @param User $user + * @param string $password + * @return Status fatal if $password is shorter than $policyVal + */ + public static function checkMaximalPasswordLength( $policyVal, User $user, $password ) { + $status = Status::newGood(); + if ( $policyVal < strlen( $password ) ) { + $status->fatal( 'passwordtoolong', $policyVal ); + } + return $status; + } + + /** + * Check if username and password match + * @param bool $policyVal true to force compliance. + * @param User $user + * @param string $password + * @return Status error if username and password match, and policy is true + */ + public static function checkPasswordCannotMatchUsername( $policyVal, User $user, $password ) { + global $wgContLang; + $status = Status::newGood(); + $username = $user->getName(); + if ( $policyVal && $wgContLang->lc( $password ) === $wgContLang->lc( $username ) ) { + $status->error( 'password-name-match' ); + } + return $status; + } + + /** + * Check if username and password are on a blacklist + * @param bool $policyVal true to force compliance. + * @param User $user + * @param string $password + * @return Status error if username and password match, and policy is true + */ + public static function checkPasswordCannotMatchBlacklist( $policyVal, User $user, $password ) { + static $blockedLogins = array( + 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 + 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 + ); + + $status = Status::newGood(); + $username = $user->getName(); + if ( $policyVal + && isset( $blockedLogins[$username] ) + && $password == $blockedLogins[$username] + ) { + $status->error( 'password-login-forbidden' ); + } + return $status; + } + +} diff --git a/includes/password/UserPasswordPolicy.php b/includes/password/UserPasswordPolicy.php new file mode 100644 index 0000000000..cdad9ba511 --- /dev/null +++ b/includes/password/UserPasswordPolicy.php @@ -0,0 +1,158 @@ +policies = $policies; + + foreach ( $checks as $statement => $check ) { + if ( !is_callable( $check ) ) { + throw new InvalidArgumentException( + 'Policy check functions must be callable' + ); + } + $this->policyCheckFunctions[$statement] = $check; + } + } + + /** + * Check if a passwords meets the effective password policy for a User. + * @param User $user who's policy we are checking + * @param string $password the password to check + * @return Status error to indicate the password didn't meet the policy, or fatal to + * indicate the user shouldn't be allowed to login. + */ + public function checkUserPassword( User $user, $password ) { + $effectivePolicy = $this->getPoliciesForUser( $user ); + $status = Status::newGood(); + + foreach ( $effectivePolicy as $policy => $value ) { + if ( !isset( $this->policyCheckFunctions[$policy] ) ) { + throw new DomainException( 'Invalid password policy config' ); + } + $status->merge( + call_user_func( + $this->policyCheckFunctions[$policy], + $value, + $user, + $password + ) + ); + } + + return $status; + } + + /** + * Get the policy for a user, based on their group membership. Public so + * UI elements can access and inform the user. + * @param User $user + * @return array the effective policy for $user + */ + public function getPoliciesForUser( User $user ) { + $effectivePolicy = self::getPoliciesForGroups( + $this->policies, + $user->getEffectiveGroups(), + $this->policies['default'] + ); + + Hooks::run( 'PasswordPoliciesForUser', array( $user, &$effectivePolicy ) ); + + return $effectivePolicy; + } + + /** + * Utility function to get the effective policy from a list of policies, based + * on a list of groups. + * @param array $policies list of policies to consider + * @param array $userGroups the groups from which we calculate the effective policy + * @param array $defaultPolicy the default policy to start from + * @return array effective policy + */ + public static function getPoliciesForGroups( array $policies, array $userGroups, + array $defaultPolicy + ) { + $effectivePolicy = $defaultPolicy; + foreach ( $policies as $group => $policy ) { + if ( in_array( $group, $userGroups ) ) { + $effectivePolicy = self::maxOfPolicies( + $effectivePolicy, + $policies[$group] + ); + } + } + + return $effectivePolicy; + } + + /** + * Utility function to get a policy that is the most restrictive of $p1 and $p2. For + * simplicity, we setup the policy values so the maximum value is always more restrictive. + * @param array $p1 + * @param array $p2 + * @return array containing the more restrictive values of $p1 and $p2 + */ + public static function maxOfPolicies( array $p1, array $p2 ) { + $ret = array(); + $keys = array_merge( array_keys( $p1 ), array_keys( $p2 ) ); + foreach ( $keys as $key ) { + if ( !isset( $p1[$key] ) ) { + $ret[$key] = $p2[$key]; + } elseif ( !isset( $p2[$key] ) ) { + $ret[$key] = $p1[$key]; + } else { + $ret[$key] = max( $p1[$key], $p2[$key] ); + } + } + return $ret; + } + +} diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php index 370b5b2c5b..a1f8361a73 100644 --- a/tests/phpunit/includes/UserTest.php +++ b/tests/phpunit/includes/UserTest.php @@ -307,9 +307,30 @@ class UserTest extends MediaWikiTestCase { */ public function testCheckPasswordValidity() { $this->setMwGlobals( array( - 'wgMinimalPasswordLength' => 6, - 'wgMaximalPasswordLength' => 30, + 'wgPasswordPolicy' => array( + 'policies' => array( + 'sysop' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => 1, + ), + 'default' => array( + 'MinimalPasswordLength' => 6, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 30, + ), + ), + 'checks' => array( + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + ), + ), ) ); + $user = User::newFromName( 'Useruser' ); // Sanity $this->assertTrue( $user->isValidPassword( 'Password1234' ) ); diff --git a/tests/phpunit/includes/password/PasswordPolicyChecksTest.php b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php new file mode 100644 index 0000000000..af34282fb3 --- /dev/null +++ b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php @@ -0,0 +1,136 @@ +assertTrue( $statusOK->isGood(), 'Password is longer than minimal policy' ); + $statusShort = PasswordPolicyChecks::checkMinimalPasswordLength( + 10, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertFalse( + $statusShort->isGood(), + 'Password is shorter than minimal policy' + ); + $this->assertTrue( + $statusShort->isOk(), + 'Password is shorter than minimal policy, not fatal' + ); + } + + /** + * @covers PasswordPolicyChecks::checkMinimumPasswordLengthToLogin + */ + public function testCheckMinimumPasswordLengthToLogin() { + $statusOK = PasswordPolicyChecks::checkMinimumPasswordLengthToLogin( + 3, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertTrue( $statusOK->isGood(), 'Password is longer than minimal policy' ); + $statusShort = PasswordPolicyChecks::checkMinimumPasswordLengthToLogin( + 10, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertFalse( + $statusShort->isGood(), + 'Password is shorter than minimum login policy' + ); + $this->assertFalse( + $statusShort->isOk(), + 'Password is shorter than minimum login policy, fatal' + ); + } + + /** + * @covers PasswordPolicyChecks::checkMaximalPasswordLength + */ + public function testCheckMaximalPasswordLength() { + $statusOK = PasswordPolicyChecks::checkMaximalPasswordLength( + 100, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertTrue( $statusOK->isGood(), 'Password is shorter than maximal policy' ); + $statusLong = PasswordPolicyChecks::checkMaximalPasswordLength( + 4, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertFalse( $statusLong->isGood(), + 'Password is longer than maximal policy' + ); + $this->assertFalse( $statusLong->isOk(), + 'Password is longer than maximal policy, fatal' + ); + } + + /** + * @covers PasswordPolicyChecks::checkPasswordCannotMatchUsername + */ + public function testCheckPasswordCannotMatchUsername() { + $statusOK = PasswordPolicyChecks::checkPasswordCannotMatchUsername( + 1, // policy value + User::newFromName( 'user' ), // User + 'password' // password + ); + $this->assertTrue( $statusOK->isGood(), 'Password does not match username' ); + $statusLong = PasswordPolicyChecks::checkPasswordCannotMatchUsername( + 1, // policy value + User::newFromName( 'user' ), // User + 'user' // password + ); + $this->assertFalse( $statusLong->isGood(), 'Password matches username' ); + $this->assertTrue( $statusLong->isOk(), 'Password matches username, not fatal' ); + } + + /** + * @covers PasswordPolicyChecks::checkPasswordCannotMatchBlacklist + */ + public function testCheckPasswordCannotMatchBlacklist() { + $statusOK = PasswordPolicyChecks::checkPasswordCannotMatchBlacklist( + true, // policy value + User::newFromName( 'Username' ), // User + 'AUniquePassword' // password + ); + $this->assertTrue( $statusOK->isGood(), 'Password is not on blacklist' ); + $statusLong = PasswordPolicyChecks::checkPasswordCannotMatchBlacklist( + true, // policy value + User::newFromName( 'Useruser1' ), // User + 'Passpass1' // password + ); + $this->assertFalse( $statusLong->isGood(), 'Password matches blacklist' ); + $this->assertTrue( $statusLong->isOk(), 'Password matches blacklist, not fatal' ); + } + +} diff --git a/tests/phpunit/includes/password/UserPasswordPolicyTest.php b/tests/phpunit/includes/password/UserPasswordPolicyTest.php new file mode 100644 index 0000000000..ce4e30abb6 --- /dev/null +++ b/tests/phpunit/includes/password/UserPasswordPolicyTest.php @@ -0,0 +1,234 @@ + array( + 'MinimalPasswordLength' => 10, + 'MinimumPasswordLengthToLogin' => 6, + 'PasswordCannotMatchUsername' => true, + ), + 'sysop' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'default' => array( + 'MinimalPasswordLength' => 4, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + ); + + protected $checks = array( + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + ); + + private function getUserPasswordPolicy() { + return new UserPasswordPolicy( $this->policies, $this->checks ); + } + + /** + * @covers UserPasswordPolicy::getPoliciesForUser + */ + public function testGetPoliciesForUser() { + + $upp = $this->getUserPasswordPolicy(); + + $user = User::newFromName( 'TestUserPolicy' ); + $user->addGroup( 'sysop' ); + + $this->assertArrayEquals( + array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => 1, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + $upp->getPoliciesForUser( $user ) + ); + } + + /** + * @covers UserPasswordPolicy::getPoliciesForGroups + */ + public function testGetPoliciesForGroups() { + $effective = UserPasswordPolicy::getPoliciesForGroups( + $this->policies, + array( 'user', 'checkuser' ), + $this->policies['default'] + ); + + $this->assertArrayEquals( + array( + 'MinimalPasswordLength' => 10, + 'MinimumPasswordLengthToLogin' => 6, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + $effective + ); + } + + /** + * @dataProvider provideCheckUserPassword + * @covers UserPasswordPolicy::checkUserPassword + */ + public function testCheckUserPassword( $username, $groups, $password, $valid, $ok, $msg ) { + + $upp = $this->getUserPasswordPolicy(); + + $user = User::newFromName( $username ); + foreach ( $groups as $group ) { + $user->addGroup( $group ); + } + + $status = $upp->checkUserPassword( $user, $password ); + $this->assertSame( $valid, $status->isGood(), $msg . ' - password valid' ); + $this->assertSame( $ok, $status->isOk(), $msg . ' - can login' ); + } + + public function provideCheckUserPassword() { + return array( + array( + 'PassPolicyUser', + array(), + '', + false, + false, + 'No groups, default policy, password too short to login' + ), + array( + 'PassPolicyUser', + array( 'user' ), + 'aaa', + false, + true, + 'Default policy, short password' + ), + array( + 'PassPolicyUser', + array( 'sysop' ), + 'abcdabcdabcd', + true, + true, + 'Sysop with good password' + ), + array( + 'PassPolicyUser', + array( 'sysop' ), + 'abcd', + false, + true, + 'Sysop with short password' + ), + array( + 'PassPolicyUser', + array( 'sysop', 'checkuser' ), + 'abcdabcd', + false, + true, + 'Checkuser with short password' + ), + array( + 'PassPolicyUser', + array( 'sysop', 'checkuser' ), + 'abcd', + false, + false, + 'Checkuser with too short password to login' + ), + array( + 'Useruser', + array( 'user' ), + 'Passpass', + false, + true, + 'Username & password on blacklist' + ), + ); + } + + /** + * @dataProvider provideMaxOfPolicies + * @covers UserPasswordPolicy::maxOfPolicies + */ + public function testMaxOfPolicies( $p1, $p2, $max, $msg ) { + $this->assertArrayEquals( + $max, + UserPasswordPolicy::maxOfPolicies( $p1, $p2 ), + $msg + ); + } + + public function provideMaxOfPolicies() { + return array( + array( + array( 'MinimalPasswordLength' => 8 ), //p1 + array( 'MinimalPasswordLength' => 2 ), //p2 + array( 'MinimalPasswordLength' => 8 ), //max + 'Basic max in p1' + ), + array( + array( 'MinimalPasswordLength' => 2 ), //p1 + array( 'MinimalPasswordLength' => 8 ), //p2 + array( 'MinimalPasswordLength' => 8 ), //max + 'Basic max in p2' + ), + array( + array( 'MinimalPasswordLength' => 8 ), //p1 + array( + 'MinimalPasswordLength' => 2, + 'PasswordCannotMatchUsername' => 1, + ), //p2 + array( + 'MinimalPasswordLength' => 8, + 'PasswordCannotMatchUsername' => 1, + ), //max + 'Missing items in p1' + ), + array( + array( + 'MinimalPasswordLength' => 8, + 'PasswordCannotMatchUsername' => 1, + ), //p1 + array( + 'MinimalPasswordLength' => 2, + ), //p2 + array( + 'MinimalPasswordLength' => 8, + 'PasswordCannotMatchUsername' => 1, + ), //max + 'Missing items in p2' + ), + ); + } + +} -- 2.20.1