diff --git a/composer.json b/composer.json index 9099b614..a706ea16 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2 <8.1", - "nette/utils": "^3.1" + "nette/utils": "^3.2.1" }, "require-dev": { "nette/di": "^3.0.1", @@ -26,8 +26,7 @@ "phpstan/phpstan-nette": "^0.12" }, "conflict": { - "nette/di": "<3.0-stable", - "nette/schema": "<1.1" + "nette/di": "<3.0-stable" }, "autoload": { "classmap": ["src/"] diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 27cba4b2..e12fe8ad 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -42,8 +42,8 @@ public function getConfigSchema(): Nette\Schema\Schema ])->castTo('array') ) ), - 'roles' => Expect::arrayOf('string|array|null')->deprecated(), // role => parent(s) - 'resources' => Expect::arrayOf('string|null')->deprecated(), // resource => parent + 'roles' => Expect::arrayOf('string|array|null'), // role => parent(s) + 'resources' => Expect::arrayOf('string|null'), // resource => parent 'authentication' => Expect::structure([ 'storage' => Expect::anyOf('session', 'cookie')->default('session'), 'expiration' => Expect::string()->dynamic(), diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 4bdc034a..e613718b 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -21,6 +21,8 @@ final class CookieStorage implements Nette\Security\UserStorage { use Nette\SmartObject; + private const MIN_LENGTH = 13; + /** @var Http\IRequest */ private $request; @@ -49,9 +51,13 @@ public function __construct(Http\IRequest $request, Http\IResponse $response) public function saveAuthentication(IIdentity $identity): void { + $uid = (string) $identity->getId(); + if (strlen($uid) < self::MIN_LENGTH) { + throw new \LogicException('UID is too short.'); + } $this->response->setCookie( $this->cookieName, - $identity->getId(), + $uid, $this->cookieExpiration, null, $this->cookieDomain @@ -72,7 +78,7 @@ public function clearAuthentication(bool $clearIdentity): void public function getState(): array { $uid = $this->request->getCookie($this->cookieName); - $identity = is_string($uid) + $identity = is_string($uid) && strlen($uid) >= self::MIN_LENGTH ? new Nette\Security\SimpleIdentity($uid) : null; return [(bool) $identity, $identity, null]; diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index 0f904d26..2c27fc67 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -57,6 +57,9 @@ public function clearAuthentication(bool $clearIdentity): void $section->authenticated = false; $section->reason = self::LOGOUT_MANUAL; $section->authTime = null; + if ($clearIdentity === true) { + $section->identity = null; + } // Session Fixation defence $this->sessionHandler->regenerateId(); diff --git a/src/Security/Permission.php b/src/Security/Permission.php index f755ff7d..65670cfb 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -566,7 +566,7 @@ protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privile * the Role are checked. * * @param string|Role|null $role - * @param string|Resource|null $resource + * @param string|Nette\Security\Resource|null $resource * @param string|null $privilege * @throws Nette\InvalidStateException */ diff --git a/src/Security/User.php b/src/Security/User.php index 174fd378..77b68663 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -10,6 +10,7 @@ namespace Nette\Security; use Nette; +use Nette\Utils\Arrays; /** @@ -39,10 +40,10 @@ class User public $authenticatedRole = 'authenticated'; /** @var callable[] function (User $sender): void; Occurs when the user is successfully logged in */ - public $onLoggedIn; + public $onLoggedIn = []; /** @var callable[] function (User $sender): void; Occurs when the user is logged out */ - public $onLoggedOut; + public $onLoggedOut = []; /** @var UserStorage|IUserStorage Session storage for current user */ private $storage; @@ -103,7 +104,7 @@ public function login($user, string $password = null): void } else { $authenticator = $this->getAuthenticator(); $this->identity = $authenticator instanceof Authenticator - ? $authenticator->authenticate($user, $password) + ? $authenticator->authenticate(...func_get_args()) : $authenticator->authenticate(func_get_args()); } @@ -119,7 +120,7 @@ public function login($user, string $password = null): void $this->authenticated = true; $this->logoutReason = null; - $this->onLoggedIn($this); + Arrays::invoke($this->onLoggedIn, $this); } @@ -128,12 +129,7 @@ public function login($user, string $password = null): void */ final public function logout(bool $clearIdentity = false): void { - if ($this->isLoggedIn()) { - $this->onLoggedOut($this); - } - - $this->authenticated = false; - $this->identity = $clearIdentity ? null : $this->identity; + $logged = $this->isLoggedIn(); if ($this->storage instanceof UserStorage) { $this->storage->clearAuthentication($clearIdentity); @@ -142,8 +138,14 @@ final public function logout(bool $clearIdentity = false): void if ($clearIdentity) { $this->storage->setIdentity(null); } - $this->logoutReason = self::MANUAL; } + + $this->authenticated = false; + $this->logoutReason = self::MANUAL; + if ($logged) { + Arrays::invoke($this->onLoggedOut, $this); + } + $this->identity = $clearIdentity ? null : $this->identity; } @@ -311,7 +313,12 @@ public function getRoles(): array */ final public function isInRole(string $role): bool { - return in_array($role, $this->getRoles(), true); + foreach ($this->getRoles() as $r) { + if ($role === ($r instanceof Role ? $r->getRoleId() : $r)) { + return true; + } + } + return false; } diff --git a/tests/Security.DI/SecurityExtension.authorizator.phpt b/tests/Security.DI/SecurityExtension.authorizator.phpt index d898082c..0f8b3b7c 100644 --- a/tests/Security.DI/SecurityExtension.authorizator.phpt +++ b/tests/Security.DI/SecurityExtension.authorizator.phpt @@ -32,7 +32,7 @@ security: article: item ', 'neon')); -@eval($compiler->addConfig($config)->compile()); // @ is deprecated +eval($compiler->addConfig($config)->compile()); $container = new Container; $authorizator = $container->getService('security.authorizator'); diff --git a/tests/Security/User.authorization.phpt b/tests/Security/User.authorization.phpt index 6c9a17f8..9061096d 100644 --- a/tests/Security/User.authorization.phpt +++ b/tests/Security/User.authorization.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Security\IIdentity; +use Nette\Security\Role; use Nette\Security\SimpleIdentity; use Tester\Assert; @@ -31,7 +32,7 @@ class Authenticator implements Nette\Security\Authenticator throw new Nette\Security\AuthenticationException('Password not match', self::INVALID_CREDENTIAL); } else { - return new SimpleIdentity('John Doe', 'admin'); + return new SimpleIdentity('John Doe', ['admin', new TesterRole]); } } } @@ -45,6 +46,13 @@ class Authorizator implements Nette\Security\Authorizator } } +class TesterRole implements Role +{ + public function getRoleId(): string + { + return 'tester'; + } +} $user = new Nette\Security\User(null, null, null, new MockUserStorage); @@ -54,6 +62,7 @@ Assert::false($user->isLoggedIn()); Assert::same(['guest'], $user->getRoles()); Assert::false($user->isInRole('admin')); +Assert::false($user->isInRole('tester')); Assert::true($user->isInRole('guest')); @@ -65,8 +74,9 @@ $user->setAuthenticator($handler); $user->login('john', 'xxx'); Assert::true($user->isLoggedIn()); -Assert::same(['admin'], $user->getRoles()); +Assert::equal(['admin', new TesterRole], $user->getRoles()); Assert::true($user->isInRole('admin')); +Assert::true($user->isInRole('tester')); Assert::false($user->isInRole('guest'));