Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/queue/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions packages/queue/phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<file>./tests/Queue/E2E/Adapter/NatsPoolTest.php</file>
<file>./tests/Queue/E2E/Adapter/KedaTest.php</file>
<file>./tests/Queue/E2E/Adapter/PoolTest.php</file>
<file>./tests/Queue/E2E/Adapter/RedisAuthTest.php</file>
<file>./tests/Queue/E2E/Adapter/RedisConnectionRecoveryTest.php</file>
<file>./tests/Queue/E2E/Adapter/SwooleTest.php</file>
<file>./tests/Queue/E2E/Adapter/SwooleRestartTest.php</file>
Expand Down
5 changes: 5 additions & 0 deletions packages/queue/src/Queue/Connection/Redis.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 7 additions & 2 deletions packages/queue/src/Queue/Connection/RedisCluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Comment thread
ChiragAgg5k marked this conversation as resolved.
return $this->redis;
} catch (\RedisClusterException $e) {
if ($attempt === self::CONNECT_MAX_ATTEMPTS) {
Expand Down
73 changes: 73 additions & 0 deletions packages/queue/tests/Queue/E2E/Adapter/RedisAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Tests\E2E\Adapter;

use PHPUnit\Framework\TestCase;
use Utopia\Queue\Connection\Redis;

/**
* Runs against the `redis-auth` compose service: `requirepass secretpw` for the
* default user and an ACL user `worker` with password `workerpw`.
*/
final class RedisAuthTest extends TestCase
{
private const string HOST = '127.0.0.1';
private const int PORT = 16380;
private const string PASSWORD = 'secretpw';
private const string USER = 'worker';
private const string USER_PASSWORD = 'workerpw';

public function testPasswordAuthenticatesDefaultUser(): void
{
$this->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');
}
}
Loading