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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 :)
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.
33 changes: 32 additions & 1 deletion src/Business/Redis/RedisFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand All @@ -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.');
}
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Configuration/AuthorizationConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}

/**
Expand Down
123 changes: 122 additions & 1 deletion tests/Unit/Business/Redis/RedisFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}
}
16 changes: 14 additions & 2 deletions tests/Unit/RedisPluginConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function testConfiguration(): void
$this->assertTrue($ssl->verify());
}

public function testDefaultConfiguration()
public function testDefaultConfiguration(): void
{
$clientCfg = $this->cfg->getClientConfiguration('default');

Expand All @@ -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());
}
}
Loading