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