Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserLocksService
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 17
1722
0.00% covered (danger)
0.00%
0 / 1
 setHardLockout
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSoftLockout
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setFailedAttemptsBeforeLockout
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPeriodOfSoftLockout
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLockStorage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setNonLockingRoles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLockout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
 catchFailedLogin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 catchSucceedLogin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 increaseLoginFails
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 lockUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 unlockUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 isLocked
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 isLockable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getLockoutRemainingTime
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getLockoutRemainingAttempts
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getStatusDetails
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2018 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 */
20
21namespace oat\tao\model\user\implementation;
22
23use core_kernel_persistence_Exception;
24use core_kernel_users_Exception;
25use DateInterval;
26use DateTime;
27use DateTimeImmutable;
28use Exception;
29use oat\oatbox\service\ConfigurableService;
30use oat\oatbox\user\User;
31use oat\tao\helpers\UserHelper;
32use oat\tao\model\event\LoginFailedEvent;
33use oat\tao\model\event\LoginSucceedEvent;
34use oat\tao\model\TaoOntology;
35use oat\tao\model\user\LockoutStorage;
36use oat\tao\model\user\UserLocks;
37use tao_helpers_Date;
38use tao_models_classes_UserService;
39
40/**
41 * Class UserLocksService
42 * @package oat\tao\model\user
43 */
44class UserLocksService extends ConfigurableService implements UserLocks
45{
46    /** Which storage implementation to use for user lockouts */
47    public const OPTION_LOCKOUT_STORAGE = 'lockout_storage';
48
49    /** @var LockoutStorage */
50    private $lockout;
51
52    public function setHardLockout()
53    {
54        $this->setOption(self::OPTION_USE_HARD_LOCKOUT, true);
55    }
56
57    public function setSoftLockout()
58    {
59        $this->setOption(self::OPTION_USE_HARD_LOCKOUT, false);
60    }
61
62    public function setFailedAttemptsBeforeLockout($attempts = 6)
63    {
64        $this->setOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS, $attempts);
65    }
66
67    public function setPeriodOfSoftLockout($period = 'PT30M')
68    {
69        $this->setOption(self::OPTION_SOFT_LOCKOUT_PERIOD, $period);
70    }
71
72    public function setLockStorage($implementation = RdfLockoutStorage::class)
73    {
74        $this->setOption(self::OPTION_LOCKOUT_STORAGE, $implementation);
75    }
76
77    public function setNonLockingRoles(array $roles)
78    {
79        $this->setOption(self::OPTION_NON_LOCKING_ROLES, $roles);
80    }
81
82    /**
83     * Returns proper lockout implementation
84     * @return LockoutStorage|RdfLockoutStorage
85     */
86    protected function getLockout()
87    {
88        if (!$this->lockout || !$this->lockout instanceof LockoutStorage) {
89            $lockout = $this->getOption(self::OPTION_LOCKOUT_STORAGE);
90            $this->lockout = ($lockout and class_exists($lockout)) ? new $lockout() : new RdfLockoutStorage();
91        }
92
93        return $this->lockout;
94    }
95
96    /**
97     * @param LoginFailedEvent $event
98     * @throws core_kernel_users_Exception
99     */
100    public function catchFailedLogin(LoginFailedEvent $event)
101    {
102        $this->increaseLoginFails($event->getLogin());
103    }
104
105    /**
106     * @param LoginSucceedEvent $event
107     * @throws core_kernel_users_Exception
108     */
109    public function catchSucceedLogin(LoginSucceedEvent $event)
110    {
111        $this->unlockUser(UserHelper::getUser($this->getLockout()->getUser($event->getLogin())));
112    }
113
114    /**
115     * @param string $login
116     *
117     * @throws core_kernel_users_Exception
118     * @throws Exception
119     */
120    private function increaseLoginFails($login)
121    {
122        $user = UserHelper::getUser($this->getLockout()->getUser($login));
123
124        /** @var DateInterval $remaining */
125        $remaining = $this->getLockoutRemainingTime($login);
126
127        if ($remaining && !$remaining->invert) {
128            $failures = $this->getLockout()->getFailures($login);
129        } else {
130            $this->unlockUser($user);
131            $failures = 0;
132        }
133
134        $failures++;
135
136        if ($failures >= (int)$this->getOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS)) {
137            $this->lockUser($user);
138        }
139
140        $this->getLockout()->setFailures($login, $failures);
141    }
142
143    /**
144     * @param User $user
145     * @return bool|mixed
146     * @throws core_kernel_users_Exception
147     */
148    public function lockUser(User $user)
149    {
150        $currentUser = tao_models_classes_UserService::singleton()->getCurrentUser();
151
152        $blocker = $currentUser ? UserHelper::getUser($currentUser) : $user;
153
154        if (!$this->isLockable($user)) {
155            return false;
156        }
157
158        $this->getLockout()->setLockedStatus(
159            UserHelper::getUserLogin($user),
160            $blocker->getIdentifier()
161        );
162
163        return true;
164    }
165
166    /**
167     * @param $user
168     *
169     * @return bool
170     * @throws core_kernel_users_Exception
171     */
172    public function unlockUser(User $user)
173    {
174        $login = UserHelper::getUserLogin($user);
175
176        $this->getLockout()->setUnlockedStatus($login);
177        $this->getLockout()->setFailures($login, 0);
178
179        return true;
180    }
181
182    /**
183     * @param $login
184     *
185     * @return bool
186     * @throws core_kernel_users_Exception
187     * @throws Exception
188     */
189    public function isLocked($login)
190    {
191        $status = $this->getLockout()->getStatus($login);
192
193        if (empty($status)) {
194            return false;
195        }
196
197        // hard lockout, only admin can reset
198        if ($status && $this->getOption(self::OPTION_USE_HARD_LOCKOUT)) {
199            return true;
200        }
201
202        $lockedBy = UserHelper::getUser($this->getLockout()->getLockedBy($login));
203        $user = UserHelper::getUser($this->getLockout()->getUser($login));
204
205        if ($lockedBy === null) {
206            return false;
207        }
208
209        if ($lockedBy->getIdentifier() !== $user->getIdentifier()) {
210            return true;
211        }
212
213        $lockoutPeriod = new DateInterval($this->getOption(self::OPTION_SOFT_LOCKOUT_PERIOD));
214        $lastFailureTime = (new DateTimeImmutable())->setTimestamp(
215            (int)(string)$this->getLockout()->getLastFailureTime($login)
216        );
217
218        return $lastFailureTime->add($lockoutPeriod) > new DateTimeImmutable();
219    }
220
221    /**
222     * @param $user
223     * @return bool
224     */
225    public function isLockable(User $user)
226    {
227        if ($user->getIdentifier() === LOCAL_NAMESPACE . TaoOntology::DEFAULT_USER_URI_SUFFIX) {
228            return false;
229        }
230
231        $nonLockingRoles = $this->getOption(self::OPTION_NON_LOCKING_ROLES);
232
233        if ($nonLockingRoles && is_array($nonLockingRoles) && count($nonLockingRoles)) {
234            return !count(array_intersect($user->getRoles(), $nonLockingRoles));
235        }
236
237        return true;
238    }
239
240    /**
241     * @param $login
242     * @return bool|DateInterval
243     * @throws Exception
244     */
245    public function getLockoutRemainingTime($login)
246    {
247        $lastFailure = $this->getLockout()->getLastFailureTime($login);
248
249        if (!$lastFailure) {
250            return false;
251        }
252
253        $unlockTime = (new DateTime('now'))
254            ->setTimestamp($lastFailure->literal)
255            ->add(new DateInterval($this->getOption(self::OPTION_SOFT_LOCKOUT_PERIOD)));
256
257        return (new DateTime('now'))->diff($unlockTime);
258    }
259
260    /**
261     * @param $login
262     *
263     * @return bool|int|mixed
264     * @throws core_kernel_users_Exception
265     * @throws core_kernel_persistence_Exception
266     */
267    public function getLockoutRemainingAttempts($login)
268    {
269        $user = UserHelper::getUser($this->getLockout()->getUser($login));
270
271        if (!$this->isLockable($user)) {
272            return false;
273        }
274
275        $allowedAttempts = $this->getOption(self::OPTION_LOCKOUT_FAILED_ATTEMPTS);
276        $failedAttempts = $this->getLockout()->getFailures($login);
277
278        $rest = $allowedAttempts - $failedAttempts;
279
280        if ($rest < 0) {
281            return false;
282        }
283
284        return $rest;
285    }
286
287    /**
288     * @param $login
289     *
290     * @return array
291     * @throws Exception
292     */
293    public function getStatusDetails($login)
294    {
295        $user = UserHelper::getUser($this->getLockout()->getUser($login));
296
297        $isLocked = $this->isLocked($login);
298
299        if (!$isLocked) {
300            $this->unlockUser($user);
301
302            return [
303                'locked' => false,
304                'auto' => false,
305                'status' => __('enabled'),
306                'remaining' => null,
307                'lockable' => $this->isLockable($user)
308            ];
309        }
310
311        $remaining = $this->getLockoutRemainingTime($login);
312        $lockedBy = UserHelper::getUser($this->getLockout()->getLockedBy($login));
313
314        $autoLocked = false;
315
316        if ($lockedBy->getIdentifier() !== $user->getIdentifier()) {
317            $status = __('locked by %s', UserHelper::getUserLogin($lockedBy));
318        } else {
319            $autoLocked = true;
320            $status = $this->getOption(self::OPTION_USE_HARD_LOCKOUT)
321                ? __('self-locked')
322                : __('auto unlocked in %s', tao_helpers_Date::displayInterval($remaining));
323        }
324
325        return [
326            'locked' => $isLocked,
327            'auto' => $autoLocked,
328            'status' => $status,
329            'remaining' => $remaining,
330            'lockable' => $this->isLockable($user)
331        ];
332    }
333}