tao-test/app/tao/models/classes/user/implementation/UserLocksService.php

334 lines
9.2 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2018 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*/
namespace oat\tao\model\user\implementation;
use core_kernel_persistence_Exception;
use core_kernel_users_Exception;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Exception;
use oat\oatbox\service\ConfigurableService;
use oat\oatbox\user\User;
use oat\tao\helpers\UserHelper;
use oat\tao\model\event\LoginFailedEvent;
use oat\tao\model\event\LoginSucceedEvent;
use oat\tao\model\TaoOntology;
use oat\tao\model\user\LockoutStorage;
use oat\tao\model\user\UserLocks;
use tao_helpers_Date;
use tao_models_classes_UserService;
/**
* Class UserLocksService
* @package oat\tao\model\user
*/
class UserLocksService extends ConfigurableService implements UserLocks
{
/** Which storage implementation to use for user lockouts */
public const OPTION_LOCKOUT_STORAGE = 'lockout_storage';
/** @var LockoutStorage */
private $lockout;
public function setHardLockout()
{
$this->setOption(self::OPTION_USE_HARD_LOCKOUT, true);
}
public function setSoftLockout()
{
$this->setOption(self::OPTION_USE_HARD_LOCKOUT, false);
}
public function setFailedAttemptsBeforeLockout($attempts = 6)
{
$this->setOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS, $attempts);
}
public function setPeriodOfSoftLockout($period = 'PT30M')
{
$this->setOption(self::OPTION_SOFT_LOCKOUT_PERIOD, $period);
}
public function setLockStorage($implementation = RdfLockoutStorage::class)
{
$this->setOption(self::OPTION_LOCKOUT_STORAGE, $implementation);
}
public function setNonLockingRoles(array $roles)
{
$this->setOption(self::OPTION_NON_LOCKING_ROLES, $roles);
}
/**
* Returns proper lockout implementation
* @return LockoutStorage|RdfLockoutStorage
*/
protected function getLockout()
{
if (!$this->lockout || !$this->lockout instanceof LockoutStorage) {
$lockout = $this->getOption(self::OPTION_LOCKOUT_STORAGE);
$this->lockout = ($lockout and class_exists($lockout)) ? new $lockout() : new RdfLockoutStorage();
}
return $this->lockout;
}
/**
* @param LoginFailedEvent $event
* @throws core_kernel_users_Exception
*/
public function catchFailedLogin(LoginFailedEvent $event)
{
$this->increaseLoginFails($event->getLogin());
}
/**
* @param LoginSucceedEvent $event
* @throws core_kernel_users_Exception
*/
public function catchSucceedLogin(LoginSucceedEvent $event)
{
$this->unlockUser(UserHelper::getUser($this->getLockout()->getUser($event->getLogin())));
}
/**
* @param string $login
*
* @throws core_kernel_users_Exception
* @throws Exception
*/
private function increaseLoginFails($login)
{
$user = UserHelper::getUser($this->getLockout()->getUser($login));
/** @var DateInterval $remaining */
$remaining = $this->getLockoutRemainingTime($login);
if ($remaining && !$remaining->invert) {
$failures = $this->getLockout()->getFailures($login);
} else {
$this->unlockUser($user);
$failures = 0;
}
$failures++;
if ($failures >= (int)$this->getOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS)) {
$this->lockUser($user);
}
$this->getLockout()->setFailures($login, $failures);
}
/**
* @param User $user
* @return bool|mixed
* @throws core_kernel_users_Exception
*/
public function lockUser(User $user)
{
$currentUser = tao_models_classes_UserService::singleton()->getCurrentUser();
$blocker = $currentUser ? UserHelper::getUser($currentUser) : $user;
if (!$this->isLockable($user)) {
return false;
}
$this->getLockout()->setLockedStatus(
UserHelper::getUserLogin($user),
$blocker->getIdentifier()
);
return true;
}
/**
* @param $user
*
* @return bool
* @throws core_kernel_users_Exception
*/
public function unlockUser(User $user)
{
$login = UserHelper::getUserLogin($user);
$this->getLockout()->setUnlockedStatus($login);
$this->getLockout()->setFailures($login, 0);
return true;
}
/**
* @param $login
*
* @return bool
* @throws core_kernel_users_Exception
* @throws Exception
*/
public function isLocked($login)
{
$status = $this->getLockout()->getStatus($login);
if (empty($status)) {
return false;
}
// hard lockout, only admin can reset
if ($status && $this->getOption(self::OPTION_USE_HARD_LOCKOUT)) {
return true;
}
$lockedBy = UserHelper::getUser($this->getLockout()->getLockedBy($login));
$user = UserHelper::getUser($this->getLockout()->getUser($login));
if ($lockedBy === null) {
return false;
}
if ($lockedBy->getIdentifier() !== $user->getIdentifier()) {
return true;
}
$lockoutPeriod = new DateInterval($this->getOption(self::OPTION_SOFT_LOCKOUT_PERIOD));
$lastFailureTime = (new DateTimeImmutable())->setTimestamp(
(int)(string)$this->getLockout()->getLastFailureTime($login)
);
return $lastFailureTime->add($lockoutPeriod) > new DateTimeImmutable();
}
/**
* @param $user
* @return bool
*/
public function isLockable(User $user)
{
if ($user->getIdentifier() === LOCAL_NAMESPACE . TaoOntology::DEFAULT_USER_URI_SUFFIX) {
return false;
}
$nonLockingRoles = $this->getOption(self::OPTION_NON_LOCKING_ROLES);
if ($nonLockingRoles && is_array($nonLockingRoles) && count($nonLockingRoles)) {
return !count(array_intersect($user->getRoles(), $nonLockingRoles));
}
return true;
}
/**
* @param $login
* @return bool|DateInterval
* @throws Exception
*/
public function getLockoutRemainingTime($login)
{
$lastFailure = $this->getLockout()->getLastFailureTime($login);
if (!$lastFailure) {
return false;
}
$unlockTime = (new DateTime('now'))
->setTimestamp($lastFailure->literal)
->add(new DateInterval($this->getOption(self::OPTION_SOFT_LOCKOUT_PERIOD)));
return (new DateTime('now'))->diff($unlockTime);
}
/**
* @param $login
*
* @return bool|int|mixed
* @throws core_kernel_users_Exception
* @throws core_kernel_persistence_Exception
*/
public function getLockoutRemainingAttempts($login)
{
$user = UserHelper::getUser($this->getLockout()->getUser($login));
if (!$this->isLockable($user)) {
return false;
}
$allowedAttempts = $this->getOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS);
$failedAttempts = $this->getLockout()->getFailures($login);
$rest = $allowedAttempts - $failedAttempts;
if ($rest < 0) {
return false;
}
return $rest;
}
/**
* @param $login
*
* @return array
* @throws Exception
*/
public function getStatusDetails($login)
{
$user = UserHelper::getUser($this->getLockout()->getUser($login));
$isLocked = $this->isLocked($login);
if (!$isLocked) {
$this->unlockUser($user);
return [
'locked' => false,
'auto' => false,
'status' => __('enabled'),
'remaining' => null,
'lockable' => $this->isLockable($user)
];
}
$remaining = $this->getLockoutRemainingTime($login);
$lockedBy = UserHelper::getUser($this->getLockout()->getLockedBy($login));
$autoLocked = false;
if ($lockedBy->getIdentifier() !== $user->getIdentifier()) {
$status = __('locked by %s', UserHelper::getUserLogin($lockedBy));
} else {
$autoLocked = true;
$status = $this->getOption(self::OPTION_USE_HARD_LOCKOUT)
? __('self-locked')
: __('auto unlocked in %s', tao_helpers_Date::displayInterval($remaining));
}
return [
'locked' => $isLocked,
'auto' => $autoLocked,
'status' => $status,
'remaining' => $remaining,
'lockable' => $this->isLockable($user)
];
}
}