diff --git a/packages/queue/docker-compose.yml b/packages/queue/docker-compose.yml
index 6e42310bd..c135c3f89 100644
--- a/packages/queue/docker-compose.yml
+++ b/packages/queue/docker-compose.yml
@@ -9,6 +9,22 @@ services:
timeout: 3s
retries: 15
+ redis-auth:
+ image: redis:alpine
+ command:
+ - sh
+ - -c
+ - |
+ printf 'requirepass secretpw\nuser worker on >workerpw ~* &* +@all\n' > /tmp/redis.conf
+ exec redis-server /tmp/redis.conf
+ ports:
+ - "16380:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "-a", "secretpw", "--no-auth-warning", "ping"]
+ interval: 2s
+ timeout: 3s
+ retries: 15
+
nats:
image: nats:2.11-alpine
command: ["-js", "-m", "8222"]
diff --git a/packages/queue/phpunit.xml b/packages/queue/phpunit.xml
index ec50e1bee..8f620047b 100644
--- a/packages/queue/phpunit.xml
+++ b/packages/queue/phpunit.xml
@@ -22,6 +22,7 @@
./tests/Queue/E2E/Adapter/NatsPoolTest.php
./tests/Queue/E2E/Adapter/KedaTest.php
./tests/Queue/E2E/Adapter/PoolTest.php
+ ./tests/Queue/E2E/Adapter/RedisAuthTest.php
./tests/Queue/E2E/Adapter/RedisConnectionRecoveryTest.php
./tests/Queue/E2E/Adapter/SwooleTest.php
./tests/Queue/E2E/Adapter/SwooleRestartTest.php
diff --git a/packages/queue/src/Queue/Connection/Redis.php b/packages/queue/src/Queue/Connection/Redis.php
index 68cbf4945..9ec680c5b 100644
--- a/packages/queue/src/Queue/Connection/Redis.php
+++ b/packages/queue/src/Queue/Connection/Redis.php
@@ -252,6 +252,11 @@ protected function getRedis(): \Redis
try {
$redis->connect($this->host, $this->port, $connectTimeout);
+ if ($this->password !== null && $this->password !== '') {
+ $hasUser = $this->user !== null && $this->user !== '';
+ $redis->auth($hasUser ? [$this->user, $this->password] : $this->password);
+ }
+
if ($this->readTimeout >= 0) {
$redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->readTimeout);
}
diff --git a/packages/queue/src/Queue/Connection/RedisCluster.php b/packages/queue/src/Queue/Connection/RedisCluster.php
index b539d663a..6ccc11ac5 100644
--- a/packages/queue/src/Queue/Connection/RedisCluster.php
+++ b/packages/queue/src/Queue/Connection/RedisCluster.php
@@ -11,7 +11,7 @@ class RedisCluster implements Connection
protected const int CONNECT_MAX_BACKOFF_MS = 3_000;
protected ?\RedisCluster $redis = null;
- public function __construct(protected array $seeds, protected float $connectTimeout = -1, protected float $readTimeout = -1) {}
+ public function __construct(protected array $seeds, protected float $connectTimeout = -1, protected float $readTimeout = -1, protected ?string $user = null, protected ?string $password = null) {}
public function rightPopLeftPushArray(string $queue, string $destination, int $timeout): array|false
{
@@ -201,7 +201,12 @@ protected function getRedis(): \RedisCluster
for ($attempt = 1; $attempt <= self::CONNECT_MAX_ATTEMPTS; $attempt++) {
try {
- $this->redis = new \RedisCluster(null, $this->seeds, $connectTimeout, $readTimeout);
+ $auth = match (true) {
+ $this->password === null || $this->password === '' => null,
+ $this->user !== null && $this->user !== '' => [$this->user, $this->password],
+ default => $this->password,
+ };
+ $this->redis = new \RedisCluster(null, $this->seeds, $connectTimeout, $readTimeout, false, $auth);
return $this->redis;
} catch (\RedisClusterException $e) {
if ($attempt === self::CONNECT_MAX_ATTEMPTS) {
diff --git a/packages/queue/tests/Queue/E2E/Adapter/RedisAuthTest.php b/packages/queue/tests/Queue/E2E/Adapter/RedisAuthTest.php
new file mode 100644
index 000000000..4df5768a7
--- /dev/null
+++ b/packages/queue/tests/Queue/E2E/Adapter/RedisAuthTest.php
@@ -0,0 +1,73 @@
+assertRoundTrip(new Redis(self::HOST, self::PORT, null, self::PASSWORD));
+ }
+
+ public function testUserAndPasswordAuthenticateAclUser(): void
+ {
+ $this->assertRoundTrip(new Redis(self::HOST, self::PORT, self::USER, self::USER_PASSWORD));
+ }
+
+ public function testMissingCredentialsAreRejected(): void
+ {
+ $this->assertRejected(new Redis(self::HOST, self::PORT));
+ }
+
+ public function testWrongPasswordIsRejected(): void
+ {
+ $this->assertRejected(new Redis(self::HOST, self::PORT, self::USER, 'not-the-password'));
+ }
+
+ /**
+ * A credentialed connection can write a job and read it back.
+ */
+ private function assertRoundTrip(Redis $connection): void
+ {
+ $key = 'auth-test-' . uniqid();
+
+ $this->assertTrue($connection->rightPush($key, 'payload'));
+ $this->assertSame('payload', $connection->rightPop($key, 1));
+ }
+
+ /**
+ * The observable contract of rejected credentials: the push throws, and the
+ * server holds nothing for that key when a trusted connection looks.
+ */
+ private function assertRejected(Redis $rejected): void
+ {
+ $key = 'auth-test-' . uniqid();
+ $thrown = null;
+
+ try {
+ $rejected->rightPush($key, 'payload');
+ } catch (\RedisException $e) {
+ $thrown = $e;
+ }
+
+ $trusted = new Redis(self::HOST, self::PORT, null, self::PASSWORD);
+
+ $this->assertInstanceOf(\RedisException::class, $thrown, 'Push with rejected credentials must throw');
+ $this->assertSame(0, $trusted->listSize($key), 'Rejected push must not reach the server');
+ }
+}