From 6da74d0e98ce6e904f604fab2f48514f06e1ce02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fengtan=20=E5=86=AF=E5=9D=A6?= <fengtan@users.noreply.github.com> Date: Thu, 15 Sep 2022 15:55:01 -0400 Subject: [PATCH] Add support for Redis-Sentinel (#1699) * Add support for Redis-Sentinel. * Add tests for redis sentinel store. * Documentation about Redis-Sentinel and password protection. * Fix markdown syntax brought up by github actions. * Add missing config parameter to unit tests. * Add missing return-type * Fix indentation * Fix incorrect configuration-methods Co-authored-by: Tim van Dijen <tvdijen@gmail.com> --- config-templates/config.php | 17 +++++++ docs/simplesamlphp-maintenance.md | 24 ++++++++++ src/SimpleSAML/Store/RedisStore.php | 45 +++++++++++++------ tests/src/SimpleSAML/Store/RedisStoreTest.php | 15 +++++++ .../src/SimpleSAML/Store/StoreFactoryTest.php | 1 + 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/config-templates/config.php b/config-templates/config.php index 34cedf5cd..b4938fc39 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -1221,4 +1221,21 @@ $config = [ * The prefix we should use on our Redis datastore. */ 'store.redis.prefix' => 'SimpleSAMLphp', + + /* + * The master group to use for Redis Sentinel. + */ + 'store.redis.mastergroup' => 'mymaster', + + /* + * The Redis Sentinel hosts. + * Example: + * array( + * 'tcp://[yoursentinel1]:[port]' + * 'tcp://[yoursentinel2]:[port]', + * 'tcp://[yoursentinel3]:[port] + * ) + */ + 'store.redis.sentinels' => [], + ]; diff --git a/docs/simplesamlphp-maintenance.md b/docs/simplesamlphp-maintenance.md index 7e849a914..8c209672f 100644 --- a/docs/simplesamlphp-maintenance.md +++ b/docs/simplesamlphp-maintenance.md @@ -179,6 +179,30 @@ For Redis instances that [require authentication](https://redis.io/commands/auth * If authentication is managed with the `requirepass` directive (legacy password protection): use the `store.redis.password` option * If authentication is managed with [ACL's](https://redis.io/docs/manual/security/acl/) (which are recommended as of Redis 6): use the `store.redis.password` and `store.redis.username` options +#### Redis-Sentinel + +If your Redis servers are controlled by [Redis-Sentinel](https://redis.io/docs/manual/sentinel/), then configure your sentinels by setting `store.redis.sentinels` to + +```php +[ + 'tcp://[yoursentinel1]:[port]', + 'tcp://[yoursentinel2]:[port]', + 'tcp://[yoursentinel3]:[port]', +] +``` + +If your sentinels are password-protected and use the same password as your Redis servers, then setting `store.redis.password` is enough. However if your sentinels use a different password than that of your Redis servers, then set the password of each sentinel: + +```php +[ + 'tcp://[yoursentinel1]:[port]?password=[password1]', + 'tcp://[yoursentinel2]:[port]?password=[password2]', + 'tcp://[yoursentinel3]:[port]?password=[password3]', +] +``` + +Configure your master group by setting `store.redis.mastergroup` (`mymaster` by default). + ## Metadata storage Several metadata storage backends are available by default, including `flatfile`, `serialize`, `mdq` and diff --git a/src/SimpleSAML/Store/RedisStore.php b/src/SimpleSAML/Store/RedisStore.php index 0bee896de..58e56ce28 100644 --- a/src/SimpleSAML/Store/RedisStore.php +++ b/src/SimpleSAML/Store/RedisStore.php @@ -42,19 +42,38 @@ class RedisStore implements StoreInterface $username = $config->getOptionalString('store.redis.username', null); $database = $config->getOptionalInteger('store.redis.database', 0); - $redis = new Client( - [ - 'scheme' => 'tcp', - 'host' => $host, - 'port' => $port, - 'database' => $database, - ] - + (!empty($password) ? ['password' => $password] : []) - + (!empty($username) ? ['username' => $username] : []), - [ - 'prefix' => $prefix, - ] - ); + $sentinels = $config->getOptionalArray('store.redis.sentinels', []); + + if (empty($sentinels)) { + $redis = new Client( + [ + 'scheme' => 'tcp', + 'host' => $host, + 'port' => $port, + 'database' => $database, + ] + + (!empty($username) ? ['username' => $username] : []) + + (!empty($password) ? ['password' => $password] : []), + [ + 'prefix' => $prefix, + ] + ); + } else { + $mastergroup = $config->getOptionalString('store.redis.mastergroup', 'mymaster'); + $redis = new Client( + $sentinels, + [ + 'replication' => 'sentinel', + 'service' => $mastergroup, + 'prefix' => $prefix, + 'parameters' => [ + 'database' => $database, + ] + + (!empty($username) ? ['username' => $username] : []) + + (!empty($password) ? ['password' => $password] : []), + ] + ); + } } $this->redis = $redis; diff --git a/tests/src/SimpleSAML/Store/RedisStoreTest.php b/tests/src/SimpleSAML/Store/RedisStoreTest.php index 0f2336e58..74b559c79 100644 --- a/tests/src/SimpleSAML/Store/RedisStoreTest.php +++ b/tests/src/SimpleSAML/Store/RedisStoreTest.php @@ -143,6 +143,21 @@ class RedisStoreTest extends TestCase $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** + * @covers \SimpleSAML\Store::getInstance + * @covers \SimpleSAML\Store\Redis::__construct + * @test + */ + public function testRedisSentinelInstance(): void + { + $config = Configuration::loadFromArray(array( + 'store.type' => 'redis', + 'store.redis.prefix' => 'phpunit_', + 'store.redis.mastergroup' => 'phpunit_mastergroup', + 'store.redis.sentinels' => array('tcp://sentinel1', 'tcp://sentinel2', 'tcp://sentinel3'), + ), '[ARRAY]', 'simplesaml'); + $this->assertInstanceOf(Store\RedisStore::class, $this->store); + } /** * @test diff --git a/tests/src/SimpleSAML/Store/StoreFactoryTest.php b/tests/src/SimpleSAML/Store/StoreFactoryTest.php index 27a421916..b0beeed64 100644 --- a/tests/src/SimpleSAML/Store/StoreFactoryTest.php +++ b/tests/src/SimpleSAML/Store/StoreFactoryTest.php @@ -86,6 +86,7 @@ class StoreFactoryTest extends TestCase Configuration::loadFromArray([ 'store.type' => 'redis', 'store.redis.prefix' => 'phpunit_', + 'store.redis.sentinels' => [], ], '[ARRAY]', 'simplesaml'); $config = Configuration::getInstance(); -- GitLab