From 1acee37ba00032c6fb04f94a65cdfd74bcd7a6f3 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 1 Mar 2021 15:30:34 +0100 Subject: [PATCH 01/53] opened 3.2-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16f8e372..8a0e94ca 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } } } From e9d3e31a3eedd18729d08eb95a6f1ec3d4975828 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 1 Mar 2021 17:38:22 +0100 Subject: [PATCH 02/53] requires PHP 8.1 --- .github/workflows/coding-style.yml | 4 ++-- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 6 +++--- composer.json | 2 +- readme.md | 2 +- tests/Security/Passwords.hash().phpt | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index d8020f6f..97895fbe 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.2 + php-version: 8.1 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.1 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 08f1414a..d385b9c2 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.1 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf1784de..144b3690 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3'] fail-fast: false @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.2 + php-version: 8.1 coverage: none - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.1 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/composer.json b/composer.json index 8a0e94ca..36846405 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "7.2 - 8.3", + "php": "8.1 - 8.3", "nette/utils": "^3.2.1 || ~4.0.0" }, "require-dev": { diff --git a/readme.md b/readme.md index c414ed90..47b2f6d3 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ Authentication & Authorization library for Nette. Documentation can be found on the [website](https://doc.nette.org/access-control). -It requires PHP version 7.2 and supports PHP up to 8.3. +It requires PHP version 8.1 and supports PHP up to 8.3. [Support Me](https://github.com/sponsors/dg) diff --git a/tests/Security/Passwords.hash().phpt b/tests/Security/Passwords.hash().phpt index c8b7d376..9aa99303 100644 --- a/tests/Security/Passwords.hash().phpt +++ b/tests/Security/Passwords.hash().phpt @@ -26,7 +26,7 @@ Assert::same($hash, crypt('dg', $hash)); Assert::exception(function () { (new Passwords(PASSWORD_BCRYPT, ['cost' => 3]))->hash('dg'); -}, PHP_VERSION_ID < 80000 ? Nette\InvalidStateException::class : ValueError::class); +}, ValueError::class); Assert::exception(function () { (new Passwords)->hash(''); From b9d064017cc9ab985ca619a8f51a7104eeb54439 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 1 Mar 2021 16:43:32 +0100 Subject: [PATCH 03/53] composer: updated dependencies --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 36846405..2f352bdc 100644 --- a/composer.json +++ b/composer.json @@ -16,15 +16,15 @@ ], "require": { "php": "8.1 - 8.3", - "nette/utils": "^3.2.1 || ~4.0.0" + "nette/utils": "^4.0" }, "require-dev": { - "nette/di": "^3.0.1", - "nette/http": "^3.0.0", - "nette/tester": "^2.0", - "tracy/tracy": "^2.4", + "nette/di": "^3.1", + "nette/http": "^3.2", + "nette/tester": "^2.5", + "tracy/tracy": "^2.9", "phpstan/phpstan-nette": "^1.0", - "mockery/mockery": "^1.3.6" + "mockery/mockery": "^1.5" }, "conflict": { "nette/di": "<3.0-stable", From 69df236333dcaf85fbb470613cca03156dd11779 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Dec 2021 18:12:40 +0100 Subject: [PATCH 04/53] coding style --- src/Bridges/SecurityDI/SecurityExtension.php | 4 ++-- src/Bridges/SecurityHttp/CookieStorage.php | 6 +++--- src/Security/Passwords.php | 4 ++-- src/Security/Permission.php | 14 +++++++------- src/Security/SimpleAuthenticator.php | 4 ++-- src/Security/User.php | 4 ++-- tests/Security.Http/CookieStorage.getState.phpt | 6 +++--- tests/Security/Passwords.hash().phpt | 4 ++-- tests/Security/Permission.ResourceInherits.phpt | 6 +++--- .../Security/Permission.RoleRegistryInherits.phpt | 6 +++--- tests/Security/User.authorization.phpt | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 0013c279..cf762ef3 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -40,8 +40,8 @@ public function getConfigSchema(): Nette\Schema\Schema 'password' => Expect::string(), 'roles' => Expect::anyOf(Expect::string(), Expect::listOf('string')), 'data' => Expect::array(), - ])->castTo('array') - ) + ])->castTo('array'), + ), ), 'roles' => Expect::arrayOf('string|array|null'), // role => parent(s) 'resources' => Expect::arrayOf('string|null'), // resource => parent diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 12764efd..48ba18a2 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -68,7 +68,7 @@ public function saveAuthentication(IIdentity $identity): void $this->cookieDomain, null, true, - $this->cookieSameSite + $this->cookieSameSite, ); } @@ -79,7 +79,7 @@ public function clearAuthentication(bool $clearIdentity): void $this->response->deleteCookie( $this->cookieName, null, - $this->cookieDomain + $this->cookieDomain, ); } @@ -106,7 +106,7 @@ public function setExpiration(?string $expire, bool $clearIdentity): void public function setCookieParameters( ?string $name = null, ?string $domain = null, - ?string $sameSite = null + ?string $sameSite = null, ) { $this->cookieName = $name ?? $this->cookieName; $this->cookieDomain = $domain ?? $this->cookieDomain; diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 476514b3..4c87a06a 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -42,7 +42,7 @@ public function __construct($algo = PASSWORD_DEFAULT, array $options = []) */ public function hash( #[\SensitiveParameter] - string $password + string $password, ): string { if ($password === '') { @@ -64,7 +64,7 @@ public function hash( public function verify( #[\SensitiveParameter] string $password, - string $hash + string $hash, ): bool { return password_verify($password, $hash); diff --git a/src/Security/Permission.php b/src/Security/Permission.php index d1a801bc..0a6b9e42 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -61,7 +61,7 @@ class Permission implements Authorizator */ public function addRole(string $role, $parents = null) { - $this->checkRole($role, false); + $this->checkRole($role, exists: false); if (isset($this->roles[$role])) { throw new Nette\InvalidStateException("Role '$role' already exists in the list."); } @@ -94,7 +94,7 @@ public function addRole(string $role, $parents = null) */ public function hasRole(string $role): bool { - $this->checkRole($role, false); + $this->checkRole($role, exists: false); return isset($this->roles[$role]); } @@ -234,7 +234,7 @@ public function removeAllRoles() */ public function addResource(string $resource, ?string $parent = null) { - $this->checkResource($resource, false); + $this->checkResource($resource, exists: false); if (isset($this->resources[$resource])) { throw new Nette\InvalidStateException("Resource '$resource' already exists in the list."); @@ -259,7 +259,7 @@ public function addResource(string $resource, ?string $parent = null) */ public function hasResource(string $resource): bool { - $this->checkResource($resource, false); + $this->checkResource($resource, exists: false); return isset($this->resources[$resource]); } @@ -391,7 +391,7 @@ public function allow( $roles = self::All, $resources = self::All, $privileges = self::All, - ?callable $assertion = null + ?callable $assertion = null, ) { $this->setRule(true, self::Allow, $roles, $resources, $privileges, $assertion); return $this; @@ -411,7 +411,7 @@ public function deny( $roles = self::All, $resources = self::All, $privileges = self::All, - ?callable $assertion = null + ?callable $assertion = null, ) { $this->setRule(true, self::Deny, $roles, $resources, $privileges, $assertion); return $this; @@ -497,7 +497,7 @@ protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privile if ($toAdd) { // add to the rules foreach ($resources as $resource) { foreach ($roles as $role) { - $rules = &$this->getRules($resource, $role, true); + $rules = &$this->getRules($resource, $role, create: true); if (count($privileges) === 0) { $rules['allPrivileges']['type'] = $type; $rules['allPrivileges']['assert'] = $assertion; diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index cbf2aae8..b1e05bba 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -38,7 +38,7 @@ public function __construct( #[\SensitiveParameter] array $passwords, array $roles = [], - array $data = [] + array $data = [], ) { $this->passwords = $passwords; $this->roles = $roles; @@ -54,7 +54,7 @@ public function __construct( public function authenticate( string $username, #[\SensitiveParameter] - string $password + string $password, ): IIdentity { foreach ($this->passwords as $name => $pass) { diff --git a/src/Security/User.php b/src/Security/User.php index 77ad9f9d..64fae555 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -76,7 +76,7 @@ public function __construct( ?IUserStorage $legacyStorage = null, ?IAuthenticator $authenticator = null, ?Authorizator $authorizator = null, - ?UserStorage $storage = null + ?UserStorage $storage = null, ) { $this->storage = $storage ?? $legacyStorage; // back compatibility if (!$this->storage) { @@ -108,7 +108,7 @@ final public function getStorage() public function login( $user, #[\SensitiveParameter] - ?string $password = null + ?string $password = null, ): void { $this->logout(true); diff --git a/tests/Security.Http/CookieStorage.getState.phpt b/tests/Security.Http/CookieStorage.getState.phpt index 148a0102..ba4d0961 100644 --- a/tests/Security.Http/CookieStorage.getState.phpt +++ b/tests/Security.Http/CookieStorage.getState.phpt @@ -15,18 +15,18 @@ $storage = new CookieStorage($request, $response); Assert::same([false, null, null], $storage->getState()); // short id -$request = new Nette\Http\Request(new Nette\Http\UrlScript, [], [], ['userid' => 'short']); +$request = new Nette\Http\Request(new Nette\Http\UrlScript, cookies: ['userid' => 'short']); $storage = new CookieStorage($request, $response); Assert::same([false, null, null], $storage->getState()); // correct id $id = '123456789123456'; -$request = new Nette\Http\Request(new Nette\Http\UrlScript, [], [], ['userid' => $id]); +$request = new Nette\Http\Request(new Nette\Http\UrlScript, cookies: ['userid' => $id]); $storage = new CookieStorage($request, $response); Assert::equal([true, new SimpleIdentity($id), null], $storage->getState()); // custom cookie -$request = new Nette\Http\Request(new Nette\Http\UrlScript, [], [], ['foo' => $id]); +$request = new Nette\Http\Request(new Nette\Http\UrlScript, cookies: ['foo' => $id]); $storage = new CookieStorage($request, $response); $storage->setCookieParameters('foo'); Assert::equal([true, new SimpleIdentity($id), null], $storage->getState()); diff --git a/tests/Security/Passwords.hash().phpt b/tests/Security/Passwords.hash().phpt index 9aa99303..f091a67c 100644 --- a/tests/Security/Passwords.hash().phpt +++ b/tests/Security/Passwords.hash().phpt @@ -14,11 +14,11 @@ require __DIR__ . '/../bootstrap.php'; Assert::truthy( - preg_match('#^\$.{50,}\z#', (new Passwords)->hash('my-password')) + preg_match('#^\$.{50,}\z#', (new Passwords)->hash('my-password')), ); Assert::truthy( - preg_match('#^\$2y\$05\$.{53}\z#', (new Passwords(PASSWORD_BCRYPT, ['cost' => 5]))->hash('dg')) + preg_match('#^\$2y\$05\$.{53}\z#', (new Passwords(PASSWORD_BCRYPT, ['cost' => 5]))->hash('dg')), ); $hash = (new Passwords(PASSWORD_BCRYPT))->hash('dg'); diff --git a/tests/Security/Permission.ResourceInherits.phpt b/tests/Security/Permission.ResourceInherits.phpt index 2512733c..38c348d1 100644 --- a/tests/Security/Permission.ResourceInherits.phpt +++ b/tests/Security/Permission.ResourceInherits.phpt @@ -19,10 +19,10 @@ $acl->addResource('building', 'city'); $acl->addResource('room', 'building'); Assert::same(['city', 'building', 'room'], $acl->getResources()); -Assert::true($acl->resourceInheritsFrom('building', 'city', true)); -Assert::true($acl->resourceInheritsFrom('room', 'building', true)); +Assert::true($acl->resourceInheritsFrom('building', 'city', onlyParent: true)); +Assert::true($acl->resourceInheritsFrom('room', 'building', onlyParent: true)); Assert::true($acl->resourceInheritsFrom('room', 'city')); -Assert::false($acl->resourceInheritsFrom('room', 'city', true)); +Assert::false($acl->resourceInheritsFrom('room', 'city', onlyParent: true)); Assert::false($acl->resourceInheritsFrom('city', 'building')); Assert::false($acl->resourceInheritsFrom('building', 'room')); Assert::false($acl->resourceInheritsFrom('city', 'room')); diff --git a/tests/Security/Permission.RoleRegistryInherits.phpt b/tests/Security/Permission.RoleRegistryInherits.phpt index aaa19373..ddb25de1 100644 --- a/tests/Security/Permission.RoleRegistryInherits.phpt +++ b/tests/Security/Permission.RoleRegistryInherits.phpt @@ -23,10 +23,10 @@ Assert::same(['guest'], $acl->getRoleParents('member')); Assert::same(['member'], $acl->getRoleParents('editor')); -Assert::true($acl->roleInheritsFrom('member', 'guest', true)); -Assert::true($acl->roleInheritsFrom('editor', 'member', true)); +Assert::true($acl->roleInheritsFrom('member', 'guest', onlyParents: true)); +Assert::true($acl->roleInheritsFrom('editor', 'member', onlyParents: true)); Assert::true($acl->roleInheritsFrom('editor', 'guest')); -Assert::false($acl->roleInheritsFrom('editor', 'guest', true)); +Assert::false($acl->roleInheritsFrom('editor', 'guest', onlyParents: true)); Assert::false($acl->roleInheritsFrom('guest', 'member')); Assert::false($acl->roleInheritsFrom('member', 'editor')); Assert::false($acl->roleInheritsFrom('guest', 'editor')); diff --git a/tests/Security/User.authorization.phpt b/tests/Security/User.authorization.phpt index a0c48ab2..e093ee56 100644 --- a/tests/Security/User.authorization.phpt +++ b/tests/Security/User.authorization.phpt @@ -42,7 +42,7 @@ class Authorizator implements Nette\Security\Authorizator { public function isAllowed($role = self::All, $resource = self::All, $privilege = self::All): bool { - return $role === 'admin' && strpos($resource, 'jany') === false; + return $role === 'admin' && !str_contains($resource, 'jany'); } } From 29b92a60a29bcfa9b7bea871b4a745ce39e0ec2e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 7 Apr 2022 18:04:32 +0200 Subject: [PATCH 05/53] coding style: reformatted Assert::exception --- .../CookieStorage.authentication.phpt | 7 +++-- tests/Security/Passwords.hash().phpt | 15 ++++++---- .../Permission.IsAllowedNonExistent.phpt | 22 ++++++++------ ...ission.ResourceAddInheritsNonExistent.phpt | 8 +++-- .../Permission.ResourceDuplicate.phpt | 12 ++++---- ...ermission.ResourceInheritsNonExistent.phpt | 18 +++++++----- ...rmission.ResourceRemoveOneNonExistent.phpt | 8 +++-- ...on.RoleRegistryAddInheritsNonExistent.phpt | 8 +++-- .../Permission.RoleRegistryDuplicate.phpt | 10 ++++--- ...ssion.RoleRegistryInheritsNonExistent.phpt | 18 +++++++----- ...sion.RoleRegistryRemoveOneNonExistent.phpt | 8 +++-- tests/Security/Permission.RuleRoleRemove.phpt | 8 +++-- .../Permission.RuleRoleRemoveAll.phpt | 8 +++-- .../Permission.RulesResourceRemove.phpt | 8 +++-- .../Permission.RulesResourceRemoveAll.phpt | 8 +++-- tests/Security/SimpleAuthenticator.phpt | 18 +++++++----- tests/Security/User.authentication.phpt | 29 ++++++++++--------- tests/Security/User.authorization.phpt | 8 +++-- 18 files changed, 133 insertions(+), 88 deletions(-) diff --git a/tests/Security.Http/CookieStorage.authentication.phpt b/tests/Security.Http/CookieStorage.authentication.phpt index 8debb1ae..f88f01a6 100644 --- a/tests/Security.Http/CookieStorage.authentication.phpt +++ b/tests/Security.Http/CookieStorage.authentication.phpt @@ -12,9 +12,10 @@ require __DIR__ . '/../bootstrap.php'; $request = new Nette\Http\Request(new Nette\Http\UrlScript); $response = Mockery::mock(Nette\Http\IResponse::class); $storage = new CookieStorage($request, $response); -Assert::exception(function () use ($storage) { - $storage->saveAuthentication(new SimpleIdentity('short')); -}, LogicException::class); +Assert::exception( + fn() => $storage->saveAuthentication(new SimpleIdentity('short')), + LogicException::class, +); // correct id $id = '123456789123456'; diff --git a/tests/Security/Passwords.hash().phpt b/tests/Security/Passwords.hash().phpt index f091a67c..7baf9afe 100644 --- a/tests/Security/Passwords.hash().phpt +++ b/tests/Security/Passwords.hash().phpt @@ -24,10 +24,13 @@ Assert::truthy( $hash = (new Passwords(PASSWORD_BCRYPT))->hash('dg'); Assert::same($hash, crypt('dg', $hash)); -Assert::exception(function () { - (new Passwords(PASSWORD_BCRYPT, ['cost' => 3]))->hash('dg'); -}, ValueError::class); +Assert::exception( + fn() => (new Passwords(PASSWORD_BCRYPT, ['cost' => 3]))->hash('dg'), + ValueError::class, +); -Assert::exception(function () { - (new Passwords)->hash(''); -}, Nette\InvalidArgumentException::class, 'Password can not be empty.'); +Assert::exception( + fn() => (new Passwords)->hash(''), + Nette\InvalidArgumentException::class, + 'Password can not be empty.', +); diff --git a/tests/Security/Permission.IsAllowedNonExistent.phpt b/tests/Security/Permission.IsAllowedNonExistent.phpt index 260303c6..437e5085 100644 --- a/tests/Security/Permission.IsAllowedNonExistent.phpt +++ b/tests/Security/Permission.IsAllowedNonExistent.phpt @@ -13,12 +13,16 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -Assert::exception(function () { - $acl = new Permission; - $acl->isAllowed('nonexistent'); -}, Nette\InvalidStateException::class, "Role 'nonexistent' does not exist."); - -Assert::exception(function () { - $acl = new Permission; - $acl->isAllowed(null, 'nonexistent'); -}, Nette\InvalidStateException::class, "Resource 'nonexistent' does not exist."); +$acl = new Permission; +Assert::exception( + fn() => $acl->isAllowed('nonexistent'), + Nette\InvalidStateException::class, + "Role 'nonexistent' does not exist.", +); + +$acl = new Permission; +Assert::exception( + fn() => $acl->isAllowed(null, 'nonexistent'), + Nette\InvalidStateException::class, + "Resource 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.ResourceAddInheritsNonExistent.phpt b/tests/Security/Permission.ResourceAddInheritsNonExistent.phpt index 9bdf4678..0351edcf 100644 --- a/tests/Security/Permission.ResourceAddInheritsNonExistent.phpt +++ b/tests/Security/Permission.ResourceAddInheritsNonExistent.phpt @@ -14,6 +14,8 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; -Assert::exception(function () use ($acl) { - $acl->addResource('area', 'nonexistent'); -}, Nette\InvalidStateException::class, "Resource 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->addResource('area', 'nonexistent'), + Nette\InvalidStateException::class, + "Resource 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.ResourceDuplicate.phpt b/tests/Security/Permission.ResourceDuplicate.phpt index 25788827..983e8308 100644 --- a/tests/Security/Permission.ResourceDuplicate.phpt +++ b/tests/Security/Permission.ResourceDuplicate.phpt @@ -13,8 +13,10 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -Assert::exception(function () { - $acl = new Permission; - $acl->addResource('area'); - $acl->addResource('area'); -}, Nette\InvalidStateException::class, "Resource 'area' already exists in the list."); +$acl = new Permission; +$acl->addResource('area'); +Assert::exception( + fn() => $acl->addResource('area'), + Nette\InvalidStateException::class, + "Resource 'area' already exists in the list.", +); diff --git a/tests/Security/Permission.ResourceInheritsNonExistent.phpt b/tests/Security/Permission.ResourceInheritsNonExistent.phpt index 5503d6e6..5f1c88cf 100644 --- a/tests/Security/Permission.ResourceInheritsNonExistent.phpt +++ b/tests/Security/Permission.ResourceInheritsNonExistent.phpt @@ -15,10 +15,14 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; $acl->addResource('area'); -Assert::exception(function () use ($acl) { - $acl->resourceInheritsFrom('nonexistent', 'area'); -}, Nette\InvalidStateException::class, "Resource 'nonexistent' does not exist."); - -Assert::exception(function () use ($acl) { - $acl->resourceInheritsFrom('area', 'nonexistent'); -}, Nette\InvalidStateException::class, "Resource 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->resourceInheritsFrom('nonexistent', 'area'), + Nette\InvalidStateException::class, + "Resource 'nonexistent' does not exist.", +); + +Assert::exception( + fn() => $acl->resourceInheritsFrom('area', 'nonexistent'), + Nette\InvalidStateException::class, + "Resource 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.ResourceRemoveOneNonExistent.phpt b/tests/Security/Permission.ResourceRemoveOneNonExistent.phpt index 2b981b95..b3c79077 100644 --- a/tests/Security/Permission.ResourceRemoveOneNonExistent.phpt +++ b/tests/Security/Permission.ResourceRemoveOneNonExistent.phpt @@ -14,6 +14,8 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; -Assert::exception(function () use ($acl) { - $acl->removeResource('nonexistent'); -}, Nette\InvalidStateException::class, "Resource 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->removeResource('nonexistent'), + Nette\InvalidStateException::class, + "Resource 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.RoleRegistryAddInheritsNonExistent.phpt b/tests/Security/Permission.RoleRegistryAddInheritsNonExistent.phpt index d2305cd5..4b91c0ca 100644 --- a/tests/Security/Permission.RoleRegistryAddInheritsNonExistent.phpt +++ b/tests/Security/Permission.RoleRegistryAddInheritsNonExistent.phpt @@ -14,6 +14,8 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; -Assert::exception(function () use ($acl) { - $acl->addRole('guest', 'nonexistent'); -}, Nette\InvalidStateException::class, "Role 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->addRole('guest', 'nonexistent'), + Nette\InvalidStateException::class, + "Role 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.RoleRegistryDuplicate.phpt b/tests/Security/Permission.RoleRegistryDuplicate.phpt index 9389568d..7eafc570 100644 --- a/tests/Security/Permission.RoleRegistryDuplicate.phpt +++ b/tests/Security/Permission.RoleRegistryDuplicate.phpt @@ -14,7 +14,9 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; -Assert::exception(function () use ($acl) { - $acl->addRole('guest'); - $acl->addRole('guest'); -}, Nette\InvalidStateException::class, "Role 'guest' already exists in the list."); +$acl->addRole('guest'); +Assert::exception( + fn() => $acl->addRole('guest'), + Nette\InvalidStateException::class, + "Role 'guest' already exists in the list.", +); diff --git a/tests/Security/Permission.RoleRegistryInheritsNonExistent.phpt b/tests/Security/Permission.RoleRegistryInheritsNonExistent.phpt index 8001493d..3840c06e 100644 --- a/tests/Security/Permission.RoleRegistryInheritsNonExistent.phpt +++ b/tests/Security/Permission.RoleRegistryInheritsNonExistent.phpt @@ -15,10 +15,14 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; $acl->addRole('guest'); -Assert::exception(function () use ($acl) { - $acl->roleInheritsFrom('nonexistent', 'guest'); -}, Nette\InvalidStateException::class, "Role 'nonexistent' does not exist."); - -Assert::exception(function () use ($acl) { - $acl->roleInheritsFrom('guest', 'nonexistent'); -}, Nette\InvalidStateException::class, "Role 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->roleInheritsFrom('nonexistent', 'guest'), + Nette\InvalidStateException::class, + "Role 'nonexistent' does not exist.", +); + +Assert::exception( + fn() => $acl->roleInheritsFrom('guest', 'nonexistent'), + Nette\InvalidStateException::class, + "Role 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.RoleRegistryRemoveOneNonExistent.phpt b/tests/Security/Permission.RoleRegistryRemoveOneNonExistent.phpt index cbc0e6a5..e49801a1 100644 --- a/tests/Security/Permission.RoleRegistryRemoveOneNonExistent.phpt +++ b/tests/Security/Permission.RoleRegistryRemoveOneNonExistent.phpt @@ -14,6 +14,8 @@ require __DIR__ . '/../bootstrap.php'; $acl = new Permission; -Assert::exception(function () use ($acl) { - $acl->removeRole('nonexistent'); -}, Nette\InvalidStateException::class, "Role 'nonexistent' does not exist."); +Assert::exception( + fn() => $acl->removeRole('nonexistent'), + Nette\InvalidStateException::class, + "Role 'nonexistent' does not exist.", +); diff --git a/tests/Security/Permission.RuleRoleRemove.phpt b/tests/Security/Permission.RuleRoleRemove.phpt index 39f76f0f..a4a2b13a 100644 --- a/tests/Security/Permission.RuleRoleRemove.phpt +++ b/tests/Security/Permission.RuleRoleRemove.phpt @@ -18,9 +18,11 @@ $acl->addRole('guest'); $acl->allow('guest'); Assert::true($acl->isAllowed('guest')); $acl->removeRole('guest'); -Assert::exception(function () use ($acl) { - $acl->isAllowed('guest'); -}, Nette\InvalidStateException::class, "Role 'guest' does not exist."); +Assert::exception( + fn() => $acl->isAllowed('guest'), + Nette\InvalidStateException::class, + "Role 'guest' does not exist.", +); $acl->addRole('guest'); Assert::false($acl->isAllowed('guest')); diff --git a/tests/Security/Permission.RuleRoleRemoveAll.phpt b/tests/Security/Permission.RuleRoleRemoveAll.phpt index 245caeca..d6b6a436 100644 --- a/tests/Security/Permission.RuleRoleRemoveAll.phpt +++ b/tests/Security/Permission.RuleRoleRemoveAll.phpt @@ -18,9 +18,11 @@ $acl->addRole('guest'); $acl->allow('guest'); Assert::true($acl->isAllowed('guest')); $acl->removeAllRoles(); -Assert::exception(function () use ($acl) { - $acl->isAllowed('guest'); -}, Nette\InvalidStateException::class, "Role 'guest' does not exist."); +Assert::exception( + fn() => $acl->isAllowed('guest'), + Nette\InvalidStateException::class, + "Role 'guest' does not exist.", +); $acl->addRole('guest'); Assert::false($acl->isAllowed('guest')); diff --git a/tests/Security/Permission.RulesResourceRemove.phpt b/tests/Security/Permission.RulesResourceRemove.phpt index ee1b902e..c51cc11b 100644 --- a/tests/Security/Permission.RulesResourceRemove.phpt +++ b/tests/Security/Permission.RulesResourceRemove.phpt @@ -18,9 +18,11 @@ $acl->addResource('area'); $acl->allow(null, 'area'); Assert::true($acl->isAllowed(null, 'area')); $acl->removeResource('area'); -Assert::exception(function () use ($acl) { - $acl->isAllowed(null, 'area'); -}, Nette\InvalidStateException::class, "Resource 'area' does not exist."); +Assert::exception( + fn() => $acl->isAllowed(null, 'area'), + Nette\InvalidStateException::class, + "Resource 'area' does not exist.", +); $acl->addResource('area'); Assert::false($acl->isAllowed(null, 'area')); diff --git a/tests/Security/Permission.RulesResourceRemoveAll.phpt b/tests/Security/Permission.RulesResourceRemoveAll.phpt index c346c096..4f14e870 100644 --- a/tests/Security/Permission.RulesResourceRemoveAll.phpt +++ b/tests/Security/Permission.RulesResourceRemoveAll.phpt @@ -18,9 +18,11 @@ $acl->addResource('area'); $acl->allow(null, 'area'); Assert::true($acl->isAllowed(null, 'area')); $acl->removeAllResources(); -Assert::exception(function () use ($acl) { - $acl->isAllowed(null, 'area'); -}, Nette\InvalidStateException::class, "Resource 'area' does not exist."); +Assert::exception( + fn() => $acl->isAllowed(null, 'area'), + Nette\InvalidStateException::class, + "Resource 'area' does not exist.", +); $acl->addResource('area'); Assert::false($acl->isAllowed(null, 'area')); diff --git a/tests/Security/SimpleAuthenticator.phpt b/tests/Security/SimpleAuthenticator.phpt index a2c2370f..ed0ecfc4 100644 --- a/tests/Security/SimpleAuthenticator.phpt +++ b/tests/Security/SimpleAuthenticator.phpt @@ -28,10 +28,14 @@ $identity = $authenticator->authenticate('admin', 'admin'); Assert::type(Nette\Security\IIdentity::class, $identity); Assert::equal('admin', $identity->getId()); -Assert::exception(function () use ($authenticator) { - $authenticator->authenticate('admin', 'wrong password'); -}, Nette\Security\AuthenticationException::class, 'Invalid password.'); - -Assert::exception(function () use ($authenticator) { - $authenticator->authenticate('nobody', 'password'); -}, Nette\Security\AuthenticationException::class, "User 'nobody' not found."); +Assert::exception( + fn() => $authenticator->authenticate('admin', 'wrong password'), + Nette\Security\AuthenticationException::class, + 'Invalid password.', +); + +Assert::exception( + fn() => $authenticator->authenticate('nobody', 'password'), + Nette\Security\AuthenticationException::class, + "User 'nobody' not found.", +); diff --git a/tests/Security/User.authentication.phpt b/tests/Security/User.authentication.phpt index 9a94a8db..558aa40e 100644 --- a/tests/Security/User.authentication.phpt +++ b/tests/Security/User.authentication.phpt @@ -58,23 +58,26 @@ Assert::null($user->getId()); // authenticate -Assert::exception(function () use ($user) { - // login without handler - $user->login('jane', ''); -}, Nette\InvalidStateException::class, 'Authenticator has not been set.'); +Assert::exception( + fn() => $user->login('jane', ''), + Nette\InvalidStateException::class, + 'Authenticator has not been set.', +); $handler = new Authenticator; $user->setAuthenticator($handler); -Assert::exception(function () use ($user) { - // login as jane - $user->login('jane', ''); -}, Nette\Security\AuthenticationException::class, 'Unknown user'); - -Assert::exception(function () use ($user) { - // login as john - $user->login('john', ''); -}, Nette\Security\AuthenticationException::class, 'Password not match'); +Assert::exception( + fn() => $user->login('jane', ''), + Nette\Security\AuthenticationException::class, + 'Unknown user', +); + +Assert::exception( + fn() => $user->login('john', ''), + Nette\Security\AuthenticationException::class, + 'Password not match', +); // login as john#2 $user->login('john', 'xxx'); diff --git a/tests/Security/User.authorization.phpt b/tests/Security/User.authorization.phpt index e093ee56..118890be 100644 --- a/tests/Security/User.authorization.phpt +++ b/tests/Security/User.authorization.phpt @@ -81,9 +81,11 @@ Assert::false($user->isInRole('guest')); // authorization -Assert::exception(function () use ($user) { - $user->isAllowed('delete_file'); -}, Nette\InvalidStateException::class, 'Authorizator has not been set.'); +Assert::exception( + fn() => $user->isAllowed('delete_file'), + Nette\InvalidStateException::class, + 'Authorizator has not been set.', +); $handler = new Authorizator; $user->setAuthorizator($handler); From dc329d35aac503b9a9a8dab0d03eda3a4275c89b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 1 Mar 2021 19:15:45 +0100 Subject: [PATCH 06/53] added property typehints --- src/Bridges/SecurityDI/SecurityExtension.php | 3 +- src/Bridges/SecurityHttp/CookieStorage.php | 27 ++++---------- src/Bridges/SecurityHttp/SessionStorage.php | 19 +++------- src/Bridges/SecurityTracy/UserPanel.php | 3 +- src/Security/Identity.php | 13 +++---- src/Security/Passwords.php | 7 ++-- src/Security/Permission.php | 17 ++++----- src/Security/SimpleAuthenticator.php | 11 ++---- src/Security/User.php | 38 ++++++++------------ 9 files changed, 43 insertions(+), 95 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index cf762ef3..016e4113 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -19,8 +19,7 @@ */ class SecurityExtension extends Nette\DI\CompilerExtension { - /** @var bool */ - private $debugMode; + private bool $debugMode; public function __construct(bool $debugMode = false) diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 48ba18a2..7279a797 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -23,26 +23,13 @@ final class CookieStorage implements Nette\Security\UserStorage private const MinLength = 13; - /** @var Http\IRequest */ - private $request; - - /** @var Http\IResponse */ - private $response; - - /** @var ?string */ - private $uid; - - /** @var string */ - private $cookieName = 'userid'; - - /** @var ?string */ - private $cookieDomain; - - /** @var string */ - private $cookieSameSite = 'Lax'; - - /** @var ?string */ - private $cookieExpiration; + private Http\IRequest $request; + private Http\IResponse $response; + private ?string $uid = null; + private string $cookieName = 'userid'; + private ?string $cookieDomain = null; + private string $cookieSameSite = 'Lax'; + private ?string $cookieExpiration = null; public function __construct(Http\IRequest $request, Http\IResponse $response) diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index 4da9f63c..fb4fdc7b 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -23,20 +23,11 @@ final class SessionStorage implements Nette\Security\UserStorage { use Nette\SmartObject; - /** @var string */ - private $namespace = ''; - - /** @var Session */ - private $sessionHandler; - - /** @var SessionSection */ - private $sessionSection; - - /** @var ?int */ - private $expireTime; - - /** @var bool */ - private $expireIdentity = false; + private string $namespace = ''; + private Session $sessionHandler; + private ?SessionSection $sessionSection = null; + private ?int $expireTime = null; + private bool $expireIdentity = false; public function __construct(Session $sessionHandler) diff --git a/src/Bridges/SecurityTracy/UserPanel.php b/src/Bridges/SecurityTracy/UserPanel.php index 488757c1..123d86c1 100644 --- a/src/Bridges/SecurityTracy/UserPanel.php +++ b/src/Bridges/SecurityTracy/UserPanel.php @@ -20,8 +20,7 @@ class UserPanel implements Tracy\IBarPanel { use Nette\SmartObject; - /** @var Nette\Security\User */ - private $user; + private Nette\Security\User $user; public function __construct(Nette\Security\User $user) diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 3a78c2e5..ab1f2ce1 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -14,7 +14,7 @@ /** * @deprecated use Nette\Security\SimpleIdentity - * @property mixed $id + * @property string|int $id * @property array $roles * @property array $data */ @@ -26,14 +26,9 @@ class Identity implements IIdentity __isset as private parentIsSet; } - /** @var mixed */ - private $id; - - /** @var array */ - private $roles; - - /** @var array */ - private $data; + private string|int $id; + private array $roles; + private array $data; public function __construct($id, $roles = null, ?iterable $data = null) diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 4c87a06a..0c690dca 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -19,11 +19,8 @@ class Passwords { use Nette\SmartObject; - /** @var int|string string since PHP 7.4 */ - private $algo; - - /** @var array */ - private $options; + private string $algo; + private array $options; /** diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 0a6b9e42..758aa943 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -21,14 +21,11 @@ class Permission implements Authorizator { use Nette\SmartObject; - /** @var array Role storage */ - private $roles = []; + private array $roles = []; + private array $resources = []; - /** @var array Resource storage */ - private $resources = []; - - /** @var array Access Control List rules; whitelist (deny everything to all) by default */ - private $rules = [ + /** Access Control List rules; whitelist (deny everything to all) by default */ + private array $rules = [ 'allResources' => [ 'allRoles' => [ 'allPrivileges' => [ @@ -42,10 +39,8 @@ class Permission implements Authorizator 'byResource' => [], ]; - /** @var mixed */ - private $queriedRole; - - private $queriedResource; + private string|Role|null $queriedRole; + private string|Resource|null $queriedResource; /********************* roles ****************d*g**/ diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index b1e05bba..8cc589f9 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -19,14 +19,9 @@ class SimpleAuthenticator implements Authenticator { use Nette\SmartObject; - /** @var array */ - private $passwords; - - /** @var array */ - private $roles; - - /** @var array */ - private $data; + private array $passwords; + private array $roles; + private array $data; /** diff --git a/src/Security/User.php b/src/Security/User.php index 64fae555..7d1209c1 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -41,35 +41,25 @@ class User public const LOGOUT_MANUAL = self::LogoutManual; public const LOGOUT_INACTIVITY = self::LogoutInactivity; - /** @var string default role for unauthenticated user */ - public $guestRole = 'guest'; + /** default role for unauthenticated user */ + public string $guestRole = 'guest'; - /** @var string default role for authenticated user without own identity */ - public $authenticatedRole = 'authenticated'; + /** default role for authenticated user without own identity */ + public string $authenticatedRole = 'authenticated'; /** @var callable[] function (User $sender): void; Occurs when the user is successfully logged in */ - public $onLoggedIn = []; + public array $onLoggedIn = []; /** @var callable[] function (User $sender): void; Occurs when the user is logged out */ - public $onLoggedOut = []; - - /** @var UserStorage|IUserStorage Session storage for current user */ - private $storage; - - /** @var IAuthenticator|null */ - private $authenticator; - - /** @var Authorizator|null */ - private $authorizator; - - /** @var IIdentity|null */ - private $identity; - - /** @var bool|null */ - private $authenticated; - - /** @var int|null */ - private $logoutReason; + public array $onLoggedOut = []; + + /** Session storage for current user */ + private UserStorage|IUserStorage $storage; + private ?IAuthenticator $authenticator; + private ?Authorizator $authorizator; + private ?IIdentity $identity = null; + private ?bool $authenticated = null; + private ?int $logoutReason = null; public function __construct( From e843379f38bd9f1561917824f7b4c8d11bddcd71 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Dec 2021 18:12:59 +0100 Subject: [PATCH 07/53] added PHP 8 typehints --- src/Bridges/SecurityDI/SecurityExtension.php | 4 +- src/Bridges/SecurityHttp/SessionStorage.php | 3 +- src/Security/Authorizator.php | 5 +- src/Security/Identity.php | 17 +-- src/Security/Passwords.php | 2 +- src/Security/Permission.php | 113 ++++++++----------- src/Security/User.php | 27 ++--- tests/Security/MockUserStorage.legacy.php | 3 +- 8 files changed, 62 insertions(+), 112 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 016e4113..1b183cca 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -55,7 +55,7 @@ public function getConfigSchema(): Nette\Schema\Schema } - public function loadConfiguration() + public function loadConfiguration(): void { /** @var object{debugger: bool, users: array, roles: array, resources: array, authentication: \stdClass} $config */ $config = $this->config; @@ -134,7 +134,7 @@ public function loadConfiguration() } - public function beforeCompile() + public function beforeCompile(): void { $builder = $this->getContainerBuilder(); diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index fb4fdc7b..efe6b654 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -102,9 +102,8 @@ private function setupExpiration(): void /** * Changes namespace; allows more users to share a session. - * @return static */ - public function setNamespace(string $namespace) + public function setNamespace(string $namespace): static { if ($this->namespace !== $namespace) { $this->namespace = $namespace; diff --git a/src/Security/Authorizator.php b/src/Security/Authorizator.php index 267fcccd..106f6cb8 100644 --- a/src/Security/Authorizator.php +++ b/src/Security/Authorizator.php @@ -31,11 +31,8 @@ interface Authorizator /** * Performs a role-based authorization. - * @param string|null $role - * @param string|null $resource - * @param string|null $privilege */ - function isAllowed($role, $resource, $privilege): bool; + function isAllowed(?string $role, ?string $resource, ?string $privilege): bool; } diff --git a/src/Security/Identity.php b/src/Security/Identity.php index ab1f2ce1..8bc45790 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -43,15 +43,9 @@ public function __construct($id, $roles = null, ?iterable $data = null) /** * Sets the ID of user. - * @param string|int $id - * @return static */ - public function setId($id) + public function setId(string|int $id): static { - if (!is_string($id) && !is_int($id)) { - throw new Nette\InvalidArgumentException('Identity identifier must be string|int, but type "' . gettype($id) . '" given.'); - } - $this->id = is_numeric($id) && !is_float($tmp = $id * 1) ? $tmp : $id; return $this; } @@ -59,9 +53,8 @@ public function setId($id) /** * Returns the ID of user. - * @return mixed */ - public function getId() + public function getId(): string|int { return $this->id; } @@ -69,9 +62,8 @@ public function getId() /** * Sets a list of roles that the user is a member of. - * @return static */ - public function setRoles(array $roles) + public function setRoles(array $roles): static { $this->roles = $roles; return $this; @@ -112,9 +104,8 @@ public function __set(string $key, $value): void /** * Returns user data value. - * @return mixed */ - public function &__get(string $key) + public function &__get(string $key): mixed { if ($this->parentIsSet($key)) { return $this->parentGet($key); diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 0c690dca..60b70090 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -27,7 +27,7 @@ class Passwords * Chooses which secure algorithm is used for hashing and how to configure it. * @see https://php.net/manual/en/password.constants.php */ - public function __construct($algo = PASSWORD_DEFAULT, array $options = []) + public function __construct(string $algo = PASSWORD_DEFAULT, array $options = []) { $this->algo = $algo; $this->options = $options; diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 758aa943..5ea5f944 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -49,12 +49,10 @@ class Permission implements Authorizator /** * Adds a Role to the list. The most recently added parent * takes precedence over parents that were previously added. - * @param string|array $parents * @throws Nette\InvalidArgumentException * @throws Nette\InvalidStateException - * @return static */ - public function addRole(string $role, $parents = null) + public function addRole(string $role, string|array $parents = null): static { $this->checkRole($role, exists: false); if (isset($this->roles[$role])) { @@ -158,9 +156,8 @@ public function roleInheritsFrom(string $role, string $inherit, bool $onlyParent * Removes the Role from the list. * * @throws Nette\InvalidStateException - * @return static */ - public function removeRole(string $role) + public function removeRole(string $role): static { $this->checkRole($role); @@ -196,10 +193,8 @@ public function removeRole(string $role) /** * Removes all Roles from the list. - * - * @return static */ - public function removeAllRoles() + public function removeAllRoles(): static { $this->roles = []; @@ -225,9 +220,8 @@ public function removeAllRoles() * * @throws Nette\InvalidArgumentException * @throws Nette\InvalidStateException - * @return static */ - public function addResource(string $resource, ?string $parent = null) + public function addResource(string $resource, ?string $parent = null): static { $this->checkResource($resource, exists: false); @@ -321,9 +315,8 @@ public function resourceInheritsFrom(string $resource, string $inherit, bool $on * Removes a Resource and all of its children. * * @throws Nette\InvalidStateException - * @return static */ - public function removeResource(string $resource) + public function removeResource(string $resource): static { $this->checkResource($resource); @@ -353,9 +346,8 @@ public function removeResource(string $resource) /** * Removes all Resources. - * @return static */ - public function removeAllResources() + public function removeAllResources(): static { foreach ($this->resources as $resource => $foo) { foreach ($this->rules['byResource'] as $resourceCurrent => $rules) { @@ -376,18 +368,14 @@ public function removeAllResources() /** * Allows one or more Roles access to [certain $privileges upon] the specified Resource(s). * If $assertion is provided, then it must return true in order for rule to apply. - * - * @param string|string[]|null $roles - * @param string|string[]|null $resources - * @param string|string[]|null $privileges - * @return static */ public function allow( - $roles = self::All, - $resources = self::All, - $privileges = self::All, + string|array|null $roles = self::All, + string|array|null $resources = self::All, + string|array|null $privileges = self::All, ?callable $assertion = null, - ) { + ): static + { $this->setRule(true, self::Allow, $roles, $resources, $privileges, $assertion); return $this; } @@ -396,18 +384,14 @@ public function allow( /** * Denies one or more Roles access to [certain $privileges upon] the specified Resource(s). * If $assertion is provided, then it must return true in order for rule to apply. - * - * @param string|string[]|null $roles - * @param string|string[]|null $resources - * @param string|string[]|null $privileges - * @return static */ public function deny( - $roles = self::All, - $resources = self::All, - $privileges = self::All, + string|array|null $roles = self::All, + string|array|null $resources = self::All, + string|array|null $privileges = self::All, ?callable $assertion = null, - ) { + ): static + { $this->setRule(true, self::Deny, $roles, $resources, $privileges, $assertion); return $this; } @@ -415,13 +399,12 @@ public function deny( /** * Removes "allow" permissions from the list in the context of the given Roles, Resources, and privileges. - * - * @param string|string[]|null $roles - * @param string|string[]|null $resources - * @param string|string[]|null $privileges - * @return static */ - public function removeAllow($roles = self::All, $resources = self::All, $privileges = self::All) + public function removeAllow( + string|array|null $roles = self::All, + string|array|null $resources = self::All, + string|array|null $privileges = self::All, + ): static { $this->setRule(false, self::Allow, $roles, $resources, $privileges); return $this; @@ -430,13 +413,12 @@ public function removeAllow($roles = self::All, $resources = self::All, $privile /** * Removes "deny" restrictions from the list in the context of the given Roles, Resources, and privileges. - * - * @param string|string[]|null $roles - * @param string|string[]|null $resources - * @param string|string[]|null $privileges - * @return static */ - public function removeDeny($roles = self::All, $resources = self::All, $privileges = self::All) + public function removeDeny( + string|array|null $roles = self::All, + string|array|null $resources = self::All, + string|array|null $privileges = self::All, + ): static { $this->setRule(false, self::Deny, $roles, $resources, $privileges); return $this; @@ -445,13 +427,16 @@ public function removeDeny($roles = self::All, $resources = self::All, $privileg /** * Performs operations on Access Control List rules. - * @param string|string[]|null $roles - * @param string|string[]|null $resources - * @param string|string[]|null $privileges * @throws Nette\InvalidStateException - * @return static */ - protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privileges, ?callable $assertion = null) + protected function setRule( + bool $toAdd, + bool $type, + string|array|null $roles, + string|array|null $resources, + string|array|null $privileges, + ?callable $assertion = null, + ): void { // ensure that all specified Roles exist; normalize input to array of Roles or null if ($roles === self::All) { @@ -545,8 +530,6 @@ protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privile } } } - - return $this; } @@ -561,12 +544,13 @@ protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privile * and its respective parents are checked similarly before the lower-priority parents of * the Role are checked. * - * @param string|Role|null $role - * @param string|Nette\Security\Resource|null $resource - * @param string|null $privilege * @throws Nette\InvalidStateException */ - public function isAllowed($role = self::All, $resource = self::All, $privilege = self::All): bool + public function isAllowed( + string|Role|null $role = self::All, + string|Resource|null $resource = self::All, + string|null $privilege = self::All, + ): bool { $this->queriedRole = $role; if ($role !== self::All) { @@ -623,9 +607,8 @@ public function isAllowed($role = self::All, $resource = self::All, $privilege = /** * Returns real currently queried Role. Use by assertion. - * @return mixed */ - public function getQueriedRole() + public function getQueriedRole(): string|Role|null { return $this->queriedRole; } @@ -633,9 +616,8 @@ public function getQueriedRole() /** * Returns real currently queried Resource. Use by assertion. - * @return mixed */ - public function getQueriedResource() + public function getQueriedResource(): string|Resource|null { return $this->queriedResource; } @@ -648,9 +630,8 @@ public function getQueriedResource() * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule * allowing/denying $role access to a/all $privilege upon $resource. * @param bool $all (true) or one? - * @return mixed null if no applicable rule is found, otherwise returns ALLOW or DENY */ - private function searchRolePrivileges(bool $all, $role, $resource, $privilege) + private function searchRolePrivileges(bool $all, ?string $role, ?string $resource, ?string $privilege): ?bool { $dfs = [ 'visited' => [], @@ -695,12 +676,8 @@ private function searchRolePrivileges(bool $all, $role, $resource, $privilege) /** * Returns the rule type associated with the specified Resource, Role, and privilege. - * @param string|null $resource - * @param string|null $role - * @param string|null $privilege - * @return bool|null null if a rule does not exist or assertion fails, otherwise returns ALLOW or DENY */ - private function getRuleType($resource, $role, $privilege): ?bool + private function getRuleType(?string $resource, ?string $role, ?string $privilege): ?bool { if (!$rules = $this->getRules($resource, $role)) { return null; @@ -737,10 +714,8 @@ private function getRuleType($resource, $role, $privilege): ?bool /** * Returns the rules associated with a Resource and a Role, or null if no such rules exist. * If the $create parameter is true, then a rule set is first created and then returned to the caller. - * @param string|null $resource - * @param string|null $role */ - private function &getRules($resource, $role, bool $create = false): ?array + private function &getRules(?string $resource, ?string $role, bool $create = false): ?array { $null = null; if ($resource === self::All) { diff --git a/src/Security/User.php b/src/Security/User.php index 7d1209c1..a7ea4d8b 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -18,7 +18,7 @@ * * @property-read bool $loggedIn * @property-read IIdentity $identity - * @property-read mixed $id + * @property-read string|int $id * @property-read array $roles * @property-read int $logoutReason * @property IAuthenticator $authenticator @@ -78,10 +78,7 @@ public function __construct( } - /** - * @return UserStorage|IUserStorage - */ - final public function getStorage() + final public function getStorage(): UserStorage|IUserStorage { return $this->storage; } @@ -96,7 +93,7 @@ final public function getStorage() * @throws AuthenticationException if authentication was not successful */ public function login( - $user, + string|IIdentity $user, #[\SensitiveParameter] ?string $password = null, ): void @@ -202,9 +199,8 @@ private function getStoredData(): void /** * Returns current user ID, if any. - * @return mixed */ - public function getId() + public function getId(): string|int|null { $identity = $this->getIdentity(); return $identity ? $identity->getId() : null; @@ -219,9 +215,8 @@ final public function refreshStorage(): void /** * Sets authentication handler. - * @return static */ - public function setAuthenticator(IAuthenticator $handler) + public function setAuthenticator(IAuthenticator $handler): static { $this->authenticator = $handler; return $this; @@ -264,16 +259,9 @@ final public function hasAuthenticator(): bool /** * Enables log out after inactivity (like '20 minutes'). - * @param string|null $expire - * @param int|bool $clearIdentity - * @return static */ - public function setExpiration($expire, $clearIdentity = null) + public function setExpiration(?string $expire, bool|int|null $clearIdentity = null) { - if ($expire !== null && !is_string($expire)) { - trigger_error("Expiration should be a string like '20 minutes' etc.", E_USER_DEPRECATED); - } - if (func_num_args() > 2) { $clearIdentity = $clearIdentity || func_get_arg(2); trigger_error(__METHOD__ . '() third parameter is deprecated, use second one: setExpiration($time, true|false)', E_USER_DEPRECATED); @@ -346,9 +334,8 @@ public function isAllowed($resource = Authorizator::All, $privilege = Authorizat /** * Sets authorization handler. - * @return static */ - public function setAuthorizator(Authorizator $handler) + public function setAuthorizator(Authorizator $handler): static { $this->authorizator = $handler; return $this; diff --git a/tests/Security/MockUserStorage.legacy.php b/tests/Security/MockUserStorage.legacy.php index d4b134be..679e2b36 100644 --- a/tests/Security/MockUserStorage.legacy.php +++ b/tests/Security/MockUserStorage.legacy.php @@ -9,9 +9,10 @@ class MockUserStorage implements Nette\Security\IUserStorage private $identity; - public function setAuthenticated(bool $state) + public function setAuthenticated(bool $state): self { $this->auth = $state; + return $this; } From b87b3e7c83f109e6e96f3168348734a865e218cd Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 2 Mar 2021 15:03:47 +0100 Subject: [PATCH 08/53] removed deprecated stuff --- src/Security/User.php | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Security/User.php b/src/Security/User.php index a7ea4d8b..91b8fb7c 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -226,14 +226,9 @@ public function setAuthenticator(IAuthenticator $handler): static /** * Returns authentication handler. */ - final public function getAuthenticator(): ?IAuthenticator + final public function getAuthenticator(): IAuthenticator { - if (func_num_args()) { - trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getAuthenticatorIfExists()', E_USER_DEPRECATED); - $throw = func_get_arg(0); - } - - if (($throw ?? true) && !$this->authenticator) { + if (!$this->authenticator) { throw new Nette\InvalidStateException('Authenticator has not been set.'); } @@ -262,11 +257,6 @@ final public function hasAuthenticator(): bool */ public function setExpiration(?string $expire, bool|int|null $clearIdentity = null) { - if (func_num_args() > 2) { - $clearIdentity = $clearIdentity || func_get_arg(2); - trigger_error(__METHOD__ . '() third parameter is deprecated, use second one: setExpiration($time, true|false)', E_USER_DEPRECATED); - } - $arg = $this->storage instanceof UserStorage ? (bool) $clearIdentity : ($clearIdentity ? IUserStorage::CLEAR_IDENTITY : 0); @@ -345,14 +335,9 @@ public function setAuthorizator(Authorizator $handler): static /** * Returns current authorization handler. */ - final public function getAuthorizator(): ?Authorizator + final public function getAuthorizator(): Authorizator { - if (func_num_args()) { - trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getAuthorizatorIfExists()', E_USER_DEPRECATED); - $throw = func_get_arg(0); - } - - if (($throw ?? true) && !$this->authorizator) { + if (!$this->authorizator) { throw new Nette\InvalidStateException('Authorizator has not been set.'); } From 590b965cdc8379a0762f18ef3f785801156a19dc Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Dec 2021 18:13:42 +0100 Subject: [PATCH 09/53] removed deprecated IUserStorage (BC break) --- src/Bridges/SecurityDI/SecurityExtension.php | 4 - src/Security/IUserStorage.php | 58 --------- src/Security/User.php | 54 ++------ tests/Security.DI/SecurityExtension.user.phpt | 1 - tests/Security/MockUserStorage.legacy.php | 46 ------- .../Security/User.authentication.legacy.phpt | 117 ------------------ tests/Security/User.authentication.phpt | 2 +- tests/Security/User.authorization.phpt | 2 +- 8 files changed, 15 insertions(+), 269 deletions(-) delete mode 100644 src/Security/IUserStorage.php delete mode 100644 tests/Security/MockUserStorage.legacy.php delete mode 100644 tests/Security/User.authentication.legacy.phpt diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 1b183cca..339ec43d 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -80,10 +80,6 @@ public function loadConfiguration(): void $storage->addSetup('setCookieParameters', [$auth->cookieName, $auth->cookieDomain, $auth->cookieSamesite]); } - $builder->addDefinition($this->prefix('legacyUserStorage')) // deprecated - ->setType(Nette\Security\IUserStorage::class) - ->setFactory(Nette\Http\UserStorage::class); - $user = $builder->addDefinition($this->prefix('user')) ->setFactory(Nette\Security\User::class); diff --git a/src/Security/IUserStorage.php b/src/Security/IUserStorage.php deleted file mode 100644 index 061e52c6..00000000 --- a/src/Security/IUserStorage.php +++ /dev/null @@ -1,58 +0,0 @@ -storage = $storage ?? $legacyStorage; // back compatibility - if (!$this->storage) { - throw new Nette\InvalidStateException('UserStorage has not been set.'); - } - + $this->storage = $storage; $this->authenticator = $authenticator; $this->authorizator = $authorizator; } - final public function getStorage(): UserStorage|IUserStorage + final public function getStorage(): UserStorage { return $this->storage; } @@ -111,13 +106,8 @@ public function login( $id = $this->authenticator instanceof IdentityHandler ? $this->authenticator->sleepIdentity($this->identity) : $this->identity; - if ($this->storage instanceof UserStorage) { - $this->storage->saveAuthentication($id); - } else { - $this->storage->setIdentity($id); - $this->storage->setAuthenticated(true); - } + $this->storage->saveAuthentication($id); $this->authenticated = true; $this->logoutReason = null; Arrays::invoke($this->onLoggedIn, $this); @@ -130,16 +120,7 @@ public function login( final public function logout(bool $clearIdentity = false): void { $logged = $this->isLoggedIn(); - - if ($this->storage instanceof UserStorage) { - $this->storage->clearAuthentication($clearIdentity); - } else { - $this->storage->setAuthenticated(false); - if ($clearIdentity) { - $this->storage->setIdentity(null); - } - } - + $this->storage->clearAuthentication($clearIdentity); $this->authenticated = false; $this->logoutReason = self::LogoutManual; if ($logged) { @@ -178,17 +159,11 @@ final public function getIdentity(): ?IIdentity private function getStoredData(): void { - if ($this->storage instanceof UserStorage) { - (function (bool $state, ?IIdentity $id, ?int $reason) use (&$identity) { - $identity = $id; - $this->authenticated = $state; - $this->logoutReason = $reason; - })(...$this->storage->getState()); - } else { - $identity = $this->storage->getIdentity(); - $this->authenticated = $this->storage->isAuthenticated(); - $this->logoutReason = $this->storage->getLogoutReason(); - } + (function (bool $state, ?IIdentity $id, ?int $reason) use (&$identity) { + $identity = $id; + $this->authenticated = $state; + $this->logoutReason = $reason; + })(...$this->storage->getState()); $this->identity = $identity && $this->authenticator instanceof IdentityHandler ? $this->authenticator->wakeupIdentity($identity) @@ -255,12 +230,9 @@ final public function hasAuthenticator(): bool /** * Enables log out after inactivity (like '20 minutes'). */ - public function setExpiration(?string $expire, bool|int|null $clearIdentity = null) + public function setExpiration(?string $expire, bool $clearIdentity = false) { - $arg = $this->storage instanceof UserStorage - ? (bool) $clearIdentity - : ($clearIdentity ? IUserStorage::CLEAR_IDENTITY : 0); - $this->storage->setExpiration($expire, $arg); + $this->storage->setExpiration($expire, $clearIdentity); return $this; } diff --git a/tests/Security.DI/SecurityExtension.user.phpt b/tests/Security.DI/SecurityExtension.user.phpt index f4f3e9ff..fa0706e5 100644 --- a/tests/Security.DI/SecurityExtension.user.phpt +++ b/tests/Security.DI/SecurityExtension.user.phpt @@ -25,7 +25,6 @@ eval($compiler->compile()); $container = new Container; Assert::type(Nette\Bridges\SecurityHttp\SessionStorage::class, $container->getService('security.userStorage')); -Assert::type(Nette\Http\UserStorage::class, $container->getService('security.legacyUserStorage')); Assert::type(Nette\Security\User::class, $container->getService('security.user')); // aliases diff --git a/tests/Security/MockUserStorage.legacy.php b/tests/Security/MockUserStorage.legacy.php deleted file mode 100644 index 679e2b36..00000000 --- a/tests/Security/MockUserStorage.legacy.php +++ /dev/null @@ -1,46 +0,0 @@ -auth = $state; - return $this; - } - - - public function isAuthenticated(): bool - { - return $this->auth; - } - - - public function setIdentity(?Nette\Security\IIdentity $identity = null) - { - $this->identity = $identity; - } - - - public function getIdentity(): ?Nette\Security\IIdentity - { - return $this->identity; - } - - - public function setExpiration(?string $time, int $flags = 0) - { - } - - - public function getLogoutReason(): ?int - { - return null; - } -} diff --git a/tests/Security/User.authentication.legacy.phpt b/tests/Security/User.authentication.legacy.phpt deleted file mode 100644 index 8c0e559d..00000000 --- a/tests/Security/User.authentication.legacy.phpt +++ /dev/null @@ -1,117 +0,0 @@ - 0, - 'logout' => 0, -]; - -$user->onLoggedIn[] = function () use ($counter) { - $counter->login++; -}; - -$user->onLoggedOut[] = function () use ($counter) { - $counter->logout++; -}; - - -Assert::false($user->isLoggedIn()); -Assert::null($user->getIdentity()); -Assert::null($user->getId()); - - -// authenticate -Assert::exception(function () use ($user) { - // login without handler - $user->login('jane', ''); -}, Nette\InvalidStateException::class, 'Authenticator has not been set.'); - -$handler = new Authenticator; -$user->setAuthenticator($handler); - -Assert::exception(function () use ($user) { - // login as jane - $user->login('jane', ''); -}, Nette\Security\AuthenticationException::class, 'Unknown user'); - -Assert::exception(function () use ($user) { - // login as john - $user->login('john', ''); -}, Nette\Security\AuthenticationException::class, 'Password not match'); - -// login as john#2 -$user->login('john', 'xxx'); -Assert::same(1, $counter->login); -Assert::true($user->isLoggedIn()); -Assert::equal(new Identity('John Doe', 'admin'), $user->getIdentity()); -Assert::same('John Doe', $user->getId()); - -// login as john#3 -$user->logout(true); -Assert::same(1, $counter->logout); -$user->login(new Identity('John Doe', 'admin')); -Assert::same(2, $counter->login); -Assert::true($user->isLoggedIn()); -Assert::equal(new Identity('John Doe', 'admin'), $user->getIdentity()); - - -// log out -// logging out... -$user->logout(false); -Assert::same(2, $counter->logout); - -Assert::false($user->isLoggedIn()); -Assert::equal(new Identity('John Doe', 'admin'), $user->getIdentity()); - - -// logging out and clearing identity... -$user->logout(true); -Assert::same(2, $counter->logout); // not logged in -> logout event not triggered - -Assert::false($user->isLoggedIn()); -Assert::null($user->getIdentity()); - - -// namespace -// login as john#2? -$user->login('john', 'xxx'); -Assert::same(3, $counter->login); -Assert::true($user->isLoggedIn()); diff --git a/tests/Security/User.authentication.phpt b/tests/Security/User.authentication.phpt index 558aa40e..5d6413e6 100644 --- a/tests/Security/User.authentication.phpt +++ b/tests/Security/User.authentication.phpt @@ -36,7 +36,7 @@ class Authenticator implements Nette\Security\Authenticator } -$user = new Nette\Security\User(null, null, null, new MockUserStorage); +$user = new Nette\Security\User(new MockUserStorage); $counter = (object) [ 'login' => 0, diff --git a/tests/Security/User.authorization.phpt b/tests/Security/User.authorization.phpt index 118890be..ee26305e 100644 --- a/tests/Security/User.authorization.phpt +++ b/tests/Security/User.authorization.phpt @@ -54,7 +54,7 @@ class TesterRole implements Role } } -$user = new Nette\Security\User(null, null, null, new MockUserStorage); +$user = new Nette\Security\User(new MockUserStorage); // guest Assert::false($user->isLoggedIn()); From 0e93577e05a0994be14a0dd3f1ce37419f2f5795 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 13 Nov 2023 12:59:56 +0100 Subject: [PATCH 10/53] removed Nette\SmartObject --- src/Bridges/SecurityHttp/CookieStorage.php | 2 -- src/Bridges/SecurityHttp/SessionStorage.php | 2 -- src/Bridges/SecurityTracy/UserPanel.php | 2 -- src/Security/Identity.php | 19 ++++++------------- src/Security/Passwords.php | 2 -- src/Security/Permission.php | 2 -- src/Security/SimpleAuthenticator.php | 4 ---- 7 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 7279a797..70c15a47 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -19,8 +19,6 @@ */ final class CookieStorage implements Nette\Security\UserStorage { - use Nette\SmartObject; - private const MinLength = 13; private Http\IRequest $request; diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index efe6b654..e0ab56ec 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -21,8 +21,6 @@ */ final class SessionStorage implements Nette\Security\UserStorage { - use Nette\SmartObject; - private string $namespace = ''; private Session $sessionHandler; private ?SessionSection $sessionSection = null; diff --git a/src/Bridges/SecurityTracy/UserPanel.php b/src/Bridges/SecurityTracy/UserPanel.php index 123d86c1..4cf753a1 100644 --- a/src/Bridges/SecurityTracy/UserPanel.php +++ b/src/Bridges/SecurityTracy/UserPanel.php @@ -18,8 +18,6 @@ */ class UserPanel implements Tracy\IBarPanel { - use Nette\SmartObject; - private Nette\Security\User $user; diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 8bc45790..ee00111c 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -9,8 +9,6 @@ namespace Nette\Security; -use Nette; - /** * @deprecated use Nette\Security\SimpleIdentity @@ -20,12 +18,6 @@ */ class Identity implements IIdentity { - use Nette\SmartObject { - __get as private parentGet; - __set as private parentSet; - __isset as private parentIsSet; - } - private string|int $id; private array $roles; private array $data; @@ -93,8 +85,8 @@ public function getData(): array */ public function __set(string $key, $value): void { - if ($this->parentIsSet($key)) { - $this->parentSet($key, $value); + if (in_array($key, ['id', 'roles', 'data'], strict: true)) { + $this->{"set$key"}($value); } else { $this->data[$key] = $value; @@ -107,8 +99,9 @@ public function __set(string $key, $value): void */ public function &__get(string $key): mixed { - if ($this->parentIsSet($key)) { - return $this->parentGet($key); + if (in_array($key, ['id', 'roles', 'data'], strict: true)) { + $res = $this->{"get$key"}(); + return $res; } else { return $this->data[$key]; @@ -118,6 +111,6 @@ public function &__get(string $key): mixed public function __isset(string $key): bool { - return isset($this->data[$key]) || $this->parentIsSet($key); + return isset($this->data[$key]) || in_array($key, ['id', 'roles', 'data'], strict: true); } } diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 60b70090..10329d65 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -17,8 +17,6 @@ */ class Passwords { - use Nette\SmartObject; - private string $algo; private array $options; diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 5ea5f944..888e85a2 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -19,8 +19,6 @@ */ class Permission implements Authorizator { - use Nette\SmartObject; - private array $roles = []; private array $resources = []; diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index 8cc589f9..bb6ab13f 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -9,16 +9,12 @@ namespace Nette\Security; -use Nette; - /** * Trivial implementation of Authenticator. */ class SimpleAuthenticator implements Authenticator { - use Nette\SmartObject; - private array $passwords; private array $roles; private array $data; From b5f6be63d6d562815538e60e7b0720075c635fab Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 11 Dec 2023 13:26:37 +0100 Subject: [PATCH 11/53] used promoted properties --- src/Security/Passwords.php | 12 ++++-------- src/Security/SimpleAuthenticator.php | 14 +++----------- src/Security/User.php | 13 +++---------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 10329d65..ffc4927f 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -17,18 +17,14 @@ */ class Passwords { - private string $algo; - private array $options; - - /** * Chooses which secure algorithm is used for hashing and how to configure it. * @see https://php.net/manual/en/password.constants.php */ - public function __construct(string $algo = PASSWORD_DEFAULT, array $options = []) - { - $this->algo = $algo; - $this->options = $options; + public function __construct( + private string $algo = PASSWORD_DEFAULT, + private array $options = [], + ) { } diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index bb6ab13f..eb5b2b27 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -15,11 +15,6 @@ */ class SimpleAuthenticator implements Authenticator { - private array $passwords; - private array $roles; - private array $data; - - /** * @param array $passwords list of pairs username => password * @param array $roles list of pairs username => role[] @@ -27,13 +22,10 @@ class SimpleAuthenticator implements Authenticator */ public function __construct( #[\SensitiveParameter] - array $passwords, - array $roles = [], - array $data = [], + private array $passwords, + private array $roles = [], + private array $data = [], ) { - $this->passwords = $passwords; - $this->roles = $roles; - $this->data = $data; } diff --git a/src/Security/User.php b/src/Security/User.php index 3748db48..b2acb47e 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -53,23 +53,16 @@ class User /** @var callable[] function (User $sender): void; Occurs when the user is logged out */ public array $onLoggedOut = []; - /** Session storage for current user */ - private UserStorage $storage; - private ?IAuthenticator $authenticator; - private ?Authorizator $authorizator; private ?IIdentity $identity = null; private ?bool $authenticated = null; private ?int $logoutReason = null; public function __construct( - UserStorage $storage, - ?IAuthenticator $authenticator = null, - ?Authorizator $authorizator = null, + private UserStorage $storage, + private ?IAuthenticator $authenticator = null, + private ?Authorizator $authorizator = null, ) { - $this->storage = $storage; - $this->authenticator = $authenticator; - $this->authorizator = $authorizator; } From fe89d52697036fb2e14835dfb46b696d28a9ebf6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 27 Nov 2022 22:32:30 +0100 Subject: [PATCH 12/53] uppercase constants marked as deprecated --- src/Security/Authenticator.php | 12 ++++++++++++ src/Security/Authorizator.php | 5 +++++ src/Security/User.php | 14 +++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Security/Authenticator.php b/src/Security/Authenticator.php index d79e6364..96829284 100644 --- a/src/Security/Authenticator.php +++ b/src/Security/Authenticator.php @@ -22,6 +22,18 @@ interface Authenticator extends IAuthenticator Failure = 3, NotApproved = 4; + /** @deprecated use Authenticator::IdentityNotFound */ + public const IDENTITY_NOT_FOUND = self::IdentityNotFound; + + /** @deprecated use Authenticator::InvalidCredential */ + public const INVALID_CREDENTIAL = self::InvalidCredential; + + /** @deprecated use Authenticator::Failure */ + public const FAILURE = self::Failure; + + /** @deprecated use Authenticator::NotApproved */ + public const NOT_APPROVED = self::NotApproved; + /** * Performs an authentication. * @throws AuthenticationException diff --git a/src/Security/Authorizator.php b/src/Security/Authorizator.php index 106f6cb8..a8fa356e 100644 --- a/src/Security/Authorizator.php +++ b/src/Security/Authorizator.php @@ -25,8 +25,13 @@ interface Authorizator /** Permission type: deny */ public const Deny = false; + /** @deprecated use Authorizator::All */ public const ALL = self::All; + + /** @deprecated use Authorizator::Allow */ public const ALLOW = self::Allow; + + /** @deprecated use Authorizator::Deny */ public const DENY = self::Deny; /** diff --git a/src/Security/User.php b/src/Security/User.php index b2acb47e..afeb67bb 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -33,14 +33,18 @@ class User LogoutManual = 1, LogoutInactivity = 2; - /** @deprecated */ - public const - MANUAL = self::LogoutManual, - INACTIVITY = self::LogoutInactivity; - + /** @deprecated use User::LogoutManual */ public const LOGOUT_MANUAL = self::LogoutManual; + + /** @deprecated use User::LogoutManual */ + public const MANUAL = self::LogoutManual; + + /** @deprecated use User::LogoutInactivity */ public const LOGOUT_INACTIVITY = self::LogoutInactivity; + /** @deprecated use User::LogoutInactivity */ + public const INACTIVITY = self::LogoutInactivity; + /** default role for unauthenticated user */ public string $guestRole = 'guest'; From dd295ca5872e90b70e1e98cada621865ebe98148 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 26 Apr 2024 16:01:01 +0200 Subject: [PATCH 13/53] cs --- src/Security/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Security/User.php b/src/Security/User.php index afeb67bb..9ac3fcdb 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -51,10 +51,10 @@ class User /** default role for authenticated user without own identity */ public string $authenticatedRole = 'authenticated'; - /** @var callable[] function (User $sender): void; Occurs when the user is successfully logged in */ + /** @var array Occurs when the user is successfully logged in */ public array $onLoggedIn = []; - /** @var callable[] function (User $sender): void; Occurs when the user is logged out */ + /** @var array Occurs when the user is logged out */ public array $onLoggedOut = []; private ?IIdentity $identity = null; From eee50d913061e0fc435c2571bd601d9a448a042b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 2 May 2024 12:59:56 +0200 Subject: [PATCH 14/53] github actions updated --- .github/workflows/coding-style.yml | 4 ++-- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 97895fbe..8c60f763 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -7,7 +7,7 @@ jobs: name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 @@ -21,7 +21,7 @@ jobs: name: Nette Coding Standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index d385b9c2..c23cc2f3 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,7 +10,7 @@ jobs: name: PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 144b3690..fd95d74b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: name: PHP ${{ matrix.php }} tests steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -32,7 +32,7 @@ jobs: name: Lowest Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 @@ -46,7 +46,7 @@ jobs: name: Code Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 From 6cc63f2060d061e3c534f1b5afae2fb90c252c24 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 18 Jun 2024 23:27:19 +0200 Subject: [PATCH 15/53] support for PHP 8.4 --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- readme.md | 2 +- src/Security/Permission.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fd95d74b..9637f74c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3', '8.4'] fail-fast: false diff --git a/composer.json b/composer.json index 2f352bdc..151d792c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "8.1 - 8.3", + "php": "8.1 - 8.4", "nette/utils": "^4.0" }, "require-dev": { diff --git a/readme.md b/readme.md index 47b2f6d3..d88b02a5 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ Authentication & Authorization library for Nette. Documentation can be found on the [website](https://doc.nette.org/access-control). -It requires PHP version 8.1 and supports PHP up to 8.3. +It requires PHP version 8.1 and supports PHP up to 8.4. [Support Me](https://github.com/sponsors/dg) diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 888e85a2..5dab4238 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -50,7 +50,7 @@ class Permission implements Authorizator * @throws Nette\InvalidArgumentException * @throws Nette\InvalidStateException */ - public function addRole(string $role, string|array $parents = null): static + public function addRole(string $role, string|array|null $parents = null): static { $this->checkRole($role, exists: false); if (isset($this->roles[$role])) { From 4391db383d789a2ba028ab9b36eb1d49ee1672da Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 4 Nov 2024 13:10:17 +0100 Subject: [PATCH 16/53] typo [Closes #75] --- src/Bridges/SecurityHttp/SessionStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index e0ab56ec..b1ae9e25 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -145,7 +145,7 @@ protected function getSessionSection(): ?SessionSection } } - $section->set('expireTime', time() + $section->expireDelta); // sliding expiration + $section->set('expireTime', time() + $section->get('expireDelta')); // sliding expiration } if (!$section->get('authenticated')) { From d698efe196f8a863a8f480e39f3eeffa0ee53da0 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 4 Nov 2024 13:16:42 +0100 Subject: [PATCH 17/53] SecurityExtension: password can be dynamic [Closes #74] --- src/Bridges/SecurityDI/SecurityExtension.php | 4 ++-- tests/Security.DI/SecurityExtension.authenticator.phpt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 339ec43d..750bcb57 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -34,9 +34,9 @@ public function getConfigSchema(): Nette\Schema\Schema 'debugger' => Expect::bool(), 'users' => Expect::arrayOf( Expect::anyOf( - Expect::string(), // user => password + Expect::string()->dynamic(), // user => password Expect::structure([ // user => password + roles + data - 'password' => Expect::string(), + 'password' => Expect::string()->dynamic(), 'roles' => Expect::anyOf(Expect::string(), Expect::listOf('string')), 'data' => Expect::array(), ])->castTo('array'), diff --git a/tests/Security.DI/SecurityExtension.authenticator.phpt b/tests/Security.DI/SecurityExtension.authenticator.phpt index ececfc23..fd159549 100644 --- a/tests/Security.DI/SecurityExtension.authenticator.phpt +++ b/tests/Security.DI/SecurityExtension.authenticator.phpt @@ -29,6 +29,8 @@ security: admin: {password: admin123, roles: [admin, user]} user: {password: user123} moderator: {password: moderator123, roles: moderator} + dynamic1: ::trim(xxx) + dynamic2: {password: ::trim(xxx)} ', 'neon')); eval($compiler->addConfig($config)->compile()); @@ -43,12 +45,16 @@ $userList = [ 'admin' => 'admin123', 'user' => 'user123', 'moderator' => 'moderator123', + 'dynamic1' => 'xxx', + 'dynamic2' => 'xxx', ]; $expectedRoles = [ 'john' => [], 'admin' => ['admin', 'user'], 'user' => [], 'moderator' => ['moderator'], + 'dynamic1' => [], + 'dynamic2' => [], ]; foreach ($userList as $username => $password) { From 6e19bf604934aec0cd3343a307e28fd997e40e96 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 4 Nov 2024 13:21:21 +0100 Subject: [PATCH 18/53] $user => $username [Closes #73] --- src/Security/Authenticator.php | 2 +- src/Security/User.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Security/Authenticator.php b/src/Security/Authenticator.php index 96829284..500331de 100644 --- a/src/Security/Authenticator.php +++ b/src/Security/Authenticator.php @@ -38,5 +38,5 @@ interface Authenticator extends IAuthenticator * Performs an authentication. * @throws AuthenticationException */ - function authenticate(string $user, string $password): IIdentity; + function authenticate(string $username, string $password): IIdentity; } diff --git a/src/Security/User.php b/src/Security/User.php index 9ac3fcdb..3fd6ad73 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -81,18 +81,18 @@ final public function getStorage(): UserStorage /** * Conducts the authentication process. Parameters are optional. - * @param string|IIdentity $user name or Identity + * @param string|IIdentity $username name or Identity * @throws AuthenticationException if authentication was not successful */ public function login( - string|IIdentity $user, + string|IIdentity $username, #[\SensitiveParameter] ?string $password = null, ): void { $this->logout(true); - if ($user instanceof IIdentity) { - $this->identity = $user; + if ($username instanceof IIdentity) { + $this->identity = $username; } else { $authenticator = $this->getAuthenticator(); $this->identity = $authenticator instanceof Authenticator From 270823b48574ed77c63c162e5d3e2d0dc90e8697 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 3 Feb 2025 03:31:59 +0100 Subject: [PATCH 19/53] github actions updated --- .github/workflows/coding-style.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 8c60f763..a8f19b11 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9637f74c..9b2ee263 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,9 +22,9 @@ jobs: - run: composer install --no-progress --prefer-dist - run: vendor/bin/tester tests -s -C - if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: output + name: output-${{ matrix.php }} path: tests/**/output From 286554b7e313e4a7e3e9bffc792da35e1b2440fb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 6 Jun 2025 01:05:43 +0200 Subject: [PATCH 20/53] composer: require stable packages outside of nette --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 151d792c..4301ed9e 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,8 @@ "nette/http": "^3.2", "nette/tester": "^2.5", "tracy/tracy": "^2.9", - "phpstan/phpstan-nette": "^1.0", - "mockery/mockery": "^1.5" + "phpstan/phpstan-nette": "^2.0@stable", + "mockery/mockery": "^1.6@stable" }, "conflict": { "nette/di": "<3.0-stable", From b6b0ddd455176daf213f662122eb034dad352d63 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 6 Jun 2025 01:20:40 +0200 Subject: [PATCH 21/53] composer: added psr-4 loader --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4301ed9e..d3062dc7 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,10 @@ "nette/http": "<3.1.3" }, "autoload": { - "classmap": ["src/"] + "classmap": ["src/"], + "psr-4": { + "Nette\\": "src" + } }, "minimum-stability": "dev", "scripts": { From 4950b6f42bf632aeeb0254ed9a99fe039321977e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 19 Jun 2025 19:47:28 +0200 Subject: [PATCH 22/53] optimized global function calls --- src/Bridges/SecurityDI/SecurityExtension.php | 1 + src/Bridges/SecurityHttp/CookieStorage.php | 1 + src/Bridges/SecurityHttp/SessionStorage.php | 1 + src/Bridges/SecurityTracy/UserPanel.php | 1 + src/Security/Identity.php | 2 ++ src/Security/Passwords.php | 1 + src/Security/Permission.php | 1 + src/Security/User.php | 1 + 8 files changed, 9 insertions(+) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 750bcb57..9d689fc4 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -12,6 +12,7 @@ use Nette; use Nette\Schema\Expect; use Tracy; +use function is_array; /** diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 70c15a47..8882b19d 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -12,6 +12,7 @@ use Nette; use Nette\Http; use Nette\Security\IIdentity; +use function is_string, strlen; /** diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index b1ae9e25..4023269d 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -14,6 +14,7 @@ use Nette\Http\SessionSection; use Nette\Security\IIdentity; use Nette\Security\User; +use function is_bool, time; /** diff --git a/src/Bridges/SecurityTracy/UserPanel.php b/src/Bridges/SecurityTracy/UserPanel.php index 4cf753a1..ac86fbf5 100644 --- a/src/Bridges/SecurityTracy/UserPanel.php +++ b/src/Bridges/SecurityTracy/UserPanel.php @@ -11,6 +11,7 @@ use Nette; use Tracy; +use const PHP_SESSION_ACTIVE; /** diff --git a/src/Security/Identity.php b/src/Security/Identity.php index ee00111c..73d6b263 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -9,6 +9,8 @@ namespace Nette\Security; +use function in_array, is_float, is_numeric, iterator_to_array; + /** * @deprecated use Nette\Security\SimpleIdentity diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index ffc4927f..570f75ff 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -10,6 +10,7 @@ namespace Nette\Security; use Nette; +use const PASSWORD_DEFAULT; /** diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 5dab4238..6d53af38 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -10,6 +10,7 @@ namespace Nette\Security; use Nette; +use function array_keys, array_pop, count, is_array; /** diff --git a/src/Security/User.php b/src/Security/User.php index 3fd6ad73..83d460e2 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -11,6 +11,7 @@ use Nette; use Nette\Utils\Arrays; +use function func_get_args; /** From 3bc104f3377433af86ce519002a19f6378bdc9f3 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 1 Aug 2025 04:14:57 +0200 Subject: [PATCH 23/53] support for PHP 8.5 --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- readme.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b2ee263..8ed81e7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3', '8.4'] + php: ['8.1', '8.2', '8.3', '8.4', '8.5'] fail-fast: false diff --git a/composer.json b/composer.json index d3062dc7..6e5e7ece 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "8.1 - 8.4", + "php": "8.1 - 8.5", "nette/utils": "^4.0" }, "require-dev": { diff --git a/readme.md b/readme.md index d88b02a5..45be02fa 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ Authentication & Authorization library for Nette. Documentation can be found on the [website](https://doc.nette.org/access-control). -It requires PHP version 8.1 and supports PHP up to 8.4. +It requires PHP version 8.1 and supports PHP up to 8.5. [Support Me](https://github.com/sponsors/dg) From beca6757457281ebc9428743bec7960809f40d49 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Jan 2025 07:16:41 +0100 Subject: [PATCH 24/53] UserPanel: convert templates to Latte-like syntax --- src/Bridges/SecurityTracy/UserPanel.php | 4 ++-- src/Bridges/SecurityTracy/dist/panel.phtml | 13 +++++++++++++ src/Bridges/SecurityTracy/dist/tab.phtml | 10 ++++++++++ src/Bridges/SecurityTracy/panel.latte | 9 +++++++++ src/Bridges/SecurityTracy/tab.latte | 7 +++++++ .../SecurityTracy/templates/UserPanel.panel.phtml | 13 ------------- .../SecurityTracy/templates/UserPanel.tab.phtml | 11 ----------- 7 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 src/Bridges/SecurityTracy/dist/panel.phtml create mode 100644 src/Bridges/SecurityTracy/dist/tab.phtml create mode 100644 src/Bridges/SecurityTracy/panel.latte create mode 100644 src/Bridges/SecurityTracy/tab.latte delete mode 100644 src/Bridges/SecurityTracy/templates/UserPanel.panel.phtml delete mode 100644 src/Bridges/SecurityTracy/templates/UserPanel.tab.phtml diff --git a/src/Bridges/SecurityTracy/UserPanel.php b/src/Bridges/SecurityTracy/UserPanel.php index ac86fbf5..39fbef4f 100644 --- a/src/Bridges/SecurityTracy/UserPanel.php +++ b/src/Bridges/SecurityTracy/UserPanel.php @@ -41,7 +41,7 @@ public function getTab(): ?string $status = session_status() === PHP_SESSION_ACTIVE ? $this->user->isLoggedIn() : '?'; - require __DIR__ . '/templates/UserPanel.tab.phtml'; + require __DIR__ . '/dist/tab.phtml'; }); } @@ -57,7 +57,7 @@ public function getPanel(): ?string return Nette\Utils\Helpers::capture(function () { $user = $this->user; - require __DIR__ . '/templates/UserPanel.panel.phtml'; + require __DIR__ . '/dist/panel.phtml'; }); } } diff --git a/src/Bridges/SecurityTracy/dist/panel.phtml b/src/Bridges/SecurityTracy/dist/panel.phtml new file mode 100644 index 00000000..5173af7c --- /dev/null +++ b/src/Bridges/SecurityTracy/dist/panel.phtml @@ -0,0 +1,13 @@ + +

isLoggedIn()): ?> +Logged in +Unlogged +

+ +
+getIdentity()): ?> getIdentity(), [Tracy\Dumper::LIVE => true]) ?> + +

no identity

+
diff --git a/src/Bridges/SecurityTracy/dist/tab.phtml b/src/Bridges/SecurityTracy/dist/tab.phtml new file mode 100644 index 00000000..0b3a2f70 --- /dev/null +++ b/src/Bridges/SecurityTracy/dist/tab.phtml @@ -0,0 +1,10 @@ + + 'Logged in', false => 'Unlogged', '?' => 'Session is closed'] ?> '#61A519', false => '#ababab', '?' => '#bb0000'] ?> + + + + diff --git a/src/Bridges/SecurityTracy/panel.latte b/src/Bridges/SecurityTracy/panel.latte new file mode 100644 index 00000000..7ba7c784 --- /dev/null +++ b/src/Bridges/SecurityTracy/panel.latte @@ -0,0 +1,9 @@ +

{if $user->isLoggedIn()}Logged in{else}Unlogged{/if}

+ +
+ {if $user->getIdentity()} + {Tracy\Dumper::toHtml($user->getIdentity(), [Tracy\Dumper::LIVE => true])} + {else} +

no identity

+ {/if} +
diff --git a/src/Bridges/SecurityTracy/tab.latte b/src/Bridges/SecurityTracy/tab.latte new file mode 100644 index 00000000..f745d1ae --- /dev/null +++ b/src/Bridges/SecurityTracy/tab.latte @@ -0,0 +1,7 @@ +{do $messages = [true => 'Logged in', false => Unlogged, '?' => 'Session is closed']} +{do $colors = [true => '#61A519', false => '#ababab', '?' => '#bb0000']} + + + + + diff --git a/src/Bridges/SecurityTracy/templates/UserPanel.panel.phtml b/src/Bridges/SecurityTracy/templates/UserPanel.panel.phtml deleted file mode 100644 index 18e3c954..00000000 --- a/src/Bridges/SecurityTracy/templates/UserPanel.panel.phtml +++ /dev/null @@ -1,13 +0,0 @@ - -
-

isLoggedIn()): ?>Logged inUnlogged

- - getIdentity()): echo Dumper::toHtml($user->getIdentity(), [Dumper::LIVE => true]); else: ?>

no identity

-
diff --git a/src/Bridges/SecurityTracy/templates/UserPanel.tab.phtml b/src/Bridges/SecurityTracy/templates/UserPanel.tab.phtml deleted file mode 100644 index 4571fbda..00000000 --- a/src/Bridges/SecurityTracy/templates/UserPanel.tab.phtml +++ /dev/null @@ -1,11 +0,0 @@ - 'Logged in', false => 'Unlogged', '?' => 'Session is closed']; -$color = [true => '#61A519', false => '#ababab', '?' => '#bb0000']; -?> - - - From 87b155f2899a4bca4aeccbfe3a6c5bdad28247f4 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 23 Feb 2026 04:50:25 +0100 Subject: [PATCH 25/53] singleline declare statements --- src/Bridges/SecurityDI/SecurityExtension.php | 4 +--- src/Bridges/SecurityHttp/CookieStorage.php | 4 +--- src/Bridges/SecurityHttp/SessionStorage.php | 4 +--- src/Bridges/SecurityTracy/UserPanel.php | 4 +--- src/Bridges/SecurityTracy/dist/panel.phtml | 4 +--- src/Bridges/SecurityTracy/dist/tab.phtml | 4 +--- src/Security/AuthenticationException.php | 4 +--- src/Security/Authenticator.php | 4 +--- src/Security/Authorizator.php | 4 +--- src/Security/IAuthenticator.php | 4 +--- src/Security/IIdentity.php | 4 +--- src/Security/Identity.php | 4 +--- src/Security/IdentityHandler.php | 4 +--- src/Security/Passwords.php | 4 +--- src/Security/Permission.php | 4 +--- src/Security/Resource.php | 4 +--- src/Security/Role.php | 4 +--- src/Security/SimpleAuthenticator.php | 4 +--- src/Security/SimpleIdentity.php | 4 +--- src/Security/User.php | 4 +--- src/Security/UserStorage.php | 4 +--- src/compatibility.php | 4 +--- tests/Security.DI/SecurityExtension.authenticator.phpt | 4 +--- tests/Security.DI/SecurityExtension.authorizator.phpt | 4 +--- tests/Security.DI/SecurityExtension.cookieStorage.phpt | 4 +--- tests/Security.DI/SecurityExtension.passwords.phpt | 4 +--- tests/Security.DI/SecurityExtension.sessionStorage.phpt | 4 +--- tests/Security.DI/SecurityExtension.user.phpt | 4 +--- tests/Security.Http/CookieStorage.authentication.phpt | 4 +--- tests/Security.Http/CookieStorage.getState.phpt | 4 +--- tests/Security/Identity.phpt | 4 +--- tests/Security/MockUserStorage.php | 4 +--- tests/Security/Passwords.hash().phpt | 4 +--- tests/Security/Passwords.needsRehash().phpt | 4 +--- tests/Security/Passwords.verify().phpt | 4 +--- tests/Security/Permission.CMSExample.phpt | 4 +--- tests/Security/Permission.DefaultAssert.phpt | 4 +--- tests/Security/Permission.DefaultDeny.phpt | 4 +--- tests/Security/Permission.DefaultRuleSet.phpt | 4 +--- tests/Security/Permission.IsAllowedNonExistent.phpt | 4 +--- tests/Security/Permission.PrivilegeAllow.phpt | 4 +--- tests/Security/Permission.PrivilegeAssert.phpt | 4 +--- tests/Security/Permission.PrivilegeDeny.phpt | 4 +--- tests/Security/Permission.Privileges.phpt | 4 +--- tests/Security/Permission.RemoveDefaultAllow.phpt | 4 +--- tests/Security/Permission.RemoveDefaultAllowNonExistent.phpt | 4 +--- tests/Security/Permission.RemoveDefaultDeny.phpt | 4 +--- tests/Security/Permission.RemoveDefaultDenyAssert.phpt | 4 +--- tests/Security/Permission.RemoveDefaultDenyNonExistent.phpt | 4 +--- ...ion.RemovingRoleAfterItWasAllowedAccessToAllResources.phpt | 4 +--- tests/Security/Permission.ResourceAddAndGetOne.phpt | 4 +--- tests/Security/Permission.ResourceAddInheritsNonExistent.phpt | 4 +--- tests/Security/Permission.ResourceDuplicate.phpt | 4 +--- tests/Security/Permission.ResourceInherits.phpt | 4 +--- tests/Security/Permission.ResourceInheritsNonExistent.phpt | 4 +--- tests/Security/Permission.ResourceRemoveAll.phpt | 4 +--- tests/Security/Permission.ResourceRemoveOneNonExistent.phpt | 4 +--- .../Permission.RoleDefaultAllowRuleWithPrivilegeDenyRule.phpt | 4 +--- .../Permission.RoleDefaultAllowRuleWithResourceDenyRule.phpt | 4 +--- tests/Security/Permission.RoleDefaultRuleSet.phpt | 4 +--- tests/Security/Permission.RoleDefaultRuleSetPrivilege.phpt | 4 +--- tests/Security/Permission.RolePrivilegeAllow.phpt | 4 +--- tests/Security/Permission.RolePrivilegeAssert.phpt | 4 +--- tests/Security/Permission.RolePrivilegeDeny.phpt | 4 +--- tests/Security/Permission.RolePrivileges.phpt | 4 +--- tests/Security/Permission.RoleRegistryAddAndGetOne.phpt | 4 +--- .../Permission.RoleRegistryAddInheritsNonExistent.phpt | 4 +--- tests/Security/Permission.RoleRegistryDuplicate.phpt | 4 +--- tests/Security/Permission.RoleRegistryInherits.phpt | 4 +--- tests/Security/Permission.RoleRegistryInheritsMultiple.phpt | 4 +--- .../Security/Permission.RoleRegistryInheritsNonExistent.phpt | 4 +--- tests/Security/Permission.RoleRegistryRemoveAll.phpt | 4 +--- .../Security/Permission.RoleRegistryRemoveOneNonExistent.phpt | 4 +--- tests/Security/Permission.RuleRoleRemove.phpt | 4 +--- tests/Security/Permission.RuleRoleRemoveAll.phpt | 4 +--- tests/Security/Permission.RulesRemove.phpt | 4 +--- tests/Security/Permission.RulesResourceRemove.phpt | 4 +--- tests/Security/Permission.RulesResourceRemoveAll.phpt | 4 +--- tests/Security/SimpleAuthenticator.Data.phpt | 4 +--- tests/Security/SimpleAuthenticator.Roles.phpt | 4 +--- tests/Security/SimpleAuthenticator.phpt | 4 +--- tests/Security/SimpleIdentity.phpt | 4 +--- tests/Security/User.authentication.phpt | 4 +--- tests/Security/User.authorization.phpt | 4 +--- tests/bootstrap.php | 4 +--- 85 files changed, 85 insertions(+), 255 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 9d689fc4..b4535453 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -1,12 +1,10 @@ - +

isLoggedIn()): ?> Logged in Unlogged diff --git a/src/Bridges/SecurityTracy/dist/tab.phtml b/src/Bridges/SecurityTracy/dist/tab.phtml index 0b3a2f70..84b088c2 100644 --- a/src/Bridges/SecurityTracy/dist/tab.phtml +++ b/src/Bridges/SecurityTracy/dist/tab.phtml @@ -1,6 +1,4 @@ - + 'Logged in', false => 'Unlogged', '?' => 'Session is closed'] ?> '#61A519', false => '#ababab', '?' => '#bb0000'] ?> diff --git a/src/Security/AuthenticationException.php b/src/Security/AuthenticationException.php index 79c90b44..2a03b5bf 100644 --- a/src/Security/AuthenticationException.php +++ b/src/Security/AuthenticationException.php @@ -1,12 +1,10 @@ - Date: Sat, 27 Dec 2025 02:24:15 +0100 Subject: [PATCH 26/53] cs --- src/Bridges/SecurityDI/SecurityExtension.php | 9 +++------ src/Bridges/SecurityHttp/CookieStorage.php | 14 ++++++-------- src/Bridges/SecurityHttp/SessionStorage.php | 9 ++++----- src/Bridges/SecurityTracy/UserPanel.php | 9 +++------ src/Security/Identity.php | 4 ++-- src/Security/Passwords.php | 5 ++--- src/Security/User.php | 8 ++++---- 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index b4535453..c6831230 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -18,12 +18,9 @@ */ class SecurityExtension extends Nette\DI\CompilerExtension { - private bool $debugMode; - - - public function __construct(bool $debugMode = false) - { - $this->debugMode = $debugMode; + public function __construct( + private readonly bool $debugMode = false, + ) { } diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 73475beb..3a814e13 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -19,9 +19,6 @@ final class CookieStorage implements Nette\Security\UserStorage { private const MinLength = 13; - - private Http\IRequest $request; - private Http\IResponse $response; private ?string $uid = null; private string $cookieName = 'userid'; private ?string $cookieDomain = null; @@ -29,10 +26,10 @@ final class CookieStorage implements Nette\Security\UserStorage private ?string $cookieExpiration = null; - public function __construct(Http\IRequest $request, Http\IResponse $response) - { - $this->response = $response; - $this->request = $request; + public function __construct( + private readonly Http\IRequest $request, + private readonly Http\IResponse $response, + ) { } @@ -91,7 +88,8 @@ public function setCookieParameters( ?string $name = null, ?string $domain = null, ?string $sameSite = null, - ) { + ): void + { $this->cookieName = $name ?? $this->cookieName; $this->cookieDomain = $domain ?? $this->cookieDomain; $this->cookieSameSite = $sameSite ?? $this->cookieSameSite; diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index 5d0e7670..f9167652 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -21,15 +21,14 @@ final class SessionStorage implements Nette\Security\UserStorage { private string $namespace = ''; - private Session $sessionHandler; private ?SessionSection $sessionSection = null; private ?int $expireTime = null; private bool $expireIdentity = false; - public function __construct(Session $sessionHandler) - { - $this->sessionHandler = $sessionHandler; + public function __construct( + private readonly Session $sessionHandler, + ) { } @@ -123,7 +122,7 @@ public function getNamespace(): string /** * Returns and initializes $this->sessionSection. */ - protected function getSessionSection(): ?SessionSection + private function getSessionSection(): ?SessionSection { if ($this->sessionSection !== null) { return $this->sessionSection; diff --git a/src/Bridges/SecurityTracy/UserPanel.php b/src/Bridges/SecurityTracy/UserPanel.php index cfc9a34e..75a523d8 100644 --- a/src/Bridges/SecurityTracy/UserPanel.php +++ b/src/Bridges/SecurityTracy/UserPanel.php @@ -17,12 +17,9 @@ */ class UserPanel implements Tracy\IBarPanel { - private Nette\Security\User $user; - - - public function __construct(Nette\Security\User $user) - { - $this->user = $user; + public function __construct( + private readonly Nette\Security\User $user, + ) { } diff --git a/src/Security/Identity.php b/src/Security/Identity.php index eb3368b0..3d3339c1 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -23,7 +23,7 @@ class Identity implements IIdentity private array $data; - public function __construct($id, $roles = null, ?iterable $data = null) + public function __construct(string|int $id, $roles = null, ?iterable $data = null) { $this->setId($id); $this->setRoles((array) $roles); @@ -83,7 +83,7 @@ public function getData(): array /** * Sets user data value. */ - public function __set(string $key, $value): void + public function __set(string $key, mixed $value): void { if (in_array($key, ['id', 'roles', 'data'], strict: true)) { $this->{"set$key"}($value); diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 940904ef..5c6c9d32 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -8,7 +8,6 @@ namespace Nette\Security; use Nette; -use const PASSWORD_DEFAULT; /** @@ -21,8 +20,8 @@ class Passwords * @see https://php.net/manual/en/password.constants.php */ public function __construct( - private string $algo = PASSWORD_DEFAULT, - private array $options = [], + private readonly string $algo = PASSWORD_DEFAULT, + private readonly array $options = [], ) { } diff --git a/src/Security/User.php b/src/Security/User.php index 1160c4ec..753c03d7 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -89,7 +89,7 @@ public function login( ?string $password = null, ): void { - $this->logout(true); + $this->logout(clearIdentity: true); if ($username instanceof IIdentity) { $this->identity = $username; } else { @@ -226,7 +226,7 @@ final public function hasAuthenticator(): bool /** * Enables log out after inactivity (like '20 minutes'). */ - public function setExpiration(?string $expire, bool $clearIdentity = false) + public function setExpiration(?string $expire, bool $clearIdentity = false): static { $this->storage->setExpiration($expire, $clearIdentity); return $this; @@ -255,7 +255,7 @@ public function getRoles(): array } $identity = $this->getIdentity(); - return $identity && $identity->getRoles() ? $identity->getRoles() : [$this->authenticatedRole]; + return $identity?->getRoles() ?? [$this->authenticatedRole]; } @@ -278,7 +278,7 @@ final public function isInRole(string $role): bool * Has a user effective access to the Resource? * If $resource is null, then the query applies to all resources. */ - public function isAllowed($resource = Authorizator::All, $privilege = Authorizator::All): bool + public function isAllowed(mixed $resource = Authorizator::All, mixed $privilege = Authorizator::All): bool { foreach ($this->getRoles() as $role) { if ($this->getAuthorizator()->isAllowed($role, $resource, $privilege)) { From eafdad1f21646386efe092c7d6e1a73ec8dc894a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 01:53:07 +0100 Subject: [PATCH 27/53] updated .gitattributes --- .gitattributes | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9670e954..433a2de9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,10 @@ -.gitattributes export-ignore -.gitignore export-ignore -.github export-ignore -ncs.* export-ignore -phpstan.neon export-ignore -tests/ export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore +src/**/*.latte export-ignore +tests/ export-ignore -*.sh eol=lf -*.php* diff=php linguist-language=PHP +*.php* diff=php +*.sh text eol=lf From 339b174fb81663e821f8379e420de8c2ca69a001 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 27 Mar 2026 01:34:35 +0100 Subject: [PATCH 28/53] updated github actions --- .github/workflows/coding-style.yml | 4 ++-- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index a8f19b11..f553ca17 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -7,7 +7,7 @@ jobs: name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.3 @@ -21,7 +21,7 @@ jobs: name: Nette Coding Standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.3 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index c23cc2f3..674979c7 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,7 +10,7 @@ jobs: name: PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ed81e7e..c2b773f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: name: PHP ${{ matrix.php }} tests steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -32,7 +32,7 @@ jobs: name: Lowest Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 @@ -46,7 +46,7 @@ jobs: name: Code Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 From 726ac3817cfdb37f591aeefaf7048e98d1d02cb3 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 29 Dec 2025 01:31:43 +0100 Subject: [PATCH 29/53] improved tests --- .github/workflows/tests.yml | 7 +- composer.json | 2 +- .../SessionStorage.expiration.phpt | 132 ++++++++ tests/Security/Passwords.needsRehash().phpt | 95 +++++- .../Permission.AssertionWithQueried.phpt | 313 ++++++++++++++++++ tests/Security/User.expiration.phpt | 76 +++++ tests/Security/User.identityHandler.phpt | 175 ++++++++++ tests/Security/User.namespaces.phpt | 290 ++++++++++++++++ tests/Security/User.refreshStorage.phpt | 304 +++++++++++++++++ tests/bootstrap.php | 7 +- 10 files changed, 1388 insertions(+), 13 deletions(-) create mode 100644 tests/Security.Http/SessionStorage.expiration.phpt create mode 100644 tests/Security/Permission.AssertionWithQueried.phpt create mode 100644 tests/Security/User.expiration.phpt create mode 100644 tests/Security/User.identityHandler.phpt create mode 100644 tests/Security/User.namespaces.phpt create mode 100644 tests/Security/User.refreshStorage.phpt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2b773f5..5ee52c1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -s -C + - run: composer tester - if: failure() uses: actions/upload-artifact@v4 with: @@ -39,12 +39,13 @@ jobs: coverage: none - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - run: vendor/bin/tester tests -s -C + - run: composer tester code_coverage: name: Code Coverage runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 @@ -53,7 +54,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src + - run: composer tester -- -p phpdbg --coverage ./coverage.xml --coverage-src ./src - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar - env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/composer.json b/composer.json index 6e5e7ece..cc7a8d5f 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "require-dev": { "nette/di": "^3.1", "nette/http": "^3.2", - "nette/tester": "^2.5", + "nette/tester": "^2.6", "tracy/tracy": "^2.9", "phpstan/phpstan-nette": "^2.0@stable", "mockery/mockery": "^1.6@stable" diff --git a/tests/Security.Http/SessionStorage.expiration.phpt b/tests/Security.Http/SessionStorage.expiration.phpt new file mode 100644 index 00000000..13792ef1 --- /dev/null +++ b/tests/Security.Http/SessionStorage.expiration.phpt @@ -0,0 +1,132 @@ +saveAuthentication(new SimpleIdentity('john')); + Assert::equal([true, new SimpleIdentity('john'), null], $storage->getState()); + + // Create new storage instance (simulates new request) + $storage2 = new SessionStorage($session); + Assert::equal([true, new SimpleIdentity('john'), null], $storage2->getState()); +}); + + +test('Expiration with clearIdentity removes identity on timeout', function () { + $request = new Nette\Http\Request(new Nette\Http\UrlScript('http://localhost')); + $response = new Nette\Http\Response; + $session = new Session($request, $response); + $storage = new SessionStorage($session); + + $storage->setExpiration('1 second', true); + $storage->saveAuthentication(new SimpleIdentity('john')); + + Assert::true($storage->getState()[0]); + Assert::notNull($storage->getState()[1]); + + // Wait for expiration + sleep(2); + + // Create new storage (simulates new request after expiration) + $storage2 = new SessionStorage($session); + [$authenticated, $identity, $reason] = $storage2->getState(); + + Assert::false($authenticated); + Assert::null($identity); // Identity cleared + Assert::same(User::LogoutInactivity, $reason); +}); + + +test('Expiration without clearIdentity keeps identity on timeout', function () { + $request = new Nette\Http\Request(new Nette\Http\UrlScript('http://localhost')); + $response = new Nette\Http\Response; + $session = new Session($request, $response); + $storage = new SessionStorage($session); + + $storage->setExpiration('1 second', false); + $storage->saveAuthentication(new SimpleIdentity('john')); + + Assert::equal([true, new SimpleIdentity('john'), null], $storage->getState()); + + // Wait for expiration + sleep(2); + + // Create new storage (simulates new request after expiration) + $storage2 = new SessionStorage($session); + [$authenticated, $identity, $reason] = $storage2->getState(); + + Assert::false($authenticated); + Assert::equal(new SimpleIdentity('john'), $identity); // Identity still available + Assert::same(User::LogoutInactivity, $reason); +}); + + +test('Sliding expiration extends session on activity', function () { + $request = new Nette\Http\Request(new Nette\Http\UrlScript('http://localhost')); + $response = new Nette\Http\Response; + $session = new Session($request, $response); + $storage = new SessionStorage($session); + + $storage->setExpiration('2 seconds'); + $storage->saveAuthentication(new SimpleIdentity('john')); + + // Activity after 1 second (within window) + sleep(1); + $storage2 = new SessionStorage($session); + Assert::true($storage2->getState()[0]); // Still authenticated + + // Another activity after 1 second (total 2 seconds from login, but 1 from last activity) + sleep(1); + $storage3 = new SessionStorage($session); + Assert::true($storage3->getState()[0]); // Still authenticated (sliding extended it) + + // Wait 3 seconds without activity (exceeds window) + sleep(3); + $storage4 = new SessionStorage($session); + [$authenticated, $identity, $reason] = $storage4->getState(); + + Assert::false($authenticated); + Assert::same(User::LogoutInactivity, $reason); +}); + + +test('setExpiration(null) disables expiration', function () { + $request = new Nette\Http\Request(new Nette\Http\UrlScript('http://localhost')); + $response = new Nette\Http\Response; + $session = new Session($request, $response); + $storage = new SessionStorage($session); + + $storage->setExpiration('1 second'); + $storage->saveAuthentication(new SimpleIdentity('john')); + + // Disable expiration + $storage->setExpiration(null); + + // Wait beyond original limit + sleep(2); + + // Still authenticated + $storage2 = new SessionStorage($session); + Assert::true($storage2->getState()[0]); +}); diff --git a/tests/Security/Passwords.needsRehash().phpt b/tests/Security/Passwords.needsRehash().phpt index fb2f99a9..652da5f2 100644 --- a/tests/Security/Passwords.needsRehash().phpt +++ b/tests/Security/Passwords.needsRehash().phpt @@ -1,7 +1,7 @@ needsRehash('$2y$05$123456789012345678901uTj3G.8OMqoqrOMca1z/iBLqLNaWe6DK')); -Assert::false((new Passwords(PASSWORD_BCRYPT, ['cost' => 5]))->needsRehash('$2y$05$123456789012345678901uTj3G.8OMqoqrOMca1z/iBLqLNaWe6DK')); +test('Complete rehash upgrade flow from old cost to new cost', function () { + // Initial setup with cost 10 + $passwords10 = new Passwords(PASSWORD_BCRYPT, ['cost' => 10]); + $password = 'secret123'; + + // Create hash with cost 10 + $hash10 = $passwords10->hash($password); + + // Verify it works + Assert::true($passwords10->verify($password, $hash10)); + + // Hash is current for cost 10 + Assert::false($passwords10->needsRehash($hash10)); + + // Upgrade to cost 12 + $passwords12 = new Passwords(PASSWORD_BCRYPT, ['cost' => 12]); + + // Old hash should verify (backward compatible) + Assert::true($passwords12->verify($password, $hash10)); + + // But needs rehash (outdated) + Assert::true($passwords12->needsRehash($hash10)); + + // Create new hash with cost 12 + $hash12 = $passwords12->hash($password); + + // New hash should not need rehash + Assert::false($passwords12->needsRehash($hash12)); + + // Both hashes verify the same password + Assert::true($passwords12->verify($password, $hash10)); + Assert::true($passwords12->verify($password, $hash12)); + + // New hash should be longer/different (higher cost) + Assert::notSame($hash10, $hash12); +}); + + +test('Different algorithms trigger needsRehash()', function () { + if (!defined('PASSWORD_ARGON2I')) { + return; + } + + $password = 'test123'; + + // Hash with BCRYPT + $bcryptPasswords = new Passwords(PASSWORD_BCRYPT, ['cost' => 10]); + $bcryptHash = $bcryptPasswords->hash($password); + + // Verify with BCRYPT - no rehash needed + Assert::false($bcryptPasswords->needsRehash($bcryptHash)); + + // Check with ARGON2I - should need rehash (different algorithm) + $argonPasswords = new Passwords(PASSWORD_ARGON2I); + Assert::true($argonPasswords->needsRehash($bcryptHash)); + + // Create ARGON2I hash + $argonHash = $argonPasswords->hash($password); + + // Now ARGON2I doesn't need rehash + Assert::false($argonPasswords->needsRehash($argonHash)); + + // But BCRYPT instance thinks it needs rehash + Assert::true($bcryptPasswords->needsRehash($argonHash)); +}); + + +test('needsRehash() with invalid hash indicates rehash needed', function () { + $passwords = new Passwords(PASSWORD_BCRYPT); + + // Invalid/corrupted hashes should indicate rehash is needed + // (password_needs_rehash returns true for invalid hashes) + Assert::true($passwords->needsRehash('invalid')); + Assert::true($passwords->needsRehash('')); + Assert::true($passwords->needsRehash('$2y$10$tooshort')); +}); + + +test('needsRehash() detects cost decrease (security downgrade)', function () { + $password = 'test'; + + // Create hash with cost 12 + $pw12 = new Passwords(PASSWORD_BCRYPT, ['cost' => 12]); + $hash12 = $pw12->hash($password); + + // Check with cost 8 (downgrade) + $pw8 = new Passwords(PASSWORD_BCRYPT, ['cost' => 8]); + + // Should not indicate rehash needed (to maintain security level) + // Assert::false($pw8->needsRehash($hash12)); // not implemented +}); diff --git a/tests/Security/Permission.AssertionWithQueried.phpt b/tests/Security/Permission.AssertionWithQueried.phpt new file mode 100644 index 00000000..f8ce997f --- /dev/null +++ b/tests/Security/Permission.AssertionWithQueried.phpt @@ -0,0 +1,313 @@ +roleId; + } +} + + +class ArticleResource implements Resource +{ + public function __construct( + public string $resourceId, + public int $authorId, + ) { + } + + + public function getResourceId(): string + { + return $this->resourceId; + } +} + + +test('Assertion can access queried role and resource objects', function () { + $acl = new Permission; + + $user1 = new UserRole('user1', 123); + $user2 = new UserRole('user2', 456); + + $article1 = new ArticleResource('article1', 123); // authored by user1 + $article2 = new ArticleResource('article2', 456); // authored by user2 + + // addRole() accepts only strings + $acl->addRole('user1'); + $acl->addRole('user2'); + $acl->addResource('article1'); + $acl->addResource('article2'); + + // Assertion: user can edit only their own articles + $ownerAssertion = function (Permission $acl): bool { + $role = $acl->getQueriedRole(); + $resource = $acl->getQueriedResource(); + + // When isAllowed() is called with objects, getQueried* returns those objects + // But they might also be strings if called with strings + if ($role instanceof UserRole && $resource instanceof ArticleResource) { + return $role->userId === $resource->authorId; + } + + return false; // Deny if not using objects + }; + + // Allow rules are set using string IDs + $acl->allow('user1', 'article1', 'edit', $ownerAssertion); + $acl->allow('user1', 'article2', 'edit', $ownerAssertion); + $acl->allow('user2', 'article1', 'edit', $ownerAssertion); + $acl->allow('user2', 'article2', 'edit', $ownerAssertion); + + // user1 can edit article1 (their own) + Assert::true($acl->isAllowed($user1, $article1, 'edit')); + + // user1 cannot edit article2 (belongs to user2) + Assert::false($acl->isAllowed($user1, $article2, 'edit')); + + // user2 can edit article2 (their own) + Assert::true($acl->isAllowed($user2, $article2, 'edit')); + + // user2 cannot edit article1 (belongs to user1) + Assert::false($acl->isAllowed($user2, $article1, 'edit')); +}); + + +test('getQueriedRole() returns string when queried with string', function () { + $acl = new Permission; + + $acl->addRole('admin'); + $acl->addResource('article'); + + $assertion = function (Permission $acl): bool { + $role = $acl->getQueriedRole(); + Assert::same('admin', $role); + Assert::type('string', $role); + return true; + }; + + $acl->allow('admin', 'article', 'edit', $assertion); + + Assert::true($acl->isAllowed('admin', 'article', 'edit')); +}); + + +test('getQueriedResource() returns string when queried with string', function () { + $acl = new Permission; + + $acl->addRole('admin'); + $acl->addResource('article'); + + $assertion = function (Permission $acl): bool { + $resource = $acl->getQueriedResource(); + Assert::same('article', $resource); + Assert::type('string', $resource); + return true; + }; + + $acl->allow('admin', 'article', 'edit', $assertion); + + Assert::true($acl->isAllowed('admin', 'article', 'edit')); +}); + + +test('Assertion with complex business logic using queried objects', function () { + $acl = new Permission; + + // Simulate a blog system with posts and comments + class Post implements Resource + { + public function __construct( + public string $resourceId, + public int $authorId, + public bool $published, + public int $categoryId, + ) { + } + + + public function getResourceId(): string + { + return $this->resourceId; + } + } + + class Author implements Role + { + public function __construct( + public string $roleId, + public int $id, + public bool $isPremium, + public array $allowedCategories, + ) { + } + + + public function getRoleId(): string + { + return $this->roleId; + } + } + + $premiumAuthor = new Author('author1', 100, true, [1, 2, 3]); + $regularAuthor = new Author('author2', 200, false, [1]); + + $post1 = new Post('post1', 100, true, 1); // by premium author, published, category 1 + $post2 = new Post('post2', 200, false, 2); // by regular author, draft, category 2 + $post3 = new Post('post3', 999, true, 3); // by someone else, published, category 3 + + // addRole/addResource accept only strings + $acl->addRole('author1'); + $acl->addRole('author2'); + $acl->addResource('post1'); + $acl->addResource('post2'); + $acl->addResource('post3'); + + // Complex assertion: can edit if: + // 1. Own post, OR + // 2. Premium user AND post is unpublished AND category allowed + $editAssertion = function (Permission $acl): bool { + $author = $acl->getQueriedRole(); + $post = $acl->getQueriedResource(); + + // When using objects, assertion receives the actual objects + if (!($author instanceof Author && $post instanceof Post)) { + return false; + } + + // Own post - always allowed + if ($author->id === $post->authorId) { + return true; + } + + // Premium users can edit unpublished posts in their categories + return $author->isPremium && !$post->published && in_array($post->categoryId, $author->allowedCategories, true); + }; + + $acl->allow('author1', ['post1', 'post2', 'post3'], 'edit', $editAssertion); + $acl->allow('author2', ['post1', 'post2', 'post3'], 'edit', $editAssertion); + + // premiumAuthor can edit their own post1 + Assert::true($acl->isAllowed($premiumAuthor, $post1, 'edit')); + + // premiumAuthor can edit post2 (unpublished, category 2 allowed) + Assert::true($acl->isAllowed($premiumAuthor, $post2, 'edit')); + + // premiumAuthor cannot edit post3 (published, even though category allowed) + Assert::false($acl->isAllowed($premiumAuthor, $post3, 'edit')); + + // regularAuthor can edit their own post2 + Assert::true($acl->isAllowed($regularAuthor, $post2, 'edit')); + + // regularAuthor cannot edit post1 (not their own, premium feature) + Assert::false($acl->isAllowed($regularAuthor, $post1, 'edit')); +}); + + +test('getQueriedRole() and getQueriedResource() are available inside assertion', function () { + $acl = new Permission; + + $acl->addRole('user'); + $acl->addResource('article'); + + $assertion = function (Permission $acl): bool { + // Inside assertion - should have values + Assert::notNull($acl->getQueriedRole()); + Assert::notNull($acl->getQueriedResource()); + return true; + }; + + $acl->allow('user', 'article', 'view', $assertion); + + $acl->isAllowed('user', 'article', 'view'); +}); + + +test('Assertion with role inheritance uses actual queried role, not inherited', function () { + $acl = new Permission; + + $admin = new UserRole('admin', 1); + $editor = new UserRole('editor', 2); + + $acl->addRole('editor'); + $acl->addRole('admin', 'editor'); // admin inherits from editor + + $acl->addResource('article'); + + $capturedRoles = []; + + $assertion = function (Permission $acl) use (&$capturedRoles): bool { + $queriedRole = $acl->getQueriedRole(); + if ($queriedRole instanceof UserRole) { + $capturedRoles[] = $queriedRole->getRoleId(); + } + return true; + }; + + $acl->allow('editor', 'article', 'view', $assertion); + + // Query with admin object - should capture 'admin', not 'editor' + $acl->isAllowed($admin, 'article', 'view'); + + Assert::same(['admin'], $capturedRoles); +}); + + +test('Multiple assertions in hierarchy all receive correct queried objects', function () { + $acl = new Permission; + + $user = new UserRole('user', 100); + $article = new ArticleResource('article', 200); + + $acl->addRole('user'); + $acl->addResource('article'); + + $assertion1Calls = 0; + $assertion2Calls = 0; + + $assertion1 = function (Permission $acl) use (&$assertion1Calls, $user, $article): bool { + $assertion1Calls++; + Assert::same($user, $acl->getQueriedRole()); + Assert::same($article, $acl->getQueriedResource()); + return true; + }; + + $assertion2 = function (Permission $acl) use (&$assertion2Calls, $user, $article): bool { + $assertion2Calls++; + Assert::same($user, $acl->getQueriedRole()); + Assert::same($article, $acl->getQueriedResource()); + return true; + }; + + $acl->allow('user', 'article', 'view', $assertion1); + $acl->allow('user', 'article', 'edit', $assertion2); + + $acl->isAllowed($user, $article, 'view'); + Assert::same(1, $assertion1Calls); + Assert::same(0, $assertion2Calls); + + $acl->isAllowed($user, $article, 'edit'); + Assert::same(1, $assertion1Calls); + Assert::same(1, $assertion2Calls); +}); diff --git a/tests/Security/User.expiration.phpt b/tests/Security/User.expiration.phpt new file mode 100644 index 00000000..d38d090b --- /dev/null +++ b/tests/Security/User.expiration.phpt @@ -0,0 +1,76 @@ +authenticated = true; + $this->identity = $identity; + $this->reason = null; + } + + + public function clearAuthentication(bool $clearIdentity): void + { + $this->authenticated = false; + $this->reason = User::LogoutManual; + if ($clearIdentity) { + $this->identity = null; + } + } + + + public function getState(): array + { + return [$this->authenticated, $this->identity, $this->reason]; + } + + + public function setExpiration(?string $time, bool $clearIdentity = false): void + { + $this->expireTime = $time; + $this->expireIdentity = $clearIdentity; + } +} + + +test('User delegates setExpiration to storage', function () { + $storage = new MockUserStorage; + $user = new User($storage); + + $user->setExpiration('30 minutes'); + Assert::same('30 minutes', $storage->expireTime); + Assert::false($storage->expireIdentity); + + $user->setExpiration(null); + Assert::null($storage->expireTime); +}); + + +test('User delegates setExpiration with clearIdentity flag', function () { + $storage = new MockUserStorage; + $user = new User($storage); + + $user->setExpiration('10 minutes', clearIdentity: true); + Assert::same('10 minutes', $storage->expireTime); + Assert::true($storage->expireIdentity); +}); diff --git a/tests/Security/User.identityHandler.phpt b/tests/Security/User.identityHandler.phpt new file mode 100644 index 00000000..56ee376a --- /dev/null +++ b/tests/Security/User.identityHandler.phpt @@ -0,0 +1,175 @@ + 'John Doe']); + } + } + + + public function sleepIdentity(IIdentity $identity): IIdentity + { + $this->sleepCalls[] = $identity; + // Simulate token-only storage (e.g., for cookies) + // Store only ID, not roles or data + return new SimpleIdentity($identity->getId()); + } + + + public function wakeupIdentity(IIdentity $identity): ?IIdentity + { + $this->wakeupCalls[] = $identity; + // Simulate refreshing roles from database + // Real implementation would fetch fresh data from DB + return new SimpleIdentity($identity->getId(), ['admin', 'user'], ['name' => 'John Doe Updated']); + } +} + + +test('IdentityHandler.sleepIdentity() is called on login', function () { + $handler = new AuthenticatorWithHandler; + $user = new Nette\Security\User(new MockUserStorage); + $user->setAuthenticator($handler); + + Assert::count(0, $handler->sleepCalls); + + $user->login('john', 'xxx'); + + // sleepIdentity should be called once + Assert::count(1, $handler->sleepCalls); + + // Original identity should have 'user' role + Assert::same(['user'], $handler->sleepCalls[0]->getRoles()); + Assert::same(['name' => 'John Doe'], $handler->sleepCalls[0]->getData()); +}); + + +test('IdentityHandler.wakeupIdentity() is called when accessing identity', function () { + $storage = new MockUserStorage; + $handler = new AuthenticatorWithHandler; + $user = new Nette\Security\User($storage); + $user->setAuthenticator($handler); + + $user->login('john', 'xxx'); + + // Reset counters + $handler->wakeupCalls = []; + + // Create new User instance with same storage to simulate new request + $user2 = new Nette\Security\User($storage); + $user2->setAuthenticator($handler); + + Assert::count(0, $handler->wakeupCalls); + + // Accessing identity should trigger wakeup + $identity = $user2->getIdentity(); + + Assert::count(1, $handler->wakeupCalls); + + // wakeupIdentity received the "slept" identity (ID only) + Assert::same('john', $handler->wakeupCalls[0]->getId()); + Assert::same([], $handler->wakeupCalls[0]->getRoles()); + + // But returned identity has updated roles from wakeup + Assert::same(['admin', 'user'], $identity->getRoles()); + Assert::same(['name' => 'John Doe Updated'], $identity->getData()); +}); + + +test('IdentityHandler.wakeupIdentity() returning null logs user out', function () { + $handler = new class implements Nette\Security\Authenticator, Nette\Security\IdentityHandler { + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity('john', ['user']); + } + + + public function sleepIdentity(IIdentity $identity): IIdentity + { + return $identity; + } + + + public function wakeupIdentity(IIdentity $identity): ?IIdentity + { + // Simulate invalid token/expired session + return null; + } + }; + + $storage = new MockUserStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator($handler); + + $user->login('john', 'xxx'); + Assert::true($user->isLoggedIn()); + + // Create new User instance to trigger wakeup + $user2 = new Nette\Security\User($storage); + $user2->setAuthenticator($handler); + + // wakeupIdentity returns null → user should be logged out + Assert::false($user2->isLoggedIn()); + Assert::null($user2->getIdentity()); +}); + + +test('IdentityHandler is not called when authenticator does not implement it', function () { + $handler = new class implements Nette\Security\Authenticator { + public int $authCount = 0; + + + public function authenticate(string $username, string $password): IIdentity + { + $this->authCount++; + return new SimpleIdentity('john', ['user']); + } + }; + + $storage = new MockUserStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator($handler); + + $user->login('john', 'xxx'); + Assert::same(1, $handler->authCount); + + // Create new User instance - should just return stored identity without wakeup + $user2 = new Nette\Security\User($storage); + $user2->setAuthenticator($handler); + + $identity = $user2->getIdentity(); + Assert::same('john', $identity->getId()); + Assert::same(['user'], $identity->getRoles()); + + // authenticate should not be called again + Assert::same(1, $handler->authCount); +}); diff --git a/tests/Security/User.namespaces.phpt b/tests/Security/User.namespaces.phpt new file mode 100644 index 00000000..0f072868 --- /dev/null +++ b/tests/Security/User.namespaces.phpt @@ -0,0 +1,290 @@ +namespaces[$this->currentNamespace] = [ + 'authenticated' => true, + 'identity' => $identity, + 'reason' => null, + ]; + } + + + public function clearAuthentication(bool $clearIdentity): void + { + if (isset($this->namespaces[$this->currentNamespace])) { + $this->namespaces[$this->currentNamespace]['authenticated'] = false; + $this->namespaces[$this->currentNamespace]['reason'] = Nette\Security\User::LogoutManual; + if ($clearIdentity) { + $this->namespaces[$this->currentNamespace]['identity'] = null; + } + } + } + + + public function getState(): array + { + $ns = $this->namespaces[$this->currentNamespace] ?? null; + if ($ns === null) { + return [false, null, null]; + } + return [$ns['authenticated'], $ns['identity'], $ns['reason']]; + } + + + public function setExpiration(?string $expire, bool $clearIdentity): void + { + } + + + public function setNamespace(string $namespace): self + { + $this->currentNamespace = $namespace; + return $this; + } + + + public function getNamespace(): string + { + return $this->currentNamespace; + } +} + + +class SimpleAuthenticator implements Nette\Security\Authenticator +{ + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity($username, [$username === 'admin' ? 'admin' : 'user']); + } +} + + +test('Different namespaces have independent authentication states', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + // Login to default namespace + $user->login('customer', 'xxx'); + Assert::true($user->isLoggedIn()); + Assert::same('customer', $user->getId()); + Assert::same(['user'], $user->getRoles()); + + // Switch to 'admin' namespace - should NOT be logged in + $storage->setNamespace('admin'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); + + // Login different user to admin namespace + $user->login('admin', 'xxx'); + Assert::true($user->isLoggedIn()); + Assert::same('admin', $user->getId()); + Assert::same(['admin'], $user->getRoles()); + + // Switch back to default namespace - customer should still be logged in + $storage->setNamespace(''); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('customer', $user->getId()); + Assert::same(['user'], $user->getRoles()); + + // Switch to admin namespace - admin should still be logged in + $storage->setNamespace('admin'); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('admin', $user->getId()); + Assert::same(['admin'], $user->getRoles()); +}); + + +test('Logout in one namespace does not affect other namespaces', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + // Login to frontend + $storage->setNamespace('frontend'); + $user->login('customer', 'xxx'); + Assert::true($user->isLoggedIn()); + + // Login to backend + $storage->setNamespace('backend'); + $user->login('admin', 'xxx'); + Assert::true($user->isLoggedIn()); + + // Logout from backend + $user->logout(); + Assert::false($user->isLoggedIn()); + + // Frontend should still be logged in + $storage->setNamespace('frontend'); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('customer', $user->getId()); +}); + + +test('Identity is preserved after logout when not clearing in one namespace', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + // Login to namespace A + $storage->setNamespace('A'); + $user->login('john', 'xxx'); + Assert::true($user->isLoggedIn()); + + // Logout without clearing identity + $user->logout(false); + Assert::false($user->isLoggedIn()); + Assert::same('john', $user->getIdentity()->getId()); // Identity preserved + + // Switch to namespace B - should not have identity + $storage->setNamespace('B'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); + + // Back to A - identity should still be there + $storage->setNamespace('A'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); + Assert::same('john', $user->getIdentity()->getId()); +}); + + +test('Multiple namespaces can be used simultaneously', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + // Create 3 different authentication contexts + $contexts = [ + 'frontend' => 'customer1', + 'backend' => 'admin', + 'api' => 'apiuser', + ]; + + foreach ($contexts as $namespace => $username) { + $storage->setNamespace($namespace); + $user->login($username, 'xxx'); + Assert::true($user->isLoggedIn()); + Assert::same($username, $user->getId()); + } + + // Verify all contexts are still independent + foreach ($contexts as $namespace => $username) { + $storage->setNamespace($namespace); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same($username, $user->getId()); + } + + // Logout from one + $storage->setNamespace('backend'); + $user->refreshStorage(); // Reload from new namespace + $user->logout(); + + // Verify others are unaffected + $storage->setNamespace('frontend'); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('customer1', $user->getId()); + + $storage->setNamespace('api'); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('apiuser', $user->getId()); + + $storage->setNamespace('backend'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); +}); + + +test('Empty namespace is valid and separate from other namespaces', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + // Default (empty) namespace + $storage->setNamespace(''); + $user->login('user1', 'xxx'); + Assert::true($user->isLoggedIn()); + + // Named namespace + $storage->setNamespace('special'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); + $user->login('user2', 'xxx'); + Assert::true($user->isLoggedIn()); + Assert::same('user2', $user->getId()); + + // Back to empty namespace + $storage->setNamespace(''); + $user->refreshStorage(); // Reload from new namespace + Assert::true($user->isLoggedIn()); + Assert::same('user1', $user->getId()); +}); + + +test('Logout event fires only for actual logout, not namespace switch', function () { + $storage = new MockNamespacedStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $logoutCount = 0; + $user->onLoggedOut[] = function () use (&$logoutCount) { + $logoutCount++; + }; + + // Login to namespace A + $storage->setNamespace('A'); + $user->login('john', 'xxx'); + Assert::same(0, $logoutCount); + + // Actual logout in namespace A + $user->logout(); + Assert::same(1, $logoutCount); // First logout event + + // Login to namespace B + $storage->setNamespace('B'); + $user->refreshStorage(); // Reload from new namespace + $user->login('jane', 'xxx'); + Assert::same(1, $logoutCount); // No additional logout + + // Logout from B + $user->logout(); + Assert::same(2, $logoutCount); // Second logout event + + // Switch to A (nothing logged in there anymore) + $storage->setNamespace('A'); + $user->refreshStorage(); // Reload from new namespace + Assert::false($user->isLoggedIn()); + Assert::same(2, $logoutCount); // No logout event for checking status +}); diff --git a/tests/Security/User.refreshStorage.phpt b/tests/Security/User.refreshStorage.phpt new file mode 100644 index 00000000..b9bb427c --- /dev/null +++ b/tests/Security/User.refreshStorage.phpt @@ -0,0 +1,304 @@ +authenticated = true; + $this->identity = $identity; + $this->reason = null; + } + + + public function clearAuthentication(bool $clearIdentity): void + { + $this->authenticated = false; + $this->reason = Nette\Security\User::LogoutManual; + if ($clearIdentity) { + $this->identity = null; + } + } + + + public function getState(): array + { + return [$this->authenticated, $this->identity, $this->reason]; + } + + + public function setExpiration(?string $expire, bool $clearIdentity): void + { + } + + + // Test helpers + public function externallyModifyIdentity(callable $modifier): void + { + $this->identity = $modifier($this->identity); + } + + + public function externallyModifyRoles(array $newRoles): void + { + if ($this->identity) { + $this->identity = new SimpleIdentity( + $this->identity->getId(), + $newRoles, + $this->identity->getData(), + ); + } + } + + + public function externallyLogout(): void + { + $this->authenticated = false; + $this->reason = Nette\Security\User::LogoutInactivity; + } +} + + +class SimpleAuthenticator implements Nette\Security\Authenticator +{ + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity($username, ['user']); + } +} + + +test('refreshStorage() reloads identity from storage', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + $identity1 = $user->getIdentity(); + + Assert::same('john', $identity1->getId()); + Assert::same(['user'], $identity1->getRoles()); + + // Externally modify storage (e.g., another request updated roles in database) + $storage->externallyModifyRoles(['admin', 'user']); + + // Without refresh - still old cached identity + $identity2 = $user->getIdentity(); + Assert::same($identity1, $identity2); // Same object + Assert::same(['user'], $identity2->getRoles()); // Old roles + + // After refresh - new identity loaded + $user->refreshStorage(); + $identity3 = $user->getIdentity(); + + Assert::notSame($identity1, $identity3); // Different object + Assert::same('john', $identity3->getId()); + Assert::same(['admin', 'user'], $identity3->getRoles()); // New roles! +}); + + +test('refreshStorage() updates authentication state', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + Assert::true($user->isLoggedIn()); + + // External logout (e.g., session timeout in storage) + $storage->externallyLogout(); + + // Without refresh - still appears logged in (cached state) + Assert::true($user->isLoggedIn()); + + // After refresh - state updated + $user->refreshStorage(); + Assert::false($user->isLoggedIn()); + Assert::same(Nette\Security\User::LogoutInactivity, $user->getLogoutReason()); +}); + + +test('refreshStorage() reloads data from storage', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + + // Get identity - it's now cached + $identity1 = $user->getIdentity(); + Assert::notNull($identity1); + + // Externally modify storage + $storage->externallyModifyRoles(['admin']); + + // Without refresh - still old cached value + Assert::same(['user'], $user->getRoles()); + + // Refresh clears cache + $user->refreshStorage(); + + // Next access loads fresh from storage with new roles + Assert::same(['admin'], $user->getRoles()); +}); + + +test('refreshStorage() with IdentityHandler triggers wakeup again', function () { + $wakeupCount = 0; + + $handler = new class ($wakeupCount) implements Nette\Security\Authenticator, Nette\Security\IdentityHandler { + public function __construct( + private int &$wakeupCount, + ) { + } + + + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity($username, ['user']); + } + + + public function sleepIdentity(IIdentity $identity): IIdentity + { + return $identity; + } + + + public function wakeupIdentity(IIdentity $identity): ?IIdentity + { + $this->wakeupCount++; + // Each wakeup adds a role + return new SimpleIdentity( + $identity->getId(), + array_merge($identity->getRoles(), ['role' . $this->wakeupCount]), + ); + } + }; + + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator($handler); + + $user->login('john', 'xxx'); + + // After login, identity is cached - no wakeup yet + $identity1 = $user->getIdentity(); + Assert::same(0, $wakeupCount); // No wakeup after fresh login + Assert::same(['user'], $identity1->getRoles()); + + // Refresh triggers wakeup on next access + $user->refreshStorage(); + $identity2 = $user->getIdentity(); + Assert::same(1, $wakeupCount); // First wakeup + Assert::same(['user', 'role1'], $identity2->getRoles()); + + // Another refresh and access - wakeup gets fresh identity from storage again + $user->refreshStorage(); + $identity3 = $user->getIdentity(); + Assert::same(2, $wakeupCount); // Second wakeup + // Note: wakeupIdentity receives the stored identity (just ['user']), not previous wakeup result + Assert::same(['user', 'role2'], $identity3->getRoles()); +}); + + +test('refreshStorage() does not affect storage data', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + + // Get state before refresh + [$auth1, $id1, $reason1] = $storage->getState(); + + $user->refreshStorage(); + + // Get state after refresh - should be unchanged + [$auth2, $id2, $reason2] = $storage->getState(); + + Assert::same($auth1, $auth2); + Assert::same($id1, $id2); + Assert::same($reason1, $reason2); +}); + + +test('refreshStorage() on logged out user works without error', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + + Assert::false($user->isLoggedIn()); + + // Should not throw + $user->refreshStorage(); + + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); +}); + + +test('Multiple refreshStorage() calls work correctly', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + + for ($i = 0; $i < 5; $i++) { + $user->refreshStorage(); + Assert::true($user->isLoggedIn()); + Assert::same('john', $user->getId()); + } + + // Modify storage + $storage->externallyModifyRoles(['admin']); + + $user->refreshStorage(); + Assert::same(['admin'], $user->getIdentity()->getRoles()); +}); + + +test('refreshStorage() allows detecting external identity changes', function () { + $storage = new MutableStorage; + $user = new Nette\Security\User($storage); + $user->setAuthenticator(new SimpleAuthenticator); + + $user->login('john', 'xxx'); + $initialData = $user->getIdentity()->getData(); + + // Simulate another process updating user data in storage + $storage->externallyModifyIdentity(fn($identity) => new SimpleIdentity( + $identity->getId(), + $identity->getRoles(), + ['updated' => true, 'timestamp' => time()], + )); + + // Refresh to get updated data + $user->refreshStorage(); + $updatedData = $user->getIdentity()->getData(); + + Assert::notSame($initialData, $updatedData); + Assert::true($updatedData['updated']); + Assert::type('int', $updatedData['timestamp']); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2e128d3e..a297aa39 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,10 +10,5 @@ Tester\Environment::setup(); +Tester\Environment::setupFunctions(); date_default_timezone_set('Europe/Prague'); - - -function test(string $title, Closure $function): void -{ - $function(); -} From 9227b77a7ba14344e741449682360f2a893461ca Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Mar 2026 02:49:17 +0100 Subject: [PATCH 30/53] improved PHPDoc descriptions --- src/Security/Authorizator.php | 3 +-- src/Security/Identity.php | 2 +- src/Security/IdentityHandler.php | 8 +++++++- src/Security/Passwords.php | 10 +++++----- src/Security/Permission.php | 10 +++++----- src/Security/Resource.php | 2 +- src/Security/Role.php | 2 +- src/Security/SimpleAuthenticator.php | 3 +-- src/Security/User.php | 25 ++++++++++++++----------- src/Security/UserStorage.php | 6 +++--- 10 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Security/Authorizator.php b/src/Security/Authorizator.php index 29e4953e..ca2d450f 100644 --- a/src/Security/Authorizator.php +++ b/src/Security/Authorizator.php @@ -9,8 +9,7 @@ /** - * Authorizator checks if a given role has authorization - * to access a given resource. + * Checks whether a given role has access to a given resource. */ interface Authorizator { diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 3d3339c1..9082355f 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -72,7 +72,7 @@ public function getRoles(): array /** - * Returns a user data. + * Returns user data. */ public function getData(): array { diff --git a/src/Security/IdentityHandler.php b/src/Security/IdentityHandler.php index e140516f..3cdd856c 100644 --- a/src/Security/IdentityHandler.php +++ b/src/Security/IdentityHandler.php @@ -9,11 +9,17 @@ /** - * Adjusts identity from/to storage. + * Serializes and restores identity to/from persistent storage. */ interface IdentityHandler { + /** + * Called before identity is written to storage. Typically replaces the full identity with a lightweight token. + */ function sleepIdentity(IIdentity $identity): IIdentity; + /** + * Called after identity is read from storage. Typically refreshes roles or validates the token. Returns null to force logout. + */ function wakeupIdentity(IIdentity $identity): ?IIdentity; } diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 5c6c9d32..dfca01b0 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -11,12 +11,12 @@ /** - * Password Hashing. + * Password hashing and verification. */ class Passwords { /** - * Chooses which secure algorithm is used for hashing and how to configure it. + * Configures the hashing algorithm and its options. * @see https://php.net/manual/en/password.constants.php */ public function __construct( @@ -27,7 +27,7 @@ public function __construct( /** - * Computes password´s hash. The result contains the algorithm ID and its settings, cryptographical salt and the hash itself. + * Computes a password hash containing the algorithm ID, settings, salt, and the hash itself. */ public function hash( #[\SensitiveParameter] @@ -48,7 +48,7 @@ public function hash( /** - * Finds out, whether the given password matches the given hash. + * Checks whether the password matches the given hash. */ public function verify( #[\SensitiveParameter] @@ -61,7 +61,7 @@ public function verify( /** - * Finds out if the hash matches the options given in constructor. + * Checks whether the hash needs to be rehashed with the current algorithm and options. */ public function needsRehash(string $hash): bool { diff --git a/src/Security/Permission.php b/src/Security/Permission.php index cc831a58..0b5c7c17 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -80,7 +80,7 @@ public function addRole(string $role, string|array|null $parents = null): static /** - * Returns true if the Role exists in the list. + * Checks whether the Role exists in the list. */ public function hasRole(string $role): bool { @@ -213,7 +213,7 @@ public function removeAllRoles(): static /** - * Adds a Resource having an identifier unique to the list. + * Adds a Resource to the list. * * @throws Nette\InvalidArgumentException * @throws Nette\InvalidStateException @@ -241,7 +241,7 @@ public function addResource(string $resource, ?string $parent = null): static /** - * Returns true if the Resource exists in the list. + * Checks whether the Resource exists in the list. */ public function hasResource(string $resource): bool { @@ -603,7 +603,7 @@ public function isAllowed( /** - * Returns real currently queried Role. Use by assertion. + * Returns the role currently being queried. Used by assertion callbacks. */ public function getQueriedRole(): string|Role|null { @@ -612,7 +612,7 @@ public function getQueriedRole(): string|Role|null /** - * Returns real currently queried Resource. Use by assertion. + * Returns the resource currently being queried. Used by assertion callbacks. */ public function getQueriedResource(): string|Resource|null { diff --git a/src/Security/Resource.php b/src/Security/Resource.php index 6d09d6b8..1a3bed4f 100644 --- a/src/Security/Resource.php +++ b/src/Security/Resource.php @@ -9,7 +9,7 @@ /** - * Represents resource, an object to which access is controlled. + * Represents a resource to which access is controlled. */ interface Resource { diff --git a/src/Security/Role.php b/src/Security/Role.php index b12638fc..e2781533 100644 --- a/src/Security/Role.php +++ b/src/Security/Role.php @@ -9,7 +9,7 @@ /** - * Represents role, an object that may request access to an IResource. + * Represents a role that can be granted access to resources. */ interface Role { diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index 5afea54a..91599129 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -28,8 +28,7 @@ public function __construct( /** - * Performs an authentication against e.g. database. - * and returns IIdentity on success or throws AuthenticationException + * Authenticates against the in-memory list of users (case-insensitive username). * @throws AuthenticationException */ public function authenticate( diff --git a/src/Security/User.php b/src/Security/User.php index 753c03d7..2eacf3af 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -79,8 +79,8 @@ final public function getStorage(): UserStorage /** - * Conducts the authentication process. Parameters are optional. - * @param string|IIdentity $username name or Identity + * Authenticates the user. Accepts username and password, or an IIdentity directly. + * @param string|IIdentity $username username or identity * @throws AuthenticationException if authentication was not successful */ public function login( @@ -128,7 +128,7 @@ final public function logout(bool $clearIdentity = false): void /** - * Is this user authenticated? + * Checks whether the user is authenticated. */ final public function isLoggedIn(): bool { @@ -141,7 +141,7 @@ final public function isLoggedIn(): bool /** - * Returns current user identity, if any. + * Returns the current user identity, or null if not authenticated. */ final public function getIdentity(): ?IIdentity { @@ -178,6 +178,9 @@ public function getId(): string|int|null } + /** + * Discards cached authentication state, forcing a reload from storage on next access. + */ final public function refreshStorage(): void { $this->identity = $this->authenticated = $this->logoutReason = null; @@ -208,7 +211,7 @@ final public function getAuthenticator(): IAuthenticator /** - * Returns authentication handler. + * Returns authentication handler, or null if none is set. */ final public function getAuthenticatorIfExists(): ?IAuthenticator { @@ -234,7 +237,7 @@ public function setExpiration(?string $expire, bool $clearIdentity = false): sta /** - * Why was user logged out? Returns LOGOUT_MANUAL or LOGOUT_INACTIVITY. + * Returns the logout reason: LogoutManual or LogoutInactivity, or null if not applicable. */ final public function getLogoutReason(): ?int { @@ -246,7 +249,7 @@ final public function getLogoutReason(): ?int /** - * Returns a list of effective roles that a user has been granted. + * Returns effective roles of the user. Unauthenticated users get the guest role. */ public function getRoles(): array { @@ -260,7 +263,7 @@ public function getRoles(): array /** - * Is a user in the specified effective role? + * Checks whether the user has the specified effective role. */ final public function isInRole(string $role): bool { @@ -275,8 +278,8 @@ final public function isInRole(string $role): bool /** - * Has a user effective access to the Resource? - * If $resource is null, then the query applies to all resources. + * Checks whether the user has access to the given resource and privilege. + * Null means all resources or all privileges. */ public function isAllowed(mixed $resource = Authorizator::All, mixed $privilege = Authorizator::All): bool { @@ -314,7 +317,7 @@ final public function getAuthorizator(): Authorizator /** - * Returns current authorization handler. + * Returns authorization handler, or null if none is set. */ final public function getAuthorizatorIfExists(): ?Authorizator { diff --git a/src/Security/UserStorage.php b/src/Security/UserStorage.php index 88ef7477..82e83c7f 100644 --- a/src/Security/UserStorage.php +++ b/src/Security/UserStorage.php @@ -9,7 +9,7 @@ /** - * Interface for persistent storage for user object data. + * Persistent storage for user authentication state and identity. */ interface UserStorage { @@ -20,12 +20,12 @@ interface UserStorage public const LOGOUT_INACTIVITY = 2; /** - * Sets the authenticated state of user. + * Saves authenticated identity to storage. */ function saveAuthentication(IIdentity $identity): void; /** - * Removed authenticated state of user. + * Removes authenticated state from storage. */ function clearAuthentication(bool $clearIdentity): void; From bf1170e79b8f618cfed247a204bd4a7c6b73bc12 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 10:55:50 +0100 Subject: [PATCH 31/53] improved phpDoc types --- src/Bridges/SecurityDI/SecurityExtension.php | 15 ++++++++- src/Bridges/SecurityHttp/CookieStorage.php | 1 + src/Security/IAuthenticator.php | 2 +- src/Security/IIdentity.php | 5 +-- src/Security/Identity.php | 17 +++++++++-- src/Security/Passwords.php | 2 +- src/Security/Permission.php | 32 ++++++++++++++++++-- src/Security/SimpleAuthenticator.php | 8 ++--- src/Security/User.php | 30 ++++++++++-------- 9 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index c6831230..9d51c5bb 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -15,6 +15,20 @@ /** * Security extension for Nette DI. + * + * @property object{ + * debugger: bool|null, + * users: array, data?: array}>, + * roles: array|null>, + * resources: array, + * authentication: object{ + * storage: 'session'|'cookie', + * expiration: string|null, + * cookieName: string|null, + * cookieDomain: string|null, + * cookieSamesite: 'Lax'|'Strict'|'None'|null, + * }, + * } $config */ class SecurityExtension extends Nette\DI\CompilerExtension { @@ -53,7 +67,6 @@ public function getConfigSchema(): Nette\Schema\Schema public function loadConfiguration(): void { - /** @var object{debugger: bool, users: array, roles: array, resources: array, authentication: \stdClass} $config */ $config = $this->config; $builder = $this->getContainerBuilder(); diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index 3a814e13..ab9a2d19 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -84,6 +84,7 @@ public function setExpiration(?string $expire, bool $clearIdentity): void } + /** @param 'Lax'|'Strict'|'None'|null $sameSite */ public function setCookieParameters( ?string $name = null, ?string $domain = null, diff --git a/src/Security/IAuthenticator.php b/src/Security/IAuthenticator.php index 7613ad2f..443f37b6 100644 --- a/src/Security/IAuthenticator.php +++ b/src/Security/IAuthenticator.php @@ -10,7 +10,7 @@ /** * @deprecated update to Nette\Security\Authenticator - * @method IIdentity authenticate(array $credentials) + * @method IIdentity authenticate(array{string, string} $credentials) */ interface IAuthenticator { diff --git a/src/Security/IIdentity.php b/src/Security/IIdentity.php index f81c3ec5..774a8fa0 100644 --- a/src/Security/IIdentity.php +++ b/src/Security/IIdentity.php @@ -10,18 +10,19 @@ /** * Represents the user of application. - * @method array getData() + * @method array getData() */ interface IIdentity { /** * Returns the ID of user. - * @return mixed + * @return string|int */ function getId(); /** * Returns a list of roles that the user is a member of. + * @return list */ function getRoles(): array; diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 9082355f..347da845 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -13,17 +13,25 @@ /** * @deprecated use Nette\Security\SimpleIdentity * @property string|int $id - * @property array $roles - * @property array $data + * @property list $roles + * @property array $data */ class Identity implements IIdentity { private string|int $id; + + /** @var list */ private array $roles; + + /** @var array */ private array $data; - public function __construct(string|int $id, $roles = null, ?iterable $data = null) + /** + * @param string|list|null $roles + * @param ?iterable $data + */ + public function __construct(string|int $id, string|array|null $roles = null, ?iterable $data = null) { $this->setId($id); $this->setRoles((array) $roles); @@ -54,6 +62,7 @@ public function getId(): string|int /** * Sets a list of roles that the user is a member of. + * @param list $roles */ public function setRoles(array $roles): static { @@ -64,6 +73,7 @@ public function setRoles(array $roles): static /** * Returns a list of roles that the user is a member of. + * @return list */ public function getRoles(): array { @@ -73,6 +83,7 @@ public function getRoles(): array /** * Returns user data. + * @return array */ public function getData(): array { diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index dfca01b0..640ec6cd 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -17,10 +17,10 @@ class Passwords { /** * Configures the hashing algorithm and its options. - * @see https://php.net/manual/en/password.constants.php */ public function __construct( private readonly string $algo = PASSWORD_DEFAULT, + /** @var array algorithm-specific options, see https://php.net/manual/en/password.constants.php */ private readonly array $options = [], ) { } diff --git a/src/Security/Permission.php b/src/Security/Permission.php index 0b5c7c17..a84d2e7c 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -18,10 +18,13 @@ */ class Permission implements Authorizator { + /** @var array, children: array}> */ private array $roles = []; + + /** @var array}> */ private array $resources = []; - /** Access Control List rules; whitelist (deny everything to all) by default */ + /** @var array Access Control List rules; whitelist (deny everything to all) by default */ private array $rules = [ 'allResources' => [ 'allRoles' => [ @@ -46,6 +49,7 @@ class Permission implements Authorizator /** * Adds a Role to the list. The most recently added parent * takes precedence over parents that were previously added. + * @param string|list|null $parents * @throws Nette\InvalidArgumentException * @throws Nette\InvalidStateException */ @@ -106,6 +110,7 @@ private function checkRole(string $role, bool $exists = true): void /** * Returns all Roles. + * @return list */ public function getRoles(): array { @@ -115,6 +120,7 @@ public function getRoles(): array /** * Returns existing Role's parents ordered by ascending priority. + * @return list */ public function getRoleParents(string $role): array { @@ -267,6 +273,7 @@ private function checkResource(string $resource, bool $exists = true): void /** * Returns all Resources. + * @return list */ public function getResources(): array { @@ -275,7 +282,7 @@ public function getResources(): array /** - * Returns true if $resource inherits from $inherit. If $onlyParents is true, + * Returns true if $resource inherits from $inherit. If $onlyParent is true, * then $resource must inherit directly from $inherit. * * @throws Nette\InvalidStateException @@ -365,6 +372,10 @@ public function removeAllResources(): static /** * Allows one or more Roles access to [certain $privileges upon] the specified Resource(s). * If $assertion is provided, then it must return true in order for rule to apply. + * @param string|list|null $roles + * @param string|list|null $resources + * @param string|list|null $privileges + * @param callable(self, ?string, ?string, ?string): bool $assertion */ public function allow( string|array|null $roles = self::All, @@ -381,6 +392,10 @@ public function allow( /** * Denies one or more Roles access to [certain $privileges upon] the specified Resource(s). * If $assertion is provided, then it must return true in order for rule to apply. + * @param string|list|null $roles + * @param string|list|null $resources + * @param string|list|null $privileges + * @param callable(self, ?string, ?string, ?string): bool $assertion */ public function deny( string|array|null $roles = self::All, @@ -396,6 +411,9 @@ public function deny( /** * Removes "allow" permissions from the list in the context of the given Roles, Resources, and privileges. + * @param string|list|null $roles + * @param string|list|null $resources + * @param string|list|null $privileges */ public function removeAllow( string|array|null $roles = self::All, @@ -410,6 +428,9 @@ public function removeAllow( /** * Removes "deny" restrictions from the list in the context of the given Roles, Resources, and privileges. + * @param string|list|null $roles + * @param string|list|null $resources + * @param string|list|null $privileges */ public function removeDeny( string|array|null $roles = self::All, @@ -424,6 +445,10 @@ public function removeDeny( /** * Performs operations on Access Control List rules. + * @param string|list|null $roles + * @param string|list|null $resources + * @param string|list|null $privileges + * @param callable(self, ?string, ?string, ?string): bool $assertion * @throws Nette\InvalidStateException */ protected function setRule( @@ -626,7 +651,7 @@ public function getQueriedResource(): string|Resource|null /** * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule * allowing/denying $role access to a/all $privilege upon $resource. - * @param bool $all (true) or one? + * @param bool $all match a rule covering all privileges (true) or the given $privilege (false) */ private function searchRolePrivileges(bool $all, ?string $role, ?string $resource, ?string $privilege): ?bool { @@ -711,6 +736,7 @@ private function getRuleType(?string $resource, ?string $role, ?string $privileg /** * Returns the rules associated with a Resource and a Role, or null if no such rules exist. * If the $create parameter is true, then a rule set is first created and then returned to the caller. + * @return array|null */ private function &getRules(?string $resource, ?string $role, bool $create = false): ?array { diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index 91599129..0e5b24b4 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -13,15 +13,13 @@ */ class SimpleAuthenticator implements Authenticator { - /** - * @param array $passwords list of pairs username => password - * @param array $roles list of pairs username => role[] - * @param array $data list of pairs username => mixed[] - */ public function __construct( + /** @var array */ #[\SensitiveParameter] private array $passwords, + /** @var array|null> */ private array $roles = [], + /** @var array> */ private array $data = [], ) { } diff --git a/src/Security/User.php b/src/Security/User.php index 2eacf3af..a15c1e34 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -16,10 +16,10 @@ * User authentication and authorization. * * @property-read bool $loggedIn - * @property-read IIdentity $identity - * @property-read string|int $id - * @property-read array $roles - * @property-read int $logoutReason + * @property-read ?IIdentity $identity + * @property-read string|int|null $id + * @property-read list $roles + * @property-read ?int $logoutReason * @property IAuthenticator $authenticator * @property Authorizator $authorizator */ @@ -50,10 +50,10 @@ class User /** default role for authenticated user without own identity */ public string $authenticatedRole = 'authenticated'; - /** @var array Occurs when the user is successfully logged in */ + /** @var array Occurs when the user is successfully logged in */ public array $onLoggedIn = []; - /** @var array Occurs when the user is logged out */ + /** @var array Occurs when the user is logged out */ public array $onLoggedOut = []; private ?IIdentity $identity = null; @@ -111,7 +111,8 @@ public function login( /** - * Logs out the user from the current session. + * Logs out the user from the current session. The identity is kept available afterwards, + * unless $clearIdentity is set. */ final public function logout(bool $clearIdentity = false): void { @@ -141,7 +142,8 @@ final public function isLoggedIn(): bool /** - * Returns the current user identity, or null if not authenticated. + * Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration), + * so its presence does not imply the user is logged in; null if none. */ final public function getIdentity(): ?IIdentity { @@ -169,7 +171,8 @@ private function getStoredData(): void /** - * Returns current user ID, if any. + * Returns the ID of the identity returned by getIdentity(), so it may be available even when not + * logged in; null if there is no identity. */ public function getId(): string|int|null { @@ -179,7 +182,7 @@ public function getId(): string|int|null /** - * Discards cached authentication state, forcing a reload from storage on next access. + * Discards the cached authentication state and identity, forcing a reload on next access. */ final public function refreshStorage(): void { @@ -227,7 +230,8 @@ final public function hasAuthenticator(): bool /** - * Enables log out after inactivity (like '20 minutes'). + * Enables log out after inactivity (like '20 minutes'). The identity is kept available afterwards, + * unless $clearIdentity is set. */ public function setExpiration(?string $expire, bool $clearIdentity = false): static { @@ -249,7 +253,9 @@ final public function getLogoutReason(): ?int /** - * Returns effective roles of the user. Unauthenticated users get the guest role. + * Returns effective roles derived from the login state, not from the (possibly retained) identity. + * Logged in: the identity's roles, or authenticatedRole. Otherwise: the guestRole. + * @return list */ public function getRoles(): array { From 89bbd0b75a5ea8419e20d8f71426d97d84aa5ff9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 10:55:50 +0100 Subject: [PATCH 32/53] uses nette/phpstan-rules --- composer.json | 9 +++++++- tests/types/security-types.php | 41 +++++++++++++++++++++++++++++++++ tests/types/security-types.phpt | 7 ++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tests/types/security-types.php create mode 100644 tests/types/security-types.phpt diff --git a/composer.json b/composer.json index cc7a8d5f..570486cc 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,9 @@ "nette/http": "^3.2", "nette/tester": "^2.6", "tracy/tracy": "^2.9", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.1@stable", + "phpstan/extension-installer": "^1.4@stable", + "nette/phpstan-rules": "^1.0", "mockery/mockery": "^1.6@stable" }, "conflict": { @@ -45,5 +47,10 @@ "branch-alias": { "dev-master": "3.2-dev" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/tests/types/security-types.php b/tests/types/security-types.php new file mode 100644 index 00000000..643b489e --- /dev/null +++ b/tests/types/security-types.php @@ -0,0 +1,41 @@ +', $acl->getRoles()); +} + + +function testPermissionGetRoleParents(Permission $acl): void +{ + $acl->addRole('admin'); + assertType('list', $acl->getRoleParents('admin')); +} + + +function testPermissionGetResources(Permission $acl): void +{ + assertType('list', $acl->getResources()); +} + + +function testIIdentityGetId(IIdentity $identity): void +{ + assertType('int|string', $identity->getId()); +} + + +function testUserGetId(User $user): void +{ + assertType('int|string|null', $user->getId()); +} diff --git a/tests/types/security-types.phpt b/tests/types/security-types.phpt new file mode 100644 index 00000000..0bb42c6d --- /dev/null +++ b/tests/types/security-types.phpt @@ -0,0 +1,7 @@ + Date: Fri, 9 Jan 2026 10:55:50 +0100 Subject: [PATCH 33/53] fixed PHPStan errors --- phpstan.neon | 30 ++++++++++++++++---- src/Bridges/SecurityDI/SecurityExtension.php | 12 ++++---- src/Bridges/SecurityHttp/SessionStorage.php | 7 ++--- src/Bridges/SecurityTracy/dist/panel.phtml | 30 ++++++++++++++------ src/Bridges/SecurityTracy/dist/tab.phtml | 17 +++++++---- src/Bridges/SecurityTracy/panel.latte | 1 + src/Bridges/SecurityTracy/tab.latte | 1 + src/Security/Identity.php | 14 ++++----- src/Security/Passwords.php | 2 +- src/Security/Permission.php | 5 ++-- src/Security/User.php | 4 +-- 11 files changed, 83 insertions(+), 40 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index f8dd9b68..9b6415cb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,14 +1,34 @@ parameters: - level: 5 + level: 8 paths: - src - treatPhpDocTypesAsCertain: false + excludePaths: + - src/compatibility.php + + fileExtensions: + - php + - phtml ignoreErrors: - - '#Variable \$this in isset\(\) always exists and is not nullable\.#' + - # generated Latte code: attribute interpolation emits a dead `=== null` check for values that are never null + identifier: identical.alwaysFalse + path: src/Bridges/SecurityTracy/dist/tab.phtml + - + message: '#^Method Nette\\Http\\IResponse\:\:setCookie\(\) invoked with 8 parameters, 3\-7 required\.$#' + identifier: arguments.count + count: 1 + path: src/Bridges/SecurityHttp/CookieStorage.php + - + message: '#^Instanceof between string and Nette\\Security\\Role will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse + count: 1 + path: src/Security/User.php -includes: - - vendor/phpstan/phpstan-nette/extension.neon + - + message: '#^Parameter \#1 \$credentials of method Nette\\Security\\IAuthenticator\:\:authenticate\(\) expects array\{string, string\}, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Security/User.php diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 9d51c5bb..210d3a5a 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -82,11 +82,11 @@ public function loadConfiguration(): void ][$auth->storage]); if ($auth->storage === 'cookie') { - if ($auth->cookieDomain === 'domain') { - $auth->cookieDomain = $builder::literal('$this->getByType(Nette\Http\IRequest::class)->getUrl()->getDomain(2)'); - } + $cookieDomain = $auth->cookieDomain === 'domain' + ? $builder::literal('$this->getByType(Nette\Http\IRequest::class)->getUrl()->getDomain(2)') + : $auth->cookieDomain; - $storage->addSetup('setCookieParameters', [$auth->cookieName, $auth->cookieDomain, $auth->cookieSamesite]); + $storage->addSetup('setCookieParameters', [$auth->cookieName, $cookieDomain, $auth->cookieSamesite]); } $user = $builder->addDefinition($this->prefix('user')) @@ -147,7 +147,9 @@ public function beforeCompile(): void $this->debugMode && ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) ) { - $builder->getDefinition($this->prefix('user'))->addSetup('@Tracy\Bar::addPanel', [ + $definition = $builder->getDefinition($this->prefix('user')); + assert($definition instanceof Nette\DI\Definitions\ServiceDefinition); + $definition->addSetup('@Tracy\Bar::addPanel', [ new Nette\DI\Definitions\Statement(Nette\Bridges\SecurityTracy\UserPanel::class), ]); } diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index f9167652..683c3205 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -64,9 +64,7 @@ public function clearAuthentication(bool $clearIdentity): void public function getState(): array { $section = $this->getSessionSection(); - return $section - ? [(bool) $section->get('authenticated'), $section->get('identity'), $section->get('reason')] - : [false, null, null]; + return [(bool) $section->get('authenticated'), $section->get('identity'), $section->get('reason')]; } @@ -83,6 +81,7 @@ public function setExpiration(?string $time, bool $clearIdentity = false): void private function setupExpiration(): void { + assert($this->sessionSection !== null); $section = $this->sessionSection; if ($this->expireTime) { $section->set('expireTime', $this->expireTime); @@ -122,7 +121,7 @@ public function getNamespace(): string /** * Returns and initializes $this->sessionSection. */ - private function getSessionSection(): ?SessionSection + private function getSessionSection(): SessionSection { if ($this->sessionSection !== null) { return $this->sessionSection; diff --git a/src/Bridges/SecurityTracy/dist/panel.phtml b/src/Bridges/SecurityTracy/dist/panel.phtml index dd5c30bb..e76d0345 100644 --- a/src/Bridges/SecurityTracy/dist/panel.phtml +++ b/src/Bridges/SecurityTracy/dist/panel.phtml @@ -1,11 +1,23 @@ - -

isLoggedIn()): ?> -Logged in -Unlogged -

+ -getIdentity()): ?> getIdentity(), [Tracy\Dumper::LIVE => true]) ?> +/** @var Nette\Security\User $user */ +echo '

'; +if ($user->isLoggedIn()) /* pos 2:5 */ { + echo 'Logged in'; +} else /* pos 2:38 */ { + echo 'Unlogged'; +} +echo '

-

no identity

- +
+'; +if ($user->getIdentity()) /* pos 5:2 */ { + echo ' '; + echo Tracy\Dumper::toHtml($user->getIdentity(), [Tracy\Dumper::LIVE => true]) /* pos 6:3 */; + echo "\n"; +} else /* pos 7:2 */ { + echo '

no identity

+'; +} +echo '
+'; diff --git a/src/Bridges/SecurityTracy/dist/tab.phtml b/src/Bridges/SecurityTracy/dist/tab.phtml index 84b088c2..a1c13389 100644 --- a/src/Bridges/SecurityTracy/dist/tab.phtml +++ b/src/Bridges/SecurityTracy/dist/tab.phtml @@ -1,8 +1,15 @@ - - 'Logged in', false => 'Unlogged', '?' => 'Session is closed'] ?> '#61A519', false => '#ababab', '?' => '#bb0000'] ?> + 'Logged in', false => 'Unlogged', '?' => 'Session is closed'] /* pos 2:1 */; +$colors = [true => '#61A519', false => '#ababab', '?' => '#bb0000'] /* pos 3:1 */; +echo ' - + +'; diff --git a/src/Bridges/SecurityTracy/panel.latte b/src/Bridges/SecurityTracy/panel.latte index 7ba7c784..fc358541 100644 --- a/src/Bridges/SecurityTracy/panel.latte +++ b/src/Bridges/SecurityTracy/panel.latte @@ -1,3 +1,4 @@ +{varType Nette\Security\User $user}

{if $user->isLoggedIn()}Logged in{else}Unlogged{/if}

diff --git a/src/Bridges/SecurityTracy/tab.latte b/src/Bridges/SecurityTracy/tab.latte index f745d1ae..06d89ee3 100644 --- a/src/Bridges/SecurityTracy/tab.latte +++ b/src/Bridges/SecurityTracy/tab.latte @@ -1,3 +1,4 @@ +{varType string|bool $status} {do $messages = [true => 'Logged in', false => Unlogged, '?' => 'Session is closed']} {do $colors = [true => '#61A519', false => '#ababab', '?' => '#bb0000']} diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 347da845..e006cbfc 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -96,12 +96,12 @@ public function getData(): array */ public function __set(string $key, mixed $value): void { - if (in_array($key, ['id', 'roles', 'data'], strict: true)) { - $this->{"set$key"}($value); - - } else { - $this->data[$key] = $value; - } + match ($key) { + 'id' => $this->setId($value), + 'roles' => $this->setRoles($value), + 'data' => $this->data = $value, + default => $this->data[$key] = $value, + }; } @@ -111,7 +111,7 @@ public function __set(string $key, mixed $value): void public function &__get(string $key): mixed { if (in_array($key, ['id', 'roles', 'data'], strict: true)) { - $res = $this->{"get$key"}(); + $res = $this->{'get' . ucfirst($key)}(); return $res; } else { diff --git a/src/Security/Passwords.php b/src/Security/Passwords.php index 640ec6cd..69a6fb96 100644 --- a/src/Security/Passwords.php +++ b/src/Security/Passwords.php @@ -40,7 +40,7 @@ public function hash( $hash = @password_hash($password, $this->algo, $this->options); // @ is escalated to exception if (!$hash) { - throw new Nette\InvalidStateException('Computed hash is invalid. ' . error_get_last()['message']); + throw new Nette\InvalidStateException('Computed hash is invalid. ' . (error_get_last()['message'] ?? '')); } return $hash; diff --git a/src/Security/Permission.php b/src/Security/Permission.php index a84d2e7c..de276ea6 100644 --- a/src/Security/Permission.php +++ b/src/Security/Permission.php @@ -8,7 +8,7 @@ namespace Nette\Security; use Nette; -use function array_keys, array_pop, count, is_array; +use function array_keys, array_pop, count, is_array, is_string; /** @@ -619,11 +619,12 @@ public function isAllowed( break; } + assert(is_string($resource)); $resource = $this->resources[$resource]['parent']; // try next Resource } while (true); $this->queriedRole = $this->queriedResource = null; - return $result ?? false; + return $result; } diff --git a/src/Security/User.php b/src/Security/User.php index a15c1e34..8510efe2 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -137,7 +137,7 @@ final public function isLoggedIn(): bool $this->getStoredData(); } - return $this->authenticated; + return (bool) $this->authenticated; } @@ -166,7 +166,7 @@ private function getStoredData(): void $this->identity = $identity && $this->authenticator instanceof IdentityHandler ? $this->authenticator->wakeupIdentity($identity) : $identity; - $this->authenticated = $this->authenticated && $this->identity; + $this->authenticated = $this->authenticated && $this->identity !== null; } From 0e3b42130b5e1ef877007b6570b7a72847e8eb34 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 23 Jan 2026 00:20:27 +0100 Subject: [PATCH 34/53] made static analysis mandatory --- .github/workflows/static-analysis.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 674979c7..b5b0dffc 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,9 +1,6 @@ -name: Static Analysis (only informative) +name: Static Analysis -on: - push: - branches: - - master +on: [push, pull_request] jobs: phpstan: @@ -13,9 +10,8 @@ jobs: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none - run: composer install --no-progress --prefer-dist - run: composer phpstan - continue-on-error: true # is only informative From b0deda30d64d8d11376b9393c0e2ae08f8fb1bf7 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 29 Dec 2025 16:31:24 +0100 Subject: [PATCH 35/53] SessionStorage: don't extend expireTime when expired --- src/Bridges/SecurityHttp/SessionStorage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index 683c3205..27396e8a 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -140,9 +140,9 @@ private function getSessionSection(): SessionSection if ($section->get('expireIdentity')) { $section->remove('identity'); } + } else { + $section->set('expireTime', time() + $section->get('expireDelta')); // sliding expiration } - - $section->set('expireTime', time() + $section->get('expireDelta')); // sliding expiration } if (!$section->get('authenticated')) { From cef12ac23bb4b80f0d638ff0564b4cca21f455e7 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 May 2026 02:49:50 +0200 Subject: [PATCH 36/53] User: deduplicated lazy-loading of stored data --- src/Security/User.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Security/User.php b/src/Security/User.php index 8510efe2..a3fc0df5 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -133,10 +133,7 @@ final public function logout(bool $clearIdentity = false): void */ final public function isLoggedIn(): bool { - if ($this->authenticated === null) { - $this->getStoredData(); - } - + $this->loadStoredData(); return (bool) $this->authenticated; } @@ -147,16 +144,17 @@ final public function isLoggedIn(): bool */ final public function getIdentity(): ?IIdentity { - if ($this->authenticated === null) { - $this->getStoredData(); - } - + $this->loadStoredData(); return $this->identity; } - private function getStoredData(): void + private function loadStoredData(): void { + if ($this->authenticated !== null) { + return; + } + (function (bool $state, ?IIdentity $id, ?int $reason) use (&$identity) { $identity = $id; $this->authenticated = $state; From 1b49d3709593e44da41d0d3d7f218ce879b9829d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 May 2026 02:32:10 +0200 Subject: [PATCH 37/53] User: added $persistIdentity option to control identity availability after logout By default the identity stays available after logout or expiration (for personalization). Setting $persistIdentity to false discards it, so getIdentity() and getId() return null when not logged in. Configurable via the security.authentication DI section. --- readme.md | 2 + src/Bridges/SecurityDI/SecurityExtension.php | 6 +++ src/Security/User.php | 19 ++++--- .../SecurityExtension.persistIdentity.phpt | 49 +++++++++++++++++ tests/Security/User.persistIdentity.phpt | 54 +++++++++++++++++++ 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 tests/Security.DI/SecurityExtension.persistIdentity.phpt create mode 100644 tests/Security/User.persistIdentity.phpt diff --git a/readme.md b/readme.md index 45be02fa..a28fb041 100644 --- a/readme.md +++ b/readme.md @@ -178,6 +178,8 @@ Importantly, **when user logs out, identity is not deleted** and is still availa Thanks to this, you can still assume which user is at the computer and, for example, display personalized offers in the e-shop, however, you can only display his personal data after logging in. +If you prefer the identity to be discarded on every logout and expiration, set `$user->persistIdentity = false`. Retaining the identity is best-effort and depends on the storage implementation. + Identity is an object that implements the [Nette\Security\IIdentity](https://api.nette.org/master/Nette/Security/IIdentity.html) interface, the default implementation is [Nette\Security\SimpleIdentity](https://api.nette.org/3.0/Nette/Security/SimpleIdentity.html). And as mentioned, identity is stored in the session, so if, for example, we change the role of some of the logged-in users, old data will be kept in the identity until he logs in again. diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 210d3a5a..37192887 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -24,6 +24,7 @@ * authentication: object{ * storage: 'session'|'cookie', * expiration: string|null, + * persistIdentity: bool, * cookieName: string|null, * cookieDomain: string|null, * cookieSamesite: 'Lax'|'Strict'|'None'|null, @@ -57,6 +58,7 @@ public function getConfigSchema(): Nette\Schema\Schema 'authentication' => Expect::structure([ 'storage' => Expect::anyOf('session', 'cookie')->default('session'), 'expiration' => Expect::string()->dynamic(), + 'persistIdentity' => Expect::bool(true), 'cookieName' => Expect::string(), 'cookieDomain' => Expect::string(), 'cookieSamesite' => Expect::anyOf('Lax', 'Strict', 'None'), @@ -96,6 +98,10 @@ public function loadConfiguration(): void $user->addSetup('setExpiration', [$auth->expiration]); } + if (!$auth->persistIdentity) { + $user->addSetup('$persistIdentity', [false]); + } + if ($config->users) { $usersList = $usersRoles = $usersData = []; foreach ($config->users as $username => $data) { diff --git a/src/Security/User.php b/src/Security/User.php index a3fc0df5..8637fe01 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -50,6 +50,9 @@ class User /** default role for authenticated user without own identity */ public string $authenticatedRole = 'authenticated'; + /** keep identity available (via getIdentity() and getId()) after logout or expiration; depends on the storage implementation */ + public bool $persistIdentity = true; + /** @var array Occurs when the user is successfully logged in */ public array $onLoggedIn = []; @@ -112,10 +115,11 @@ public function login( /** * Logs out the user from the current session. The identity is kept available afterwards, - * unless $clearIdentity is set. + * unless $clearIdentity is set or the $persistIdentity property is disabled. */ final public function logout(bool $clearIdentity = false): void { + $clearIdentity = $clearIdentity || !$this->persistIdentity; $logged = $this->isLoggedIn(); $this->storage->clearAuthentication($clearIdentity); $this->authenticated = false; @@ -139,8 +143,8 @@ final public function isLoggedIn(): bool /** - * Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration), - * so its presence does not imply the user is logged in; null if none. + * Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration) + * unless $persistIdentity is disabled, so its presence does not imply the user is logged in; null if none. */ final public function getIdentity(): ?IIdentity { @@ -161,10 +165,11 @@ private function loadStoredData(): void $this->logoutReason = $reason; })(...$this->storage->getState()); - $this->identity = $identity && $this->authenticator instanceof IdentityHandler + $identity = $identity && $this->authenticator instanceof IdentityHandler ? $this->authenticator->wakeupIdentity($identity) : $identity; - $this->authenticated = $this->authenticated && $this->identity !== null; + $this->authenticated = $this->authenticated && $identity !== null; + $this->identity = !$this->authenticated && !$this->persistIdentity ? null : $identity; } @@ -229,11 +234,11 @@ final public function hasAuthenticator(): bool /** * Enables log out after inactivity (like '20 minutes'). The identity is kept available afterwards, - * unless $clearIdentity is set. + * unless $clearIdentity is set or the $persistIdentity property is disabled. */ public function setExpiration(?string $expire, bool $clearIdentity = false): static { - $this->storage->setExpiration($expire, $clearIdentity); + $this->storage->setExpiration($expire, $clearIdentity || !$this->persistIdentity); return $this; } diff --git a/tests/Security.DI/SecurityExtension.persistIdentity.phpt b/tests/Security.DI/SecurityExtension.persistIdentity.phpt new file mode 100644 index 00000000..4c839ca2 --- /dev/null +++ b/tests/Security.DI/SecurityExtension.persistIdentity.phpt @@ -0,0 +1,49 @@ +addExtension('foo', new HttpExtension); + $compiler->addExtension('bar', new SessionExtension); + $compiler->addExtension('security', new SecurityExtension); + $compiler->setClassName('ContainerDefault'); + + eval($compiler->compile()); + $container = new ContainerDefault; + + Assert::true($container->getService('security.user')->persistIdentity); +}); + + +test('disabled via configuration', function () { + $compiler = new DI\Compiler; + $compiler->addExtension('foo', new HttpExtension); + $compiler->addExtension('bar', new SessionExtension); + $compiler->addExtension('security', new SecurityExtension); + $compiler->setClassName('ContainerDisabled'); + + $loader = new Nette\DI\Config\Loader; + $config = $loader->load(Tester\FileMock::create(' +security: + authentication: + persistIdentity: false +', 'neon')); + + eval($compiler->addConfig($config)->compile()); + $container = new ContainerDisabled; + + Assert::false($container->getService('security.user')->persistIdentity); +}); diff --git a/tests/Security/User.persistIdentity.phpt b/tests/Security/User.persistIdentity.phpt new file mode 100644 index 00000000..4fdf3dcc --- /dev/null +++ b/tests/Security/User.persistIdentity.phpt @@ -0,0 +1,54 @@ +login(new SimpleIdentity('John Doe', 'admin')); + + $user->logout(); + + Assert::false($user->isLoggedIn()); + Assert::equal(new SimpleIdentity('John Doe', 'admin'), $user->getIdentity()); + Assert::same('John Doe', $user->getId()); +}); + + +test('identity is discarded after logout when persistIdentity is off', function () { + $user = new User(new MockUserStorage); + $user->persistIdentity = false; + $user->login(new SimpleIdentity('John Doe', 'admin')); + + $user->logout(); + + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); + Assert::null($user->getId()); +}); + + +test('identity already stored without authentication is not exposed when persistIdentity is off', function () { + $storage = new MockUserStorage; + $user = new User($storage); + $user->login(new SimpleIdentity('John Doe', 'admin')); + $user->logout(); // identity stays in storage, not authenticated + + // fresh User over the same storage with persistIdentity off (e.g. after disabling it in config) + $user = new User($storage); + $user->persistIdentity = false; + + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); + Assert::null($user->getId()); +}); From 9af540e72fe3452145f5190016d40b902ff9e0e8 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 May 2026 03:43:52 +0200 Subject: [PATCH 38/53] added guest identity --- phpstan.neon | 5 + readme.md | 15 +++ src/Security/IdentityHandler.php | 1 + src/Security/User.php | 35 +++-- tests/Security/User.emptyRoles.phpt | 62 +++++++++ tests/Security/User.guestIdentity.phpt | 157 +++++++++++++++++++++++ tests/Security/User.identityHandler.phpt | 12 ++ tests/Security/User.refreshStorage.phpt | 6 + 8 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 tests/Security/User.emptyRoles.phpt create mode 100644 tests/Security/User.guestIdentity.phpt diff --git a/phpstan.neon b/phpstan.neon index 9b6415cb..59de5727 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -32,3 +32,8 @@ parameters: identifier: argument.type count: 1 path: src/Security/User.php + + - # getGuestIdentity() is declared via @method as optional; method_exists() guards it at runtime + identifier: function.alreadyNarrowedType + count: 1 + path: src/Security/User.php diff --git a/readme.md b/readme.md index a28fb041..a9e93b80 100644 --- a/readme.md +++ b/readme.md @@ -183,6 +183,21 @@ If you prefer the identity to be discarded on every logout and expiration, set ` Identity is an object that implements the [Nette\Security\IIdentity](https://api.nette.org/master/Nette/Security/IIdentity.html) interface, the default implementation is [Nette\Security\SimpleIdentity](https://api.nette.org/3.0/Nette/Security/SimpleIdentity.html). And as mentioned, identity is stored in the session, so if, for example, we change the role of some of the logged-in users, old data will be kept in the identity until he logs in again. +Guest Identity +-------------- + +You can provide an identity even for users who are not logged in. If the authenticator implements `IdentityHandler` and its optional method `getGuestIdentity()`, the returned identity is used as a fallback for `getIdentity()`, `getId()` and `getRoles()`. So anonymous visitors can carry their own roles and data instead of the plain `guest` role: + +```php +public function getGuestIdentity(): ?Nette\Security\IIdentity +{ + return new Nette\Security\SimpleIdentity('guest', ['guest'], ['name' => 'Guest']); +} +``` + +The guest identity is resolved only when reading and is never stored. + + Authorization ============= diff --git a/src/Security/IdentityHandler.php b/src/Security/IdentityHandler.php index 3cdd856c..ed9956fe 100644 --- a/src/Security/IdentityHandler.php +++ b/src/Security/IdentityHandler.php @@ -10,6 +10,7 @@ /** * Serializes and restores identity to/from persistent storage. + * @method ?IIdentity getGuestIdentity() */ interface IdentityHandler { diff --git a/src/Security/User.php b/src/Security/User.php index 8637fe01..d8c699e0 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -44,7 +44,7 @@ class User /** @deprecated use User::LogoutInactivity */ public const INACTIVITY = self::LogoutInactivity; - /** default role for unauthenticated user */ + /** role for an unauthenticated user, unless a guest identity provides its own roles */ public string $guestRole = 'guest'; /** default role for authenticated user without own identity */ @@ -62,6 +62,8 @@ class User private ?IIdentity $identity = null; private ?bool $authenticated = null; private ?int $logoutReason = null; + private ?IIdentity $guestIdentity = null; + private bool $guestIdentityResolved = false; public function __construct( @@ -143,13 +145,13 @@ final public function isLoggedIn(): bool /** - * Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration) - * unless $persistIdentity is disabled, so its presence does not imply the user is logged in; null if none. + * Returns the user identity. When not logged in, this is the retained identity (unless $persistIdentity + * is disabled) or a guest identity if the authenticator provides one; null otherwise. */ final public function getIdentity(): ?IIdentity { $this->loadStoredData(); - return $this->identity; + return $this->identity ?? $this->resolveGuestIdentity(); } @@ -173,9 +175,23 @@ private function loadStoredData(): void } + /** Returns the guest identity provided by the IdentityHandler authenticator, or null. */ + private function resolveGuestIdentity(): ?IIdentity + { + if (!$this->guestIdentityResolved) { + $this->guestIdentityResolved = true; + $this->guestIdentity = $this->authenticator instanceof IdentityHandler && method_exists($this->authenticator, 'getGuestIdentity') + ? $this->authenticator->getGuestIdentity() + : null; + } + + return $this->guestIdentity; + } + + /** - * Returns the ID of the identity returned by getIdentity(), so it may be available even when not - * logged in; null if there is no identity. + * Returns the ID of the identity returned by getIdentity(), so it may be the retained or guest + * identity's ID even when not logged in; null if there is no identity. */ public function getId(): string|int|null { @@ -190,6 +206,8 @@ public function getId(): string|int|null final public function refreshStorage(): void { $this->identity = $this->authenticated = $this->logoutReason = null; + $this->guestIdentity = null; + $this->guestIdentityResolved = false; } @@ -199,6 +217,7 @@ final public function refreshStorage(): void public function setAuthenticator(IAuthenticator $handler): static { $this->authenticator = $handler; + $this->guestIdentityResolved = false; return $this; } @@ -257,13 +276,13 @@ final public function getLogoutReason(): ?int /** * Returns effective roles derived from the login state, not from the (possibly retained) identity. - * Logged in: the identity's roles, or authenticatedRole. Otherwise: the guestRole. + * Logged in: the identity's roles, or authenticatedRole. Otherwise: the guest identity's roles, or guestRole. * @return list */ public function getRoles(): array { if (!$this->isLoggedIn()) { - return [$this->guestRole]; + return $this->resolveGuestIdentity()?->getRoles() ?? [$this->guestRole]; } $identity = $this->getIdentity(); diff --git a/tests/Security/User.emptyRoles.phpt b/tests/Security/User.emptyRoles.phpt new file mode 100644 index 00000000..a43d4a64 --- /dev/null +++ b/tests/Security/User.emptyRoles.phpt @@ -0,0 +1,62 @@ +login('john', 'pass'); + + Assert::true($user->isLoggedIn()); + Assert::same([], $user->getRoles()); +}); + + +test('guest identity with empty roles is returned as-is (no guestRole)', function () { + $authenticator = new class implements Nette\Security\Authenticator, Nette\Security\IdentityHandler { + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity('john', ['admin']); + } + + + public function sleepIdentity(IIdentity $identity): IIdentity + { + return $identity; + } + + + public function wakeupIdentity(IIdentity $identity): ?IIdentity + { + return $identity; + } + + + public function getGuestIdentity(): ?IIdentity + { + return new SimpleIdentity('guest', []); + } + }; + $user = new User(new MockUserStorage, $authenticator); + + Assert::false($user->isLoggedIn()); + Assert::same([], $user->getRoles()); +}); diff --git a/tests/Security/User.guestIdentity.phpt b/tests/Security/User.guestIdentity.phpt new file mode 100644 index 00000000..78dbadd5 --- /dev/null +++ b/tests/Security/User.guestIdentity.phpt @@ -0,0 +1,157 @@ + 'Guest']); + } +} + + +class RecordingStorage implements Nette\Security\UserStorage +{ + /** @var list */ + public array $saved = []; + private bool $auth = false; + private ?IIdentity $identity = null; + + + public function saveAuthentication(IIdentity $identity): void + { + $this->saved[] = $identity; + $this->auth = true; + $this->identity = $identity; + } + + + public function clearAuthentication(bool $clearIdentity): void + { + $this->auth = false; + $this->identity = $clearIdentity ? null : $this->identity; + } + + + public function getState(): array + { + return [$this->auth, $this->identity, null]; + } + + + public function setExpiration(?string $expire, bool $clearIdentity): void + { + } +} + + +test('guest identity is exposed when not logged in', function () { + $user = new User(new MockUserStorage, new GuestAuthenticator); + + Assert::false($user->isLoggedIn()); + Assert::equal(new SimpleIdentity('guest', ['guest-role'], ['name' => 'Guest']), $user->getIdentity()); + Assert::same('guest', $user->getId()); + Assert::same(['guest-role'], $user->getRoles()); + Assert::true($user->isInRole('guest-role')); + Assert::false($user->isInRole('admin')); +}); + + +test('login overrides the guest identity', function () { + $user = new User(new MockUserStorage, new GuestAuthenticator); + $user->login('john', 'pass'); + + Assert::true($user->isLoggedIn()); + Assert::same('john', $user->getId()); + Assert::same(['admin'], $user->getRoles()); +}); + + +test('guest identity returns after logout that clears identity', function () { + $user = new User(new MockUserStorage, new GuestAuthenticator); + $user->login('john', 'pass'); + + $user->logout(clearIdentity: true); + + Assert::false($user->isLoggedIn()); + Assert::same('guest', $user->getId()); + Assert::same(['guest-role'], $user->getRoles()); +}); + + +test('retained identity stays for personalization but roles fall back to the guest identity', function () { + $user = new User(new MockUserStorage, new GuestAuthenticator); + $user->login('john', 'pass'); + + $user->logout(); // persistIdentity is on by default -> identity is retained + + Assert::false($user->isLoggedIn()); + Assert::same('john', $user->getId()); // retained real identity for personalization + Assert::same(['guest-role'], $user->getRoles()); // but NOT the real ['admin'] roles +}); + + +test('without a guest identity provider the behaviour is unchanged', function () { + $authenticator = new class implements Nette\Security\Authenticator { + public function authenticate(string $username, string $password): IIdentity + { + return new SimpleIdentity('john', ['admin']); + } + }; + $user = new User(new MockUserStorage, $authenticator); + + Assert::false($user->isLoggedIn()); + Assert::null($user->getIdentity()); + Assert::same(['guest'], $user->getRoles()); +}); + + +test('guest identity is never written to storage', function () { + $storage = new RecordingStorage; + $user = new User($storage, new GuestAuthenticator); + + // acting as a guest must not persist anything + Assert::same('guest', $user->getId()); + Assert::same(['guest-role'], $user->getRoles()); + Assert::equal(new SimpleIdentity('guest', ['guest-role'], ['name' => 'Guest']), $user->getIdentity()); + Assert::same([], $storage->saved); + + // login/logout cycle, then guest again + $user->login('john', 'pass'); + $user->logout(clearIdentity: true); + Assert::same('guest', $user->getId()); + + // only the real identity may ever reach the storage + Assert::same(['john'], array_map(fn(IIdentity $i) => $i->getId(), $storage->saved)); +}); diff --git a/tests/Security/User.identityHandler.phpt b/tests/Security/User.identityHandler.phpt index 56ee376a..dbd654d4 100644 --- a/tests/Security/User.identityHandler.phpt +++ b/tests/Security/User.identityHandler.phpt @@ -52,6 +52,12 @@ class AuthenticatorWithHandler implements Nette\Security\Authenticator, Nette\Se // Real implementation would fetch fresh data from DB return new SimpleIdentity($identity->getId(), ['admin', 'user'], ['name' => 'John Doe Updated']); } + + + public function getGuestIdentity(): ?IIdentity + { + return null; + } } @@ -124,6 +130,12 @@ test('IdentityHandler.wakeupIdentity() returning null logs user out', function ( // Simulate invalid token/expired session return null; } + + + public function getGuestIdentity(): ?IIdentity + { + return null; + } }; $storage = new MockUserStorage; diff --git a/tests/Security/User.refreshStorage.phpt b/tests/Security/User.refreshStorage.phpt index b9bb427c..5bfd4052 100644 --- a/tests/Security/User.refreshStorage.phpt +++ b/tests/Security/User.refreshStorage.phpt @@ -195,6 +195,12 @@ test('refreshStorage() with IdentityHandler triggers wakeup again', function () array_merge($identity->getRoles(), ['role' . $this->wakeupCount]), ); } + + + public function getGuestIdentity(): ?IIdentity + { + return null; + } }; $storage = new MutableStorage; From 0cd092c28d3bf4fddd76a3bc53401ec491c00bc0 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 28 May 2026 01:43:43 +0200 Subject: [PATCH 39/53] phpstan.neon: narrow ignore --- phpstan.neon | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 59de5727..d957064d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,6 +14,7 @@ parameters: ignoreErrors: - # generated Latte code: attribute interpolation emits a dead `=== null` check for values that are never null identifier: identical.alwaysFalse + count: 2 path: src/Bridges/SecurityTracy/dist/tab.phtml - message: '#^Method Nette\\Http\\IResponse\:\:setCookie\(\) invoked with 8 parameters, 3\-7 required\.$#' @@ -32,8 +33,3 @@ parameters: identifier: argument.type count: 1 path: src/Security/User.php - - - # getGuestIdentity() is declared via @method as optional; method_exists() guards it at runtime - identifier: function.alreadyNarrowedType - count: 1 - path: src/Security/User.php From 4a1c657b0b51903765c038ab7f8b5a4e2b0b004e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 28 May 2026 01:40:58 +0200 Subject: [PATCH 40/53] readonly properties --- src/Security/SimpleAuthenticator.php | 6 +++--- src/Security/User.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Security/SimpleAuthenticator.php b/src/Security/SimpleAuthenticator.php index 0e5b24b4..30b120cf 100644 --- a/src/Security/SimpleAuthenticator.php +++ b/src/Security/SimpleAuthenticator.php @@ -16,11 +16,11 @@ class SimpleAuthenticator implements Authenticator public function __construct( /** @var array */ #[\SensitiveParameter] - private array $passwords, + private readonly array $passwords, /** @var array|null> */ - private array $roles = [], + private readonly array $roles = [], /** @var array> */ - private array $data = [], + private readonly array $data = [], ) { } diff --git a/src/Security/User.php b/src/Security/User.php index d8c699e0..e5222f3c 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -67,7 +67,7 @@ class User public function __construct( - private UserStorage $storage, + private readonly UserStorage $storage, private ?IAuthenticator $authenticator = null, private ?Authorizator $authorizator = null, ) { @@ -196,7 +196,7 @@ private function resolveGuestIdentity(): ?IIdentity public function getId(): string|int|null { $identity = $this->getIdentity(); - return $identity ? $identity->getId() : null; + return $identity?->getId(); } From 63aa20f03eb8c8881289827eef2c460bc1581b43 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 15 Jun 2026 07:24:31 +0200 Subject: [PATCH 41/53] fixed compatibility with nette/http 3.4 Since nette/http 3.4 SessionSection::setExpiration() parses its argument via Helpers::expirationToSeconds(), where a bare numeric string is taken as a relative number of seconds and an empty string throws. Passing the absolute timestamp as (string) therefore broke: a null expiration produced '' and threw, and a real timestamp triggered the absolute-timestamp deprecation. Pass null when unset and an '@'-prefixed timestamp otherwise, which is parsed as an absolute, timezone-independent time. --- src/Bridges/SecurityHttp/SessionStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/SecurityHttp/SessionStorage.php b/src/Bridges/SecurityHttp/SessionStorage.php index 27396e8a..e4aca259 100644 --- a/src/Bridges/SecurityHttp/SessionStorage.php +++ b/src/Bridges/SecurityHttp/SessionStorage.php @@ -91,7 +91,7 @@ private function setupExpiration(): void } $section->set('expireIdentity', $this->expireIdentity); - $section->setExpiration((string) $this->expireTime, 'foo'); // time check + $section->setExpiration($this->expireTime === null ? null : '@' . $this->expireTime, 'foo'); // time check } From 50ce3c4b8edd27cb134237cac3af514a84331524 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 2 Jun 2026 03:03:07 +0200 Subject: [PATCH 42/53] Identity: use ctype_digit for stricter numeric ID detection --- src/Security/Identity.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Security/Identity.php b/src/Security/Identity.php index e006cbfc..56cc1329 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -7,7 +7,7 @@ namespace Nette\Security; -use function in_array, is_float, is_numeric, iterator_to_array; +use function ctype_digit, in_array, is_string, iterator_to_array; /** @@ -46,7 +46,7 @@ public function __construct(string|int $id, string|array|null $roles = null, ?it */ public function setId(string|int $id): static { - $this->id = is_numeric($id) && !is_float($tmp = $id * 1) ? $tmp : $id; + $this->id = is_string($id) && ctype_digit($id) && (string) ($tmp = (int) $id) === $id ? $tmp : $id; return $this; } From 7caaaee658cf464d85f238dd3b1ff669ad680bfb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 May 2026 03:43:56 +0200 Subject: [PATCH 43/53] added CLAUDE.md --- CLAUDE.md | 580 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 580 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a1da84d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,580 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Nette Security is a standalone PHP library providing authentication, authorization, and role-based access control (ACL) for the Nette Framework ecosystem. + +- **Type**: Reusable PHP library/package +- **PHP Support**: 8.1 - 8.5 +- **Key Components**: Authentication (User login/logout), Authorization (Permission checking), ACL (Access Control Lists) +- **Documentation**: https://doc.nette.org/access-control + +## Essential Commands + +### Testing +```bash +# Run all tests +composer run tester + +# Run specific test file +vendor/bin/tester tests/Security/User.login.phpt -s + +# Run tests in specific directory +vendor/bin/tester tests/Security.DI/ -s + +# Single-threaded execution (useful for debugging) +vendor/bin/tester tests -s +``` + +### Code Quality +```bash +# Run PHPStan static analysis +composer run phpstan +``` + +## Architecture Overview + +### Three-Pillar Design + +The library separates security concerns into three independent domains: + +1. **Authentication** (`src/Security/User.php`, `Authenticator.php`) + - User identity verification (login/logout) + - Session management and expiration + - Credential validation through pluggable authenticators + +2. **Authorization** (`src/Security/Permission.php`, `Authorizator.php`) + - Role-based access control + - Resource and privilege management + - Dynamic permission assertions + +3. **Persistence** (`src/Bridges/SecurityHttp/`) + - Session-based storage (default) + - Cookie-based storage (alternative) + - Custom storage implementations via `UserStorage` interface + +### Interface-First Philosophy + +The codebase uses interface segregation for extensibility: + +- `Authenticator` → `SimpleAuthenticator` (example implementation) +- `Authorizator` → `Permission` (full ACL implementation) +- `UserStorage` → `SessionStorage`, `CookieStorage` +- `IIdentity` → `SimpleIdentity` + +**Key principle**: Developers implement interfaces, never extend concrete classes. + +### Directory Structure + +``` +src/ +├── Security/ # Core authentication/authorization +│ ├── User.php # Central user management (250 lines) +│ ├── Permission.php # Complete ACL implementation (600 lines) +│ ├── Passwords.php # Password hashing utilities +│ ├── Authenticator.php # Authentication contract +│ └── SimpleAuthenticator.php # Basic implementation for testing +│ +└── Bridges/ # Framework integration + ├── SecurityDI/ # Dependency injection container + ├── SecurityHttp/ # Session and cookie storage + └── SecurityTracy/ # Debugger panel +``` + +## Testing Conventions + +### Test File Format + +Tests use Nette Tester with `.phpt` extension: + +```php +isLoggedIn()); +}); +``` + +**Important**: Use `test()` function with descriptive title as first parameter. Do not add comments before `test()` calls. + +### Testing Exceptions + +Use `Assert::exception()` for expected exceptions: + +```php +Assert::exception( + fn() => $user->login('invalid', 'credentials'), + Nette\Security\AuthenticationException::class, + 'User not found.', +); +``` + +For entire test blocks that should throw, use `testException()`. + +### Test Organization + +- **Unit tests**: `tests/Security/` - Core functionality (User, Permission, Passwords) +- **Integration tests**: `tests/Security.DI/` - DI container integration +- **Storage tests**: `tests/Security.Http/` - Session/Cookie storage +- **ACL tests**: `tests/Security/Permission*.phpt` - 30+ comprehensive ACL scenarios + +## Coding Standards + +### General Rules + +- Every PHP file must include `declare(strict_types=1)` +- Use TABS for indentation (never spaces) +- Single quotes for strings unless containing apostrophes +- All code, comments, variables in English only +- Return type and opening brace on separate lines for methods +- No space before parentheses in arrow functions: `fn($a) => $b` + +### Type Declarations + +- All properties, parameters, and return values must have types +- Interface methods don't need visibility (always public) +- Use `#[\SensitiveParameter]` attribute for password parameters + +### Naming Conventions + +- PascalCase for classes, interfaces, constants +- camelCase for methods and properties +- Never use `Abstract`, `Interface`, or `I` prefixes + +### Documentation + +- Focus on describing purpose, not duplicating signature information +- Start method docs with 3rd person singular present tense verb +- Document array contents: `@return string[]` +- Use two spaces after `@param` and `@return` type declarations + +Example: +```php +/** + * Verifies user credentials against database. + * @return SimpleIdentity User identity with roles and metadata + * @throws AuthenticationException + */ +public function authenticate(string $username, string $password): IIdentity +{ + // Implementation +} +``` + +## Important Implementation Patterns + +### Identity Persistence After Logout + +**Critical behavior**: Logout does NOT delete identity by default. Identity remains available for personalization even when not authenticated. + +```php +$user->logout(); // Logs out but keeps identity +$user->logout(true); // Logs out AND clears identity +$user->getIdentity(); // Still available after logout() +$user->isLoggedIn(); // false after logout() + +// Check why user was logged out +$reason = $user->getLogoutReason(); +if ($reason === Nette\Security\UserStorage::LogoutInactivity) { + // User was logged out due to inactivity timeout +} elseif ($reason === Nette\Security\UserStorage::LogoutManual) { + // User was logged out manually via logout() +} +``` + +To always discard the identity on logout or expiration, set `$user->persistIdentity = false` (or the `persistIdentity` config option). Retaining the identity is best-effort and depends on the storage implementation. + +### IdentityHandler Interface + +Implement `IdentityHandler` to customize how identity is saved/restored from storage: + +```php +final class Authenticator implements + Nette\Security\Authenticator, + Nette\Security\IdentityHandler +{ + public function sleepIdentity(IIdentity $identity): IIdentity + { + // Called before identity is written to storage + // Useful for: replacing full identity with token-only proxy (for cookie storage) + return new SimpleIdentity($identity->authtoken); + } + + public function wakeupIdentity(IIdentity $identity): ?IIdentity + { + // Called after identity is read from storage + // Useful for: refreshing user roles from database, validating tokens + $userId = $identity->getId(); + $identity->setRoles($this->getUserRoles($userId)); + return $identity; // Return null to log user out + } +} +``` + +**Use cases:** +- Updating user roles on each request without re-login +- Cookie-based authentication with auth tokens +- Validating session integrity + +### Guest Identity + +An `IdentityHandler` may optionally implement `getGuestIdentity(): ?IIdentity` to provide an identity for anonymous (not logged-in) users. When present, `getIdentity()`, `getId()` and `getRoles()` fall back to it, so guests can carry their own roles and data instead of just the `$guestRole` string. + +```php +public function getGuestIdentity(): ?IIdentity +{ + return new SimpleIdentity('guest', ['guest'], ['name' => 'Guest']); +} +``` + +The guest identity is resolved on read only and is never written to storage. While not logged in, `getRoles()` (and thus `isAllowed()`/`isInRole()`) uses the guest roles. + +### Events: $onLoggedIn, $onLoggedOut + +User object provides events for login/logout lifecycle hooks: + +```php +$user->onLoggedIn[] = function (Nette\Security\User $user) { + // Log login event, update last_login timestamp, send notification, etc. +}; + +$user->onLoggedOut[] = function (Nette\Security\User $user) { + // Clear user-specific cache, log logout event, etc. +}; +``` + +### Role Inheritance and Weight + +When a role inherits from multiple parents with conflicting permissions, **last role has highest weight**: + +```php +$acl->addRole('john', ['admin', 'guest']); // 'guest' wins conflicts +$acl->addRole('mary', ['guest', 'admin']); // 'admin' wins conflicts +``` + +### Session Namespace for Multiple Authentications + +Support independent authentication contexts within single session. **Critical**: Set namespace in `checkRequirements()` of base presenter: + +```php +// In BasePresenter for admin module +public function checkRequirements($element): void +{ + $this->getUser()->getStorage()->setNamespace('backend'); + parent::checkRequirements($element); +} + +// In BasePresenter for frontend +public function checkRequirements($element): void +{ + $this->getUser()->getStorage()->setNamespace('frontend'); + parent::checkRequirements($element); +} +``` + +### Multiple Authenticators + +When using different authenticators for different parts of application, restrict autowiring with `autowired: self`: + +```neon +services: + - + create: FrontAuthenticator + autowired: self # Only autowire when explicitly requested + - + create: AdminAuthenticator + autowired: self +``` + +Then inject specific authenticator and set it before login: + +```php +class SignPresenter extends Nette\Application\UI\Presenter +{ + public function __construct( + private FrontAuthenticator $authenticator, + ) { + } + + protected function createComponentSignInForm(): Form + { + $form->onSuccess[] = function ($form, $data) { + $user = $this->getUser(); + $user->setAuthenticator($this->authenticator); + $user->login($data->username, $data->password); + }; + } +} +``` + +### Dynamic Permission Assertions + +Use callbacks for context-aware authorization: + +```php +$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool { + $role = $acl->getQueriedRole(); // Actual role object + $resource = $acl->getQueriedResource(); // Actual resource object + return $role->id === $resource->authorId; // Custom logic +}; + +$acl->allow('registered', 'article', 'edit', $assertion); +``` + +### AuthorizatorFactory Pattern + +Create Permission ACL as a DI service using factory method: + +```php +namespace App\Model; + +class AuthorizatorFactory +{ + public static function create(): Nette\Security\Permission + { + $acl = new Nette\Security\Permission; + + // Define roles + $acl->addRole('guest'); + $acl->addRole('registered', 'guest'); + $acl->addRole('admin', 'registered'); + + // Define resources + $acl->addResource('article'); + $acl->addResource('comment'); + + // Define permissions + $acl->allow('guest', ['article', 'comment'], 'view'); + $acl->allow('registered', 'comment', 'add'); + $acl->allow('admin', $acl::All, ['view', 'edit', 'add']); + + return $acl; + } +} +``` + +Register in configuration: + +```neon +services: + - App\Model\AuthorizatorFactory::create +``` + +### Password Hashing Best Practices + +The `Passwords` class handles secure password hashing with bcrypt: + +```php +// Hash contains algorithm identifier, cost, and salt - store all together +$hash = $passwords->hash($password); // Store this in database (255 chars recommended) + +// Verify password +if ($passwords->verify($password, $hash)) { + // Password correct +} + +// Upgrade hash when algorithm/cost changes +if ($passwords->needsRehash($hash)) { + $newHash = $passwords->hash($password); + // Update database with new hash +} +``` + +**Cost parameter** (higher = slower = more secure): +- Cost 10: ~80ms (default) +- Cost 11: ~160ms +- Cost 12: ~320ms (recommended for production) + +Configure in NEON: + +```neon +services: + security.passwords: Nette\Security\Passwords(::PASSWORD_BCRYPT, [cost: 12]) +``` + +## NEON Configuration + +### Simple Authentication (Testing Only) + +Define users directly in configuration using `SimpleAuthenticator`: + +```neon +security: + # Show user panel in Tracy Bar + debugger: true # (bool) defaults to true + + users: + # Simple format: username: password + johndoe: secret123 + + # Extended format with roles and data + janedoe: + password: secret123 + roles: [admin] + data: + name: Jane Doe + email: jane@example.com +``` + +### ACL Configuration + +Define roles and resources as configuration basis for `Permission`: + +```neon +security: + roles: + guest: + registered: [guest] # Inherits from guest + admin: [registered] # Inherits from registered + + resources: + article: + comment: [article] # Inherits from article + poll: +``` + +### User Storage Configuration + +```neon +security: + authentication: + # Period of inactivity before logout + expiration: 30 minutes + + # Storage type: session (default) or cookie + storage: session + + # Keep identity available after logout/expiration for personalization (defaults to true) + persistIdentity: true +``` + +### Cookie Storage Configuration + +When using `storage: cookie`, additional options are available: + +```neon +security: + authentication: + storage: cookie + cookieName: userId # (string) defaults to 'userid' + cookieDomain: 'example.com' # (string|domain) + cookieSamesite: Lax # (Strict|Lax|None) defaults to Lax +``` + +**Important**: Cookie storage requires implementing `IdentityHandler` to store only auth token (not full identity) in cookie. + +### DI Services + +These services are automatically registered in the DI container: + +| Service Name | Type | Description | +|---------------------------|------------------------------|----------------------------------| +| `security.authenticator` | `Authenticator` | Credential verification | +| `security.authorizator` | `Authorizator` | Permission checking (ACL) | +| `security.passwords` | `Passwords` | Password hashing utilities | +| `security.user` | `User` | Current user management | +| `security.userStorage` | `UserStorage` | Identity persistence layer | + +Access via dependency injection: + +```php +class MyService +{ + public function __construct( + private Nette\Security\User $user, + private Nette\Security\Passwords $passwords, + ) { + } +} +``` + +## Presenter Integration Patterns + +### Verify Login in Presenters + +Use `startup()` method to enforce authentication: + +```php +protected function startup() +{ + parent::startup(); + if (!$this->getUser()->isLoggedIn()) { + $this->redirect('Sign:in'); + } +} +``` + +### Verify Permissions in Presenters + +Use `startup()` method to enforce authorization: + +```php +protected function startup() +{ + parent::startup(); + if (!$this->getUser()->isAllowed('backend')) { + $this->error('Forbidden', 403); + } +} +``` + +**"Less Code, More Security" Principle**: When checking roles with `isInRole()`, you don't need to verify `isLoggedIn()` first. The method automatically works with effective roles: logged-in users get their assigned roles, logged-out users automatically get the special `guest` role. This reduces boilerplate while maintaining security: + +```php +// GOOD: Simple and secure +if ($user->isInRole('admin')) { + deleteItem(); +} + +// BAD: Unnecessary verbosity +if ($user->isLoggedIn() && $user->isInRole('admin')) { + deleteItem(); +} +``` + +### Set Session Namespace for Module + +Use `checkRequirements()` in BasePresenter: + +```php +public function checkRequirements($element): void +{ + $this->getUser()->getStorage()->setNamespace('backend'); + parent::checkRequirements($element); +} +``` + +## Common Pitfalls + +1. **Don't check isLoggedIn() before isInRole()** - `isInRole()` handles logged-out users with automatic 'guest' role +2. **Don't assume logout() clears identity** - Use `logout(true)` if you need to clear identity +3. **Remember ACL inheritance order** - Last parent role has highest weight in conflicts +4. **Session expiration must be ≤ session lifetime** - Set with `$user->setExpiration('30 minutes')` +5. **Password hash storage needs 255 chars** - Hash contains algorithm, cost, and salt; use VARCHAR(255) or TEXT +6. **Cookie storage requires IdentityHandler** - Never store full identity in cookie, only auth token +7. **Don't use SimpleAuthenticator in production** - It's for testing only; implement custom Authenticator with database +8. **Update hashes when algorithm changes** - Use `needsRehash()` to detect and upgrade old hashes on login + +## Commit Message Style + +- Lowercase, imperative mood +- No period at end +- Format: `component: change description` or direct action +- Examples: `User: support for custom authenticators`, `fixed session expiration handling` + +## CI/CD Pipeline + +GitHub Actions run on all pull requests: +- Tests across PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- Code style checks (Nette Code Checker) +- Static analysis (PHPStan Level 5) +- Code coverage tracking From 223caa0ca8ca9f8d796ea388b09e15121248da36 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 1 Mar 2021 15:30:34 +0100 Subject: [PATCH 44/53] opened 4.0-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 570486cc..0c6f74f9 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "config": { From fc68696f93bf2b5a4b36bb6cf93bafd8093054e9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 30 Oct 2025 23:51:04 +0100 Subject: [PATCH 45/53] requires PHP 8.3 --- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 6 +++--- CLAUDE.md | 4 ++-- composer.json | 2 +- readme.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b5b0dffc..1d8d0645 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5ee52c1e..686f085b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + php: ['8.3', '8.4', '8.5'] fail-fast: false @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/CLAUDE.md b/CLAUDE.md index a1da84d9..594c3963 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Nette Security is a standalone PHP library providing authentication, authorization, and role-based access control (ACL) for the Nette Framework ecosystem. - **Type**: Reusable PHP library/package -- **PHP Support**: 8.1 - 8.5 +- **PHP Support**: 8.3 - 8.5 - **Key Components**: Authentication (User login/logout), Authorization (Permission checking), ACL (Access Control Lists) - **Documentation**: https://doc.nette.org/access-control @@ -574,7 +574,7 @@ public function checkRequirements($element): void ## CI/CD Pipeline GitHub Actions run on all pull requests: -- Tests across PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- Tests across PHP 8.3, 8.4, 8.5 - Code style checks (Nette Code Checker) - Static analysis (PHPStan Level 5) - Code coverage tracking diff --git a/composer.json b/composer.json index 0c6f74f9..169ee8af 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "8.1 - 8.5", + "php": "8.3 - 8.5", "nette/utils": "^4.0" }, "require-dev": { diff --git a/readme.md b/readme.md index a9e93b80..9853eab9 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ Authentication & Authorization library for Nette. Documentation can be found on the [website](https://doc.nette.org/access-control). -It requires PHP version 8.1 and supports PHP up to 8.5. +It requires PHP version 8.3 and supports PHP up to 8.5. [Support Me](https://github.com/sponsors/dg) From a663c1751b861357f971c8a1aa4a65a36691fc7b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 11 Dec 2023 13:59:38 +0100 Subject: [PATCH 46/53] uses nette/http 4 --- composer.json | 4 ++-- src/Bridges/SecurityHttp/CookieStorage.php | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 169ee8af..84842ec0 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "nette/di": "^3.1", - "nette/http": "^3.2", + "nette/http": "^4.0", "nette/tester": "^2.6", "tracy/tracy": "^2.9", "phpstan/phpstan": "^2.1@stable", @@ -30,7 +30,7 @@ }, "conflict": { "nette/di": "<3.0-stable", - "nette/http": "<3.1.3" + "nette/http": "<4" }, "autoload": { "classmap": ["src/"], diff --git a/src/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index ab9a2d19..03b5a2e9 100644 --- a/src/Bridges/SecurityHttp/CookieStorage.php +++ b/src/Bridges/SecurityHttp/CookieStorage.php @@ -45,11 +45,8 @@ public function saveAuthentication(IIdentity $identity): void $this->cookieName, $uid, $this->cookieExpiration, - null, - $this->cookieDomain, - null, - true, - $this->cookieSameSite, + domain: $this->cookieDomain, + sameSite: $this->cookieSameSite, ); } @@ -59,8 +56,7 @@ public function clearAuthentication(bool $clearIdentity): void $this->uid = ''; $this->response->deleteCookie( $this->cookieName, - null, - $this->cookieDomain, + domain: $this->cookieDomain, ); } From 5b594674e78dbde321915bcc2ce6cd5b51110db9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 23 Dec 2025 22:50:45 +0100 Subject: [PATCH 47/53] used PHP 8.2 features --- src/Security/Identity.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Security/Identity.php b/src/Security/Identity.php index 56cc1329..cfc5dbbd 100644 --- a/src/Security/Identity.php +++ b/src/Security/Identity.php @@ -35,9 +35,7 @@ public function __construct(string|int $id, string|array|null $roles = null, ?it { $this->setId($id); $this->setRoles((array) $roles); - $this->data = $data instanceof \Traversable - ? iterator_to_array($data) - : (array) $data; + $this->data = iterator_to_array($data ?? []); } From 9b3c3268e9498f4e6e70ae78ae0e0626776a5f4e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 20 Sep 2021 18:39:13 +0200 Subject: [PATCH 48/53] removed deprecated IAuthenticator (BC break) --- src/Bridges/SecurityDI/SecurityExtension.php | 2 +- src/Security/Authenticator.php | 2 +- src/Security/IAuthenticator.php | 35 -------------------- src/Security/User.php | 14 ++++---- 4 files changed, 8 insertions(+), 45 deletions(-) delete mode 100644 src/Security/IAuthenticator.php diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 37192887..8cbcebda 100644 --- a/src/Bridges/SecurityDI/SecurityExtension.php +++ b/src/Bridges/SecurityDI/SecurityExtension.php @@ -112,7 +112,7 @@ public function loadConfiguration(): void } $builder->addDefinition($this->prefix('authenticator')) - ->setType(Nette\Security\IAuthenticator::class) + ->setType(Nette\Security\Authenticator::class) ->setFactory(Nette\Security\SimpleAuthenticator::class, [$usersList, $usersRoles, $usersData]); if ($this->name === 'security') { diff --git a/src/Security/Authenticator.php b/src/Security/Authenticator.php index 2b47c70e..42c0cab3 100644 --- a/src/Security/Authenticator.php +++ b/src/Security/Authenticator.php @@ -11,7 +11,7 @@ /** * Performs authentication. */ -interface Authenticator extends IAuthenticator +interface Authenticator { /** Exception error code */ public const diff --git a/src/Security/IAuthenticator.php b/src/Security/IAuthenticator.php deleted file mode 100644 index 443f37b6..00000000 --- a/src/Security/IAuthenticator.php +++ /dev/null @@ -1,35 +0,0 @@ - $roles * @property-read ?int $logoutReason - * @property IAuthenticator $authenticator + * @property Authenticator $authenticator * @property Authorizator $authorizator */ class User @@ -68,7 +68,7 @@ class User public function __construct( private readonly UserStorage $storage, - private ?IAuthenticator $authenticator = null, + private ?Authenticator $authenticator = null, private ?Authorizator $authorizator = null, ) { } @@ -99,9 +99,7 @@ public function login( $this->identity = $username; } else { $authenticator = $this->getAuthenticator(); - $this->identity = $authenticator instanceof Authenticator - ? $authenticator->authenticate(...func_get_args()) - : $authenticator->authenticate(func_get_args()); + $this->identity = $authenticator->authenticate(...func_get_args()); } $id = $this->authenticator instanceof IdentityHandler @@ -214,7 +212,7 @@ final public function refreshStorage(): void /** * Sets authentication handler. */ - public function setAuthenticator(IAuthenticator $handler): static + public function setAuthenticator(Authenticator $handler): static { $this->authenticator = $handler; $this->guestIdentityResolved = false; @@ -225,7 +223,7 @@ public function setAuthenticator(IAuthenticator $handler): static /** * Returns authentication handler. */ - final public function getAuthenticator(): IAuthenticator + final public function getAuthenticator(): Authenticator { if (!$this->authenticator) { throw new Nette\InvalidStateException('Authenticator has not been set.'); @@ -238,7 +236,7 @@ final public function getAuthenticator(): IAuthenticator /** * Returns authentication handler, or null if none is set. */ - final public function getAuthenticatorIfExists(): ?IAuthenticator + final public function getAuthenticatorIfExists(): ?Authenticator { return $this->authenticator; } From 0fe0f5f16365b00a961c012f385ff2d91bd7988a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Dec 2021 18:12:59 +0100 Subject: [PATCH 49/53] IIdentity: added return typehint (BC break) --- src/Security/IIdentity.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Security/IIdentity.php b/src/Security/IIdentity.php index 774a8fa0..1b820f98 100644 --- a/src/Security/IIdentity.php +++ b/src/Security/IIdentity.php @@ -16,9 +16,8 @@ interface IIdentity { /** * Returns the ID of user. - * @return string|int */ - function getId(); + function getId(): string|int; /** * Returns a list of roles that the user is a member of. From 78c930aaa000bdd24bb3ad86be6156723b44be3c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 15 Jun 2026 07:42:52 +0200 Subject: [PATCH 50/53] Identity -> SimpleIdentity Identity class must exist as an alias in order to read it from session --- src/Security/Identity.php | 125 -------------------------------- src/Security/SimpleIdentity.php | 111 +++++++++++++++++++++++++++- src/compatibility-intf.php | 35 +++++++++ src/compatibility.php | 26 +------ tests/Security/Identity.phpt | 34 --------- 5 files changed, 149 insertions(+), 182 deletions(-) delete mode 100644 src/Security/Identity.php create mode 100644 src/compatibility-intf.php delete mode 100644 tests/Security/Identity.phpt diff --git a/src/Security/Identity.php b/src/Security/Identity.php deleted file mode 100644 index cfc5dbbd..00000000 --- a/src/Security/Identity.php +++ /dev/null @@ -1,125 +0,0 @@ - $roles - * @property array $data - */ -class Identity implements IIdentity -{ - private string|int $id; - - /** @var list */ - private array $roles; - - /** @var array */ - private array $data; - - - /** - * @param string|list|null $roles - * @param ?iterable $data - */ - public function __construct(string|int $id, string|array|null $roles = null, ?iterable $data = null) - { - $this->setId($id); - $this->setRoles((array) $roles); - $this->data = iterator_to_array($data ?? []); - } - - - /** - * Sets the ID of user. - */ - public function setId(string|int $id): static - { - $this->id = is_string($id) && ctype_digit($id) && (string) ($tmp = (int) $id) === $id ? $tmp : $id; - return $this; - } - - - /** - * Returns the ID of user. - */ - public function getId(): string|int - { - return $this->id; - } - - - /** - * Sets a list of roles that the user is a member of. - * @param list $roles - */ - public function setRoles(array $roles): static - { - $this->roles = $roles; - return $this; - } - - - /** - * Returns a list of roles that the user is a member of. - * @return list - */ - public function getRoles(): array - { - return $this->roles; - } - - - /** - * Returns user data. - * @return array - */ - public function getData(): array - { - return $this->data; - } - - - /** - * Sets user data value. - */ - public function __set(string $key, mixed $value): void - { - match ($key) { - 'id' => $this->setId($value), - 'roles' => $this->setRoles($value), - 'data' => $this->data = $value, - default => $this->data[$key] = $value, - }; - } - - - /** - * Returns user data value. - */ - public function &__get(string $key): mixed - { - if (in_array($key, ['id', 'roles', 'data'], strict: true)) { - $res = $this->{'get' . ucfirst($key)}(); - return $res; - - } else { - return $this->data[$key]; - } - } - - - public function __isset(string $key): bool - { - return isset($this->data[$key]) || in_array($key, ['id', 'roles', 'data'], strict: true); - } -} diff --git a/src/Security/SimpleIdentity.php b/src/Security/SimpleIdentity.php index c987f6ff..b831a288 100644 --- a/src/Security/SimpleIdentity.php +++ b/src/Security/SimpleIdentity.php @@ -7,10 +7,119 @@ namespace Nette\Security; +use function ctype_digit, in_array, is_string, iterator_to_array; + /** * Default implementation of IIdentity. + * @property string|int $id + * @property list $roles + * @property array $data */ -class SimpleIdentity extends Identity +class SimpleIdentity implements IIdentity { + private string|int $id; + + /** @var list */ + private array $roles; + + /** @var array */ + private array $data; + + + /** + * @param string|list|null $roles + * @param ?iterable $data + */ + public function __construct(string|int $id, string|array|null $roles = null, ?iterable $data = null) + { + $this->setId($id); + $this->setRoles((array) $roles); + $this->data = iterator_to_array($data ?? []); + } + + + /** + * Sets the ID of user. + */ + public function setId(string|int $id): static + { + $this->id = is_string($id) && ctype_digit($id) && (string) ($tmp = (int) $id) === $id ? $tmp : $id; + return $this; + } + + + /** + * Returns the ID of user. + */ + public function getId(): string|int + { + return $this->id; + } + + + /** + * Sets a list of roles that the user is a member of. + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + return $this; + } + + + /** + * Returns a list of roles that the user is a member of. + * @return list + */ + public function getRoles(): array + { + return $this->roles; + } + + + /** + * Returns user data. + * @return array + */ + public function getData(): array + { + return $this->data; + } + + + /** + * Sets user data value. + */ + public function __set(string $key, mixed $value): void + { + match ($key) { + 'id' => $this->setId($value), + 'roles' => $this->setRoles($value), + 'data' => $this->data = $value, + default => $this->data[$key] = $value, + }; + } + + + /** + * Returns user data value. + */ + public function &__get(string $key): mixed + { + if (in_array($key, ['id', 'roles', 'data'], strict: true)) { + $res = $this->{'get' . ucfirst($key)}(); + return $res; + + } else { + return $this->data[$key]; + } + } + + + public function __isset(string $key): bool + { + return isset($this->data[$key]) || in_array($key, ['id', 'roles', 'data'], strict: true); + } } diff --git a/src/compatibility-intf.php b/src/compatibility-intf.php new file mode 100644 index 00000000..fc11a813 --- /dev/null +++ b/src/compatibility-intf.php @@ -0,0 +1,35 @@ + 'John']); - - Assert::same(12, $id->getId()); - Assert::same(12, $id->id); - Assert::same(['admin'], $id->getRoles()); - Assert::same(['admin'], $id->roles); - Assert::same(['name' => 'John'], $id->getData()); - Assert::same(['name' => 'John'], $id->data); - Assert::same('John', $id->name); -}); - - -test('', function () { - $id = new Identity('12'); - Assert::same(12, $id->getId()); - - - $id = new Identity('12345678901234567890'); - Assert::same('12345678901234567890', $id->getId()); -}); From 2407d2521e728eb3d20a50afee12f2c6d12b43e5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 20 Sep 2021 18:29:35 +0200 Subject: [PATCH 51/53] SimpleIdentity: uses __serialize & __unserialize --- src/Security/SimpleIdentity.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Security/SimpleIdentity.php b/src/Security/SimpleIdentity.php index b831a288..ac749cc1 100644 --- a/src/Security/SimpleIdentity.php +++ b/src/Security/SimpleIdentity.php @@ -122,4 +122,22 @@ public function __isset(string $key): bool { return isset($this->data[$key]) || in_array($key, ['id', 'roles', 'data'], strict: true); } + + + public function __serialize(): array + { + return [ + 'id' => $this->id, + 'roles' => $this->roles, + 'data' => $this->data, + ]; + } + + + public function __unserialize(array $data): void + { + $this->id = $data['id'] ?? $data["\00Nette\\Security\\Identity\00id"] ?? 0; + $this->roles = $data['roles'] ?? $data["\00Nette\\Security\\Identity\00roles"] ?? []; + $this->data = $data['data'] ?? $data["\00Nette\\Security\\Identity\00data"] ?? []; + } } From 29ed1fd1cb4c62f0a6de31076d1c53f61fd83c66 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 24 Sep 2021 14:12:52 +0200 Subject: [PATCH 52/53] User: deprecated magic properties (BC break) --- src/Security/User.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Security/User.php b/src/Security/User.php index f9055d28..21387ac8 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -17,11 +17,11 @@ * * @property-read bool $loggedIn * @property-read ?IIdentity $identity - * @property-read string|int|null $id - * @property-read list $roles - * @property-read ?int $logoutReason - * @property Authenticator $authenticator - * @property Authorizator $authorizator + * @property-deprecated string|int|null $id + * @property-deprecated list $roles + * @property-deprecated ?int $logoutReason + * @property-deprecated Authenticator $authenticator + * @property-deprecated Authorizator $authorizator */ class User { From b3dcf9fe8a7052efee78ebb8a718386eae9eb709 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 May 2026 04:00:44 +0200 Subject: [PATCH 53/53] IdentityHandler: make getGuestIdentity() a regular interface method (BC break) Promotes the optional @method to a required method, so User no longer needs method_exists() to call it. BC break: existing IdentityHandler implementations must now declare getGuestIdentity(). --- src/Security/IdentityHandler.php | 6 +++++- src/Security/User.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Security/IdentityHandler.php b/src/Security/IdentityHandler.php index ed9956fe..02523867 100644 --- a/src/Security/IdentityHandler.php +++ b/src/Security/IdentityHandler.php @@ -10,7 +10,6 @@ /** * Serializes and restores identity to/from persistent storage. - * @method ?IIdentity getGuestIdentity() */ interface IdentityHandler { @@ -23,4 +22,9 @@ function sleepIdentity(IIdentity $identity): IIdentity; * Called after identity is read from storage. Typically refreshes roles or validates the token. Returns null to force logout. */ function wakeupIdentity(IIdentity $identity): ?IIdentity; + + /** + * Returns the identity for an anonymous (not logged-in) user, or null if none. + */ + function getGuestIdentity(): ?IIdentity; } diff --git a/src/Security/User.php b/src/Security/User.php index 21387ac8..f9c9742c 100644 --- a/src/Security/User.php +++ b/src/Security/User.php @@ -178,7 +178,7 @@ private function resolveGuestIdentity(): ?IIdentity { if (!$this->guestIdentityResolved) { $this->guestIdentityResolved = true; - $this->guestIdentity = $this->authenticator instanceof IdentityHandler && method_exists($this->authenticator, 'getGuestIdentity') + $this->guestIdentity = $this->authenticator instanceof IdentityHandler ? $this->authenticator->getGuestIdentity() : null; }