diff --git a/src/RedisOptions.php b/src/RedisOptions.php index 3ce6161..db48340 100644 --- a/src/RedisOptions.php +++ b/src/RedisOptions.php @@ -5,7 +5,6 @@ namespace Laminas\Cache\Storage\Adapter; use Laminas\Cache\Exception; -use Laminas\Cache\Storage\Adapter\RedisResourceManager; use function sprintf; use function strlen; @@ -261,4 +260,26 @@ public function getPassword() { return $this->getResourceManager()->getPassword($this->getResourceId()); } + + /** + * Set resource user + * + * @param string $user ACL User + * @return RedisOptions Provides a fluent interface + */ + public function setUser($user) + { + $this->getResourceManager()->setUser($this->getResourceId(), $user); + return $this; + } + + /** + * Get resource user + * + * @return string + */ + public function getUser() + { + return $this->getResourceManager()->getUser($this->getResourceId()); + } } diff --git a/src/RedisResourceManager.php b/src/RedisResourceManager.php index c89c705..25daaf1 100644 --- a/src/RedisResourceManager.php +++ b/src/RedisResourceManager.php @@ -123,6 +123,22 @@ public function getPassword($id) return $resource['password']; } + /** + * Get redis resource user + * + * @param string $id + * @return string + */ + public function getUser($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = &$this->resources[$id]; + return $resource['user']; + } + /** * Gets a redis resource * @@ -279,6 +295,36 @@ protected function extractPassword($resource, $serverUri) return $server['pass'] ?? null; } + /** + * Extract password to be used on connection + * + * @param mixed $resource + * @param mixed $serverUri + * @return string|null + */ + protected function extractUser($resource, $serverUri) + { + if (! empty($resource['user'])) { + return $resource['user']; + } + + if (! is_string($serverUri)) { + return null; + } + + // parse server from URI host{:?port} + $server = trim($serverUri); + + if (strpos($server, '/') === 0) { + return null; + } + + //non unix domain socket connection + $server = parse_url($server); + + return $server['user'] ?? null; + } + /** * Connects to redis server * @@ -315,7 +361,9 @@ protected function connect(array &$resource) } $resource['initialized'] = true; - if ($resource['password']) { + if ($resource['user'] && $resource['password']) { + $redis->auth([$resource['user'], $resource['password']]); + } elseif ($resource['password']) { $redis->auth($resource['password']); } $redis->select($resource['database']); @@ -339,6 +387,7 @@ public function setResource($id, $resource) 'persistent_id' => '', 'lib_options' => [], 'server' => [], + 'user' => '', 'password' => '', 'database' => 0, 'resource' => null, @@ -361,6 +410,7 @@ public function setResource($id, $resource) // #6495 note: order is important here, as `normalizeServer` applies destructive // transformations on $resource['server'] $resource['password'] = $this->extractPassword($resource, $resource['server']); + $resource['user'] = $this->extractUser($resource, $resource['server']); $this->normalizeServer($resource['server']); } else { @@ -624,12 +674,17 @@ public function setServer($id, $server) $resource = &$this->resources[$id]; $resource['password'] = $this->extractPassword($resource, $server); + $resource['user'] = $this->extractUser($resource, $server); + if ($resource['resource'] instanceof RedisResource) { $resourceParams = ['server' => $server]; if (! empty($resource['password'])) { $resourceParams['password'] = $resource['password']; } + if (! empty($resource['user'])) { + $resourceParams['user'] = $resource['user']; + } $this->setResource($id, $resourceParams); } else { @@ -644,7 +699,7 @@ public function setServer($id, $server) * * @param string $id * @param string $password - * @return RedisResource + * @return RedisResourceManager */ public function setPassword($id, $password) { @@ -713,4 +768,25 @@ private function getRedisInfo(RedisResource $redis): array return $info; } + + /** + * Set redis user + * + * @param string $id + * @param string $user + * @return RedisResourceManager + */ + public function setUser($id, $user) + { + if (! $this->hasResource($id)) { + return $this->setResource($id, [ + 'user' => $user, + ]); + } + + $resource = &$this->resources[$id]; + $resource['user'] = $user; + $resource['initialized'] = false; + return $this; + } } diff --git a/test/integration/Laminas/RedisTest.php b/test/integration/Laminas/RedisTest.php index a588022..e108e2c 100644 --- a/test/integration/Laminas/RedisTest.php +++ b/test/integration/Laminas/RedisTest.php @@ -328,6 +328,17 @@ public function testOptionsGetSetPassword(): void 'Password was set incorrectly using RedisOptions' ); } + + public function testOptionsGetSetUser(): void + { + $user = 'dummyuser'; + $this->options->setUser($user); + $this->assertEquals( + $user, + $this->options->getUser(), + 'User was set incorrectly using RedisOptions' + ); + } public function testTouchItem(): void { diff --git a/test/unit/RedisResourceManagerTest.php b/test/unit/RedisResourceManagerTest.php index 47187cc..f5c4841 100644 --- a/test/unit/RedisResourceManagerTest.php +++ b/test/unit/RedisResourceManagerTest.php @@ -49,6 +49,7 @@ public function testSetServerWithPasswordInUri(): void $this->assertEquals('testhost', $server['host']); $this->assertEquals(1234, $server['port']); $this->assertEquals('dummypass', $this->resourceManager->getPassword($dummyResId)); + $this->assertEquals('dummyuser', $this->resourceManager->getUser($dummyResId)); } /** @@ -71,6 +72,27 @@ public function testSetServerWithPasswordInParameters(): void $this->assertEquals('testhost', $server['host']); $this->assertEquals(1234, $server['port']); $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); + $this->assertEquals('dummyuser', $this->resourceManager->getUser($dummyResId2)); + } + + public function testSetServerWithPasswordInParametersAndNoUser(): void + { + $server = 'redis://testhost:1234'; + $dummyResId2 = '12345678901'; + $resource = [ + 'persistent_id' => 'my_connection_name', + 'server' => $server, + 'password' => 'abcd1234', + ]; + + $this->resourceManager->setResource($dummyResId2, $resource); + + $server = $this->resourceManager->getServer($dummyResId2); + + $this->assertEquals('testhost', $server['host']); + $this->assertEquals(1234, $server['port']); + $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); + $this->assertEquals('', $this->resourceManager->getUser($dummyResId2)); } /** @@ -287,6 +309,36 @@ public function testWillCatchAuthDuringConnectException(): void $this->resourceManager->getResource('default'); } + public function testWillCatchAuthDuringConnectExceptionWithUser(): void + { + $redis = $this->createMock(Redis::class); + + $redis + ->method('connect') + ->willReturn(true); + + $redis + ->expects(self::atLeastOnce()) + ->method('auth') + ->with(['dummyuser', 'secret']) + ->willThrowException(new RedisException('test')); + + $this->resourceManager->setResource( + 'default', + [ + 'resource' => $redis, + 'initialized' => false, + 'server' => 'somewhere:6379', + 'password' => 'secret', + 'user' => 'dummyuser', + ] + ); + + $this->expectException(RedisRuntimeException::class); + $this->expectExceptionMessage('test'); + $this->resourceManager->getResource('default'); + } + public function testWillCatchSelectDatabaseException(): void { $redis = $this->createMock(Redis::class);