Changeset View
Changeset View
Standalone View
Standalone View
src/app/Rules/Password.php
<?php | <?php | ||||
namespace App\Rules; | namespace App\Rules; | ||||
use Illuminate\Contracts\Validation\Rule; | use Illuminate\Contracts\Validation\Rule; | ||||
use Illuminate\Support\Facades\Hash; | |||||
use Illuminate\Support\Facades\Validator; | use Illuminate\Support\Facades\Validator; | ||||
use Illuminate\Support\Str; | use Illuminate\Support\Str; | ||||
class Password implements Rule | class Password implements Rule | ||||
{ | { | ||||
/** @var ?string The validation error message */ | |||||
private $message; | private $message; | ||||
/** @var ?\App\User The account owner which to take the policy from */ | |||||
private $owner; | private $owner; | ||||
/** @var ?\App\User The user to whom the checked password belongs */ | |||||
private $user; | |||||
/** | /** | ||||
* Class constructor. | * Class constructor. | ||||
* | * | ||||
* @param \App\User $owner The account owner (to take the policy from) | * @param ?\App\User $owner The account owner (to take the policy from) | ||||
* @param ?\App\User $user The user the password is for (Null for a new user) | |||||
*/ | */ | ||||
public function __construct(?\App\User $owner = null) | public function __construct(?\App\User $owner = null, ?\App\User $user = null) | ||||
{ | { | ||||
$this->owner = $owner; | $this->owner = $owner; | ||||
$this->user = $user; | |||||
} | } | ||||
/** | /** | ||||
* Determine if the validation rule passes. | * Determine if the validation rule passes. | ||||
* | * | ||||
* @param string $attribute Attribute name | * @param string $attribute Attribute name | ||||
* @param mixed $password Password string | * @param mixed $password Password string | ||||
* | * | ||||
Show All 29 Lines | class Password implements Rule | ||||
public function check($password): array | public function check($password): array | ||||
{ | { | ||||
$rules = $this->rules(); | $rules = $this->rules(); | ||||
foreach ($rules as $name => $rule) { | foreach ($rules as $name => $rule) { | ||||
switch ($name) { | switch ($name) { | ||||
case 'min': | case 'min': | ||||
// Check the min length | // Check the min length | ||||
$pass = strlen($password) >= intval($rule['param']); | $status = strlen($password) >= intval($rule['param']); | ||||
break; | break; | ||||
case 'max': | case 'max': | ||||
// Check the max length | // Check the max length | ||||
$length = strlen($password); | $length = strlen($password); | ||||
$pass = $length && $length <= intval($rule['param']); | $status = $length && $length <= intval($rule['param']); | ||||
break; | break; | ||||
case 'lower': | case 'lower': | ||||
// Check if password contains a lower-case character | // Check if password contains a lower-case character | ||||
$pass = preg_match('/[a-z]/', $password) > 0; | $status = preg_match('/[a-z]/', $password) > 0; | ||||
break; | break; | ||||
case 'upper': | case 'upper': | ||||
// Check if password contains a upper-case character | // Check if password contains a upper-case character | ||||
$pass = preg_match('/[A-Z]/', $password) > 0; | $status = preg_match('/[A-Z]/', $password) > 0; | ||||
break; | break; | ||||
case 'digit': | case 'digit': | ||||
// Check if password contains a digit | // Check if password contains a digit | ||||
$pass = preg_match('/[0-9]/', $password) > 0; | $status = preg_match('/[0-9]/', $password) > 0; | ||||
break; | break; | ||||
case 'special': | case 'special': | ||||
// Check if password contains a special character | // Check if password contains a special character | ||||
$pass = preg_match('/[-~!@#$%^&*_+=`(){}[]|:;"\'`<>,.?\/\\]/', $password) > 0; | $status = preg_match('/[-~!@#$%^&*_+=`(){}[]|:;"\'`<>,.?\/\\]/', $password) > 0; | ||||
break; | |||||
case 'last': | |||||
// TODO: For performance reasons we might consider checking the history | |||||
// only when the password passed all other checks | |||||
$status = $this->checkPasswordHistory($password, (int) $rule['param']); | |||||
break; | break; | ||||
default: | default: | ||||
// Ignore unknown rule name | // Ignore unknown rule name | ||||
$pass = true; | $status = true; | ||||
} | } | ||||
$rules[$name]['status'] = $pass; | $rules[$name]['status'] = $status; | ||||
} | } | ||||
return $rules; | return $rules; | ||||
} | } | ||||
/** | /** | ||||
* Get the list of rules for a password | * Get the list of rules for a password | ||||
* | * | ||||
* @param bool $all List all supported rules, instead of the enabled ones | * @param bool $all List all supported rules, instead of the enabled ones | ||||
* | * | ||||
* @return array List of rule definitions | * @return array List of rule definitions | ||||
*/ | */ | ||||
public function rules(bool $all = false): array | public function rules(bool $all = false): array | ||||
{ | { | ||||
// All supported password policy rules (with default params) | // All supported password policy rules (with default params) | ||||
$supported = 'min:6,max:255,lower,upper,digit,special'; | $supported = 'min:6,max:255,lower,upper,digit,special,last:3'; | ||||
// Get the password policy from the $owner settings | // Get the password policy from the $owner settings | ||||
if ($this->owner) { | if ($this->owner) { | ||||
$conf = $this->owner->getSetting('password_policy'); | $conf = $this->owner->getSetting('password_policy'); | ||||
} | } | ||||
// Fallback to the configured policy | // Fallback to the configured policy | ||||
if (empty($conf)) { | if (empty($conf)) { | ||||
Show All 37 Lines | public static function parsePolicy(?string $policy): array | ||||
$policy = explode(',', strtolower((string) $policy)); | $policy = explode(',', strtolower((string) $policy)); | ||||
$policy = array_map('trim', $policy); | $policy = array_map('trim', $policy); | ||||
$policy = array_unique(array_filter($policy)); | $policy = array_unique(array_filter($policy)); | ||||
return self::mapWithKeys($policy); | return self::mapWithKeys($policy); | ||||
} | } | ||||
/** | /** | ||||
* Check password agains <count> of old passwords in user history | |||||
* | |||||
* @param string $password The password to check | |||||
* @param int $count Number of old passwords to check (including current one) | |||||
* | |||||
* @return bool True if password is unique, False otherwise | |||||
*/ | |||||
protected function checkPasswordHistory($password, int $count): bool | |||||
{ | |||||
$status = strlen($password) > 0; | |||||
mollekopf: ```
if (strlen($password) == 0) {
return false;
}
```
would be easier to read imo. | |||||
// Check if password is not the same as last X passwords | |||||
if ($status && $this->user && $count > 0) { | |||||
// Current password | |||||
if ($this->user->password) { | |||||
$count -= 1; | |||||
if (Hash::check($password, $this->user->password)) { | |||||
return false; | |||||
} | |||||
} | |||||
// Passwords from the history | |||||
if ($count > 0) { | |||||
$this->user->passwords()->latest()->limit($count)->get() | |||||
->each(function ($oldPassword) use (&$status, $password) { | |||||
if (Hash::check($password, $oldPassword->password)) { | |||||
$status = false; | |||||
return false; // stop iteration | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
return $status; | |||||
} | |||||
/** | |||||
* Convert an array with password policy rules into one indexed by the rule name | * Convert an array with password policy rules into one indexed by the rule name | ||||
* | * | ||||
* @param array $rules The rules list | * @param array $rules The rules list | ||||
* | * | ||||
* @return array | * @return array | ||||
*/ | */ | ||||
private static function mapWithKeys(array $rules): array | private static function mapWithKeys(array $rules): array | ||||
{ | { | ||||
Show All 16 Lines |
would be easier to read imo.