diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b5b0dff..1d8d064 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 5ee52c1..686f085 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 a1da84d..594c396 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 570486c..84842ec 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,12 @@ } ], "require": { - "php": "8.1 - 8.5", + "php": "8.3 - 8.5", "nette/utils": "^4.0" }, "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/"], @@ -45,7 +45,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "config": { diff --git a/readme.md b/readme.md index a9e93b8..9853eab 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) diff --git a/src/Bridges/SecurityDI/SecurityExtension.php b/src/Bridges/SecurityDI/SecurityExtension.php index 3719288..8cbcebd 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/Bridges/SecurityHttp/CookieStorage.php b/src/Bridges/SecurityHttp/CookieStorage.php index ab9a2d1..03b5a2e 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, ); } diff --git a/src/Security/Authenticator.php b/src/Security/Authenticator.php index 2b47c70..42c0cab 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 443f37b..0000000 --- a/src/Security/IAuthenticator.php +++ /dev/null @@ -1,35 +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 = $data instanceof \Traversable - ? iterator_to_array($data) - : (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/IdentityHandler.php b/src/Security/IdentityHandler.php index ed9956f..0252386 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/SimpleIdentity.php b/src/Security/SimpleIdentity.php index c987f6f..ac749cc 100644 --- a/src/Security/SimpleIdentity.php +++ b/src/Security/SimpleIdentity.php @@ -7,10 +7,137 @@ 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); + } + + + 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"] ?? []; + } } diff --git a/src/Security/User.php b/src/Security/User.php index e5222f3..f9c9742 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 IAuthenticator $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 { @@ -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 @@ -180,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; } @@ -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; } diff --git a/src/compatibility-intf.php b/src/compatibility-intf.php new file mode 100644 index 0000000..fc11a81 --- /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()); -});