From 5979789e55074da3a31ef1ddd94425559245e937 Mon Sep 17 00:00:00 2001 From: Stanislau Komar Date: Mon, 12 Oct 2026 19:31:06 +0400 Subject: [PATCH] v1.6.1 - Fix AUTH --- README.md | 22 +++- src/Business/Redis/RedisFactory.php | 33 ++++- .../AuthorizationConfiguration.php | 4 +- .../Unit/Business/Redis/RedisFactoryTest.php | 123 +++++++++++++++++- tests/Unit/RedisPluginConfigurationTest.php | 16 ++- 5 files changed, 192 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e6f23de..e658290 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ # Micro Framework -Documentation is available [here](https://micro-php.net/docs). If not, we will be grateful if you can become its author :) \ No newline at end of file +Documentation is available [here](https://micro-php.net/docs). If not, we will be grateful if you can become its author :) + +## Redis authentication + +Configure credentials per client (replace `DEFAULT` with the uppercase client name): + +```dotenv +REDIS_DEFAULT_AUTH_USERNAME=default +REDIS_DEFAULT_AUTH_PASSWORD=your-redis-password +``` + +The username defaults to Redis's `default` user. Redis has no built-in default +password: use the password configured on the server with `requirepass` or ACLs. +When only a password is configured, the plugin uses password-only `AUTH`, which +also supports Redis versions before 6. Set a username to authenticate as a named +ACL user on Redis 6 or later. + +Leave the password unset for Redis without authentication. An explicitly configured +empty password is sent as an empty password. Authentication runs after connecting +over TCP or a Unix socket, including persistent connections. Invalid credentials +cause client creation to fail. diff --git a/src/Business/Redis/RedisFactory.php b/src/Business/Redis/RedisFactory.php index 1ee01ae..efb8cdc 100644 --- a/src/Business/Redis/RedisFactory.php +++ b/src/Business/Redis/RedisFactory.php @@ -13,6 +13,8 @@ namespace Micro\Plugin\Redis\Business\Redis; +use Micro\Plugin\Redis\Configuration\AuthorizationConfiguration; +use Micro\Plugin\Redis\Configuration\AuthorizationConfigurationInterface; use Micro\Plugin\Redis\Configuration\ClientOptionsConfigurationInterface; use Micro\Plugin\Redis\Configuration\RedisClientConfigurationInterface; @@ -34,6 +36,16 @@ public function create(RedisClientConfigurationInterface $clientConfiguration): * @throws \RedisException */ protected function initialize(\Redis $redis, RedisClientConfigurationInterface $configuration): void + { + $this->connect($redis, $configuration); + $this->authenticate($redis, $configuration->authorizationConfiguration()); + $this->setOptions($redis, $configuration->clientOptionsConfiguration()); + } + + /** + * @throws \RedisException + */ + protected function connect(\Redis $redis, RedisClientConfigurationInterface $configuration): void { $connectionMethod = $this->getConnectionMethod($configuration); @@ -52,8 +64,27 @@ protected function initialize(\Redis $redis, RedisClientConfigurationInterface $ $configuration->retryInterval(), $configuration->readTimeout(), ); + } - $this->setOptions($redis, $configuration->clientOptionsConfiguration()); + /** + * @throws \RedisException + */ + protected function authenticate(\Redis $redis, AuthorizationConfigurationInterface $configuration): void + { + $password = $configuration->password(); + if (null === $password) { + return; + } + + $username = $configuration->username(); + // Password-only AUTH uses Redis's default user and also supports Redis < 6. + $credentials = null === $username || '' === $username || AuthorizationConfiguration::USERNAME_DEFAULT === $username + ? $password + : [$username, $password]; + + if (!$redis->auth($credentials)) { + throw new \RedisException('Redis authentication failed.'); + } } /** diff --git a/src/Configuration/AuthorizationConfiguration.php b/src/Configuration/AuthorizationConfiguration.php index fa05169..5c9981f 100644 --- a/src/Configuration/AuthorizationConfiguration.php +++ b/src/Configuration/AuthorizationConfiguration.php @@ -17,6 +17,8 @@ class AuthorizationConfiguration extends PluginRoutingKeyConfiguration implements AuthorizationConfigurationInterface { + public const USERNAME_DEFAULT = 'default'; + protected const CFG_USERNAME = 'REDIS_%s_AUTH_USERNAME'; protected const CFG_PASSWORD = 'REDIS_%s_AUTH_PASSWORD'; @@ -25,7 +27,7 @@ class AuthorizationConfiguration extends PluginRoutingKeyConfiguration implement */ public function username(): ?string { - return $this->get(self::CFG_USERNAME); + return $this->get(self::CFG_USERNAME, self::USERNAME_DEFAULT); } /** diff --git a/tests/Unit/Business/Redis/RedisFactoryTest.php b/tests/Unit/Business/Redis/RedisFactoryTest.php index 1505b2f..ab5be2f 100644 --- a/tests/Unit/Business/Redis/RedisFactoryTest.php +++ b/tests/Unit/Business/Redis/RedisFactoryTest.php @@ -15,12 +15,121 @@ use Micro\Framework\Kernel\Configuration\DefaultApplicationConfiguration; use Micro\Plugin\Redis\Business\Redis\RedisFactory; +use Micro\Plugin\Redis\Configuration\AuthorizationConfigurationInterface; +use Micro\Plugin\Redis\Configuration\ClientOptionsConfiguration; use Micro\Plugin\Redis\Configuration\RedisClientConfiguration; +use Micro\Plugin\Redis\Configuration\RedisClientConfigurationInterface; use PHPUnit\Framework\TestCase; class RedisFactoryTest extends TestCase { - public function testCreate() + /** + * @dataProvider provideConnections + */ + public function testAuthentication(string $connectionType, bool $persistent, array $credentials, $expectedAuth): void + { + $configuration = new RedisClientConfiguration(new DefaultApplicationConfiguration(array_merge([ + 'REDIS_TEST_CONNECTION_TYPE' => $connectionType, + 'REDIS_TEST_CONNECTION_REUSE' => $persistent, + 'REDIS_TEST_HOST' => 'unix' === $connectionType ? '/tmp/redis.sock' : 'localhost', + 'REDIS_TEST_OPT_PREFIX' => 'test:', + ], $credentials)), 'test'); + + $redis = $this->createMock(\Redis::class); + $method = $persistent ? 'pconnect' : 'connect'; + $redis->expects($this->once())->method($method)->willReturn(true); + $redis->expects($this->never())->method($persistent ? 'connect' : 'pconnect'); + $redis->expects($this->exactly(null === $expectedAuth ? 0 : 1)) + ->method('auth')->with($expectedAuth)->willReturn(true); + $redis->expects($this->exactly(2))->method('setOption')->withConsecutive( + [\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE], + [\Redis::OPT_PREFIX, 'test:'], + )->willReturn(true); + + $this->initialize($redis, $configuration); + } + + public function provideConnections(): iterable + { + $credentials = [ + 'no auth' => [[], null], + 'password only' => [['REDIS_TEST_AUTH_PASSWORD' => 'secret'], 'secret'], + 'default user' => [[ + 'REDIS_TEST_AUTH_USERNAME' => 'default', + 'REDIS_TEST_AUTH_PASSWORD' => 'secret', + ], 'secret'], + 'ACL user' => [[ + 'REDIS_TEST_AUTH_USERNAME' => 'app', + 'REDIS_TEST_AUTH_PASSWORD' => 'secret', + ], ['app', 'secret']], + 'empty username' => [[ + 'REDIS_TEST_AUTH_USERNAME' => '', + 'REDIS_TEST_AUTH_PASSWORD' => 'secret', + ], 'secret'], + 'zero password' => [['REDIS_TEST_AUTH_PASSWORD' => '0'], '0'], + 'empty password' => [['REDIS_TEST_AUTH_PASSWORD' => ''], ''], + ]; + + foreach (['network', 'unix'] as $type) { + foreach ([false, true] as $persistent) { + foreach ($credentials as $name => [$config, $auth]) { + yield \sprintf('%s %s %s', $type, $persistent ? 'persistent' : 'regular', $name) => [ + $type, $persistent, $config, $auth, + ]; + } + } + } + } + + public function testNullUsernameUsesPasswordOnlyAuthentication(): void + { + $auth = $this->createMock(AuthorizationConfigurationInterface::class); + $auth->method('username')->willReturn(null); + $auth->method('password')->willReturn('secret'); + + $configuration = $this->createMock(RedisClientConfigurationInterface::class); + $configuration->method('authorizationConfiguration')->willReturn($auth); + $configuration->method('clientOptionsConfiguration')->willReturn( + new ClientOptionsConfiguration(new DefaultApplicationConfiguration([]), 'test'), + ); + + $redis = $this->createMock(\Redis::class); + $redis->expects($this->once())->method('auth')->with('secret')->willReturn(true); + + $this->initialize($redis, $configuration); + } + + public function testFailedAuthenticationRejectsClient(): void + { + $configuration = new RedisClientConfiguration(new DefaultApplicationConfiguration([ + 'REDIS_TEST_AUTH_PASSWORD' => 'wrong-password', + ]), 'test'); + $redis = $this->createMock(\Redis::class); + $redis->expects($this->once())->method('auth')->willReturn(false); + $redis->expects($this->never())->method('setOption'); + + $this->expectException(\RedisException::class); + $this->expectExceptionMessage('Redis authentication failed.'); + + $this->initialize($redis, $configuration); + } + + public function testAuthenticationExceptionRejectsClient(): void + { + $configuration = new RedisClientConfiguration(new DefaultApplicationConfiguration([ + 'REDIS_TEST_AUTH_PASSWORD' => 'wrong-password', + ]), 'test'); + $redis = $this->createMock(\Redis::class); + $redis->expects($this->once())->method('auth')->willThrowException(new \RedisException('WRONGPASS')); + $redis->expects($this->never())->method('setOption'); + + $this->expectException(\RedisException::class); + $this->expectExceptionMessage('WRONGPASS'); + + $this->initialize($redis, $configuration); + } + + public function testCreate(): void { $this->expectException(\RedisException::class); @@ -30,4 +139,16 @@ public function testCreate() $factory = new RedisFactory(); $factory->create($cfg); } + + private function initialize(\Redis $redis, RedisClientConfigurationInterface $configuration): void + { + $factory = new class extends RedisFactory { + public function initialize(\Redis $redis, RedisClientConfigurationInterface $configuration): void + { + parent::initialize($redis, $configuration); + } + }; + + $factory->initialize($redis, $configuration); + } } diff --git a/tests/Unit/RedisPluginConfigurationTest.php b/tests/Unit/RedisPluginConfigurationTest.php index 8707640..c247099 100644 --- a/tests/Unit/RedisPluginConfigurationTest.php +++ b/tests/Unit/RedisPluginConfigurationTest.php @@ -85,7 +85,7 @@ public function testConfiguration(): void $this->assertTrue($ssl->verify()); } - public function testDefaultConfiguration() + public function testDefaultConfiguration(): void { $clientCfg = $this->cfg->getClientConfiguration('default'); @@ -105,10 +105,22 @@ public function testDefaultConfiguration() $auth = $clientCfg->authorizationConfiguration(); $this->assertNull($auth->password()); - $this->assertNull($auth->username()); + $this->assertSame('default', $auth->username()); $ssl = $clientCfg->sslConfiguration(); $this->assertFalse($ssl->enabled()); $this->assertFalse($ssl->verify()); } + + public function testPasswordOnlyAuthorizationUsesDefaultUser(): void + { + $configuration = new RedisPluginConfiguration(new DefaultApplicationConfiguration([ + 'REDIS_TEST_AUTH_PASSWORD' => 'password', + ])); + + $auth = $configuration->getClientConfiguration('test')->authorizationConfiguration(); + + $this->assertSame('default', $auth->username()); + $this->assertSame('password', $auth->password()); + } }