From c1ba76ea5a1e5636a4896bcb4ec8182368adf9c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Steffen=20J=C3=B8rgensen?= <steffen.jorgensen@lyse.no>
Date: Wed, 15 May 2024 13:48:17 +0200
Subject: [PATCH] (#527) Add masteruser parameter

Enable setting the masteruser parameter which was introduced in Redis 6+ to be able to connect using the new ACL rules.
---
 README.md                           | 10 ++++++++++
 REFERENCE.md                        | 31 +++++++++++++++++++++++++++--
 manifests/init.pp                   |  5 ++++-
 manifests/instance.pp               |  6 +++++-
 manifests/sentinel.pp               |  9 +++++++++
 spec/classes/redis_sentinel_spec.rb |  4 ++++
 spec/classes/redis_spec.rb          | 14 +++++++++++++
 templates/redis-sentinel.conf.erb   |  3 +++
 templates/redis.conf.epp            | 13 ++++++++++++
 9 files changed, 91 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 32e6fa48..818643d5 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,16 @@ class { 'redis':
 }
 ```
 
+With ACL authentication
+
+```puppet
+class { 'redis':
+  bind       => '10.0.1.1',
+  masterauth => 'secret',
+  masteruser => 'username',
+}
+```
+
 ### Slave node
 
 ```puppet
diff --git a/REFERENCE.md b/REFERENCE.md
index c2b5a708..ba1068c5 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -122,6 +122,7 @@ The following parameters are available in the `redis` class:
 * [`manage_package`](#-redis--manage_package)
 * [`managed_by_cluster_manager`](#-redis--managed_by_cluster_manager)
 * [`masterauth`](#-redis--masterauth)
+* [`masteruser`](#-redis--masteruser)
 * [`maxclients`](#-redis--maxclients)
 * [`maxmemory`](#-redis--maxmemory)
 * [`maxmemory_policy`](#-redis--maxmemory_policy)
@@ -532,7 +533,15 @@ Default value: `false`
 
 Data type: `Optional[Variant[String[1], Sensitive[String[1]], Deferred]]`
 
-If the master is password protected (using the "requirepass" configuration
+If the master is password protected (using the "requirepass" configuration)
+
+Default value: `undef`
+
+##### <a name="-redis--masteruser"></a>`masteruser`
+
+Data type: `Optional[Variant[String[1], Sensitive[String[1]], Deferred]]`
+
+If the master is password protected and a user is defined (using the "user" configuration)
 
 Default value: `undef`
 
@@ -1514,6 +1523,7 @@ class {'redis::sentinel':
 The following parameters are available in the `redis::sentinel` class:
 
 * [`auth_pass`](#-redis--sentinel--auth_pass)
+* [`auth_user`](#-redis--sentinel--auth_user)
 * [`config_file`](#-redis--sentinel--config_file)
 * [`config_file_orig`](#-redis--sentinel--config_file_orig)
 * [`config_file_mode`](#-redis--sentinel--config_file_mode)
@@ -1563,6 +1573,14 @@ The password to use to authenticate with the master and slaves.
 
 Default value: `undef`
 
+##### <a name="-redis--sentinel--auth_user"></a>`auth_user`
+
+Data type: `Optional[Variant[String[1], Sensitive[String[1]]]]`
+
+The username to use to authenticate with the master and slaves.
+
+Default value: `undef`
+
 ##### <a name="-redis--sentinel--config_file"></a>`config_file`
 
 Data type: `Stdlib::Absolutepath`
@@ -1953,6 +1971,7 @@ The following parameters are available in the `redis::instance` defined type:
 * [`managed_by_cluster_manager`](#-redis--instance--managed_by_cluster_manager)
 * [`manage_service_file`](#-redis--instance--manage_service_file)
 * [`masterauth`](#-redis--instance--masterauth)
+* [`masteruser`](#-redis--instance--masteruser)
 * [`maxclients`](#-redis--instance--maxclients)
 * [`maxmemory`](#-redis--instance--maxmemory)
 * [`maxmemory_policy`](#-redis--instance--maxmemory_policy)
@@ -2305,7 +2324,15 @@ Default value: `true`
 
 Data type: `Optional[Variant[String[1], Sensitive[String[1]], Deferred]]`
 
-If the master is password protected (using the "requirepass" configuration
+If the master is password protected (using the "requirepass" configuration)
+
+Default value: `$redis::masterauth`
+
+##### <a name="-redis--instance--masteruser"></a>`masteruser`
+
+Data type: `Optional[Variant[String[1], Sensitive[String[1]], Deferred]]`
+
+If the master is password protected and a user is defined (using the "user" configuration)
 
 Default value: `$redis::masterauth`
 
diff --git a/manifests/init.pp b/manifests/init.pp
index e48ed67c..f19f8d79 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -95,7 +95,9 @@
 # @param managed_by_cluster_manager
 #   Choose if redis will be managed by a cluster manager such as pacemaker or rgmanager
 # @param masterauth
-#   If the master is password protected (using the "requirepass" configuration
+#   If the master is password protected (using the "requirepass" configuration)
+# @param masteruser
+#   If the master is password protected and a user is defined (using the "user" configuration)
 # @param maxclients
 #   Set the max number of connected clients at the same time.
 # @param maxmemory
@@ -392,6 +394,7 @@
   Boolean $manage_package                                        = true,
   Boolean $manage_repo                                           = false,
   Optional[Variant[String[1], Sensitive[String[1]], Deferred]] $masterauth = undef,
+  Optional[Variant[String[1], Sensitive[String[1]], Deferred]] $masteruser = undef,
   Integer[1] $maxclients                                         = 10000,
   $maxmemory                                                     = undef,
   Optional[Redis::MemoryPolicy] $maxmemory_policy                = undef,
diff --git a/manifests/instance.pp b/manifests/instance.pp
index 3b18cfca..b8256b87 100644
--- a/manifests/instance.pp
+++ b/manifests/instance.pp
@@ -74,7 +74,9 @@
 # @param manage_service_file
 #   Determine if the systemd service file should be managed
 # @param masterauth
-#   If the master is password protected (using the "requirepass" configuration
+#   If the master is password protected (using the "requirepass" configuration)
+# @param masteruser
+#   If the master is password protected and a user is defined (using the "user" configuration)
 # @param maxclients
 #   Set the max number of connected clients at the same time.
 # @param maxmemory
@@ -325,6 +327,7 @@
   Stdlib::Filemode $log_dir_mode                                 = $redis::log_dir_mode,
   Redis::LogLevel $log_level                                     = $redis::log_level,
   Optional[Variant[String[1], Sensitive[String[1]], Deferred]] $masterauth = $redis::masterauth,
+  Optional[Variant[String[1], Sensitive[String[1]], Deferred]] $masteruser = $redis::masterauth,
   Integer[1] $maxclients                                         = $redis::maxclients,
   Optional[Variant[Integer, String]] $maxmemory                  = $redis::maxmemory,
   Optional[Redis::MemoryPolicy] $maxmemory_policy                = $redis::maxmemory_policy,
@@ -526,6 +529,7 @@
     slaveof                       => $slaveof,
     replicaof                     => $replicaof,
     masterauth                    => $masterauth,
+    masteruser                    => $masteruser,
     slave_serve_stale_data        => $slave_serve_stale_data,
     slave_read_only               => $slave_read_only,
     repl_announce_ip              => $repl_announce_ip,
diff --git a/manifests/sentinel.pp b/manifests/sentinel.pp
index 2813802b..ea956e3b 100644
--- a/manifests/sentinel.pp
+++ b/manifests/sentinel.pp
@@ -3,6 +3,9 @@
 # @param auth_pass
 #   The password to use to authenticate with the master and slaves.
 #
+# @param auth_user
+#   The username to use to authenticate with the master and slaves.
+#
 # @param config_file
 #   The location and name of the sentinel config file.
 #
@@ -147,6 +150,7 @@
 #
 class redis::sentinel (
   Optional[Variant[String[1], Sensitive[String[1]]]] $auth_pass = undef,
+  Optional[Variant[String[1], Sensitive[String[1]]]] $auth_user = undef,
   Stdlib::Absolutepath $config_file = $redis::params::sentinel_config_file,
   Stdlib::Absolutepath $config_file_orig = $redis::params::sentinel_config_file_orig,
   Stdlib::Filemode $config_file_mode = '0644',
@@ -193,6 +197,11 @@
   } else {
     $auth_pass
   }
+  $auth_user_unsensitive = if $auth_user =~ Sensitive {
+    $auth_user.unwrap
+  } else {
+    $auth_user
+  }
 
   contain 'redis'
 
diff --git a/spec/classes/redis_sentinel_spec.rb b/spec/classes/redis_sentinel_spec.rb
index 945d8561..9b450024 100644
--- a/spec/classes/redis_sentinel_spec.rb
+++ b/spec/classes/redis_sentinel_spec.rb
@@ -110,6 +110,7 @@ class { 'redis':
           {
             sentinel_tls_port: 26_380,
             auth_pass: 'password',
+            auth_user: 'username',
             sentinel_bind: '192.0.2.10',
             protected_mode: false,
             master_name: 'cow',
@@ -151,6 +152,7 @@ class { 'redis':
             sentinel parallel-syncs cow 1
             sentinel failover-timeout cow 28000
             sentinel auth-pass cow password
+            sentinel auth-user cow username
             sentinel notification-script cow /path/to/bar.sh
             sentinel client-reconfig-script cow /path/to/foo.sh
 
@@ -177,6 +179,7 @@ class { 'redis':
         let(:params) do
           {
             auth_pass: 'password',
+            auth_user: 'username',
             sentinel_bind: ['192.0.2.10', '192.168.1.1'],
             master_name: 'cow',
             down_after: 6000,
@@ -203,6 +206,7 @@ class { 'redis':
             sentinel parallel-syncs cow 1
             sentinel failover-timeout cow 28000
             sentinel auth-pass cow password
+            sentinel auth-user cow username
             sentinel notification-script cow /path/to/bar.sh
             sentinel client-reconfig-script cow /path/to/foo.sh
 
diff --git a/spec/classes/redis_spec.rb b/spec/classes/redis_spec.rb
index b0cae74a..4344f423 100644
--- a/spec/classes/redis_spec.rb
+++ b/spec/classes/redis_spec.rb
@@ -523,6 +523,20 @@ class { 'redis':
         }
       end
 
+      describe 'with parameter masteruser' do
+        let(:params) do
+          {
+            masteruser: '_VALUE_'
+          }
+        end
+
+        it {
+          is_expected.to contain_file(config_file_orig).with(
+            'content' => %r{masteruser.*'_VALUE_'}
+          )
+        }
+      end
+
       describe 'with parameter maxclients' do
         let(:params) do
           {
diff --git a/templates/redis-sentinel.conf.erb b/templates/redis-sentinel.conf.erb
index 283743c1..ed6c9a20 100644
--- a/templates/redis-sentinel.conf.erb
+++ b/templates/redis-sentinel.conf.erb
@@ -27,6 +27,9 @@ sentinel failover-timeout <%= @master_name %> <%= @failover_timeout %>
 <% if @auth_pass_unsensitive -%>
 sentinel auth-pass <%= @master_name %> <%= @auth_pass_unsensitive %>
 <% end -%>
+<% if @auth_user_unsensitive -%>
+sentinel auth-user <%= @master_name %> <%= @auth_user_unsensitive %>
+<% end -%>
 <% if @notification_script -%>
 sentinel notification-script <%= @master_name %> <%= @notification_script %>
 <% end -%>
diff --git a/templates/redis.conf.epp b/templates/redis.conf.epp
index a14f7060..0186b151 100644
--- a/templates/redis.conf.epp
+++ b/templates/redis.conf.epp
@@ -23,6 +23,7 @@
   Optional[String[1]]                                $slaveof,
   Optional[String[1]]                                $replicaof,
   Optional[Variant[String[1], Sensitive[String[1]]]] $masterauth,
+  Optional[Variant[String[1], Sensitive[String[1]]]] $masteruser,
   Boolean                                            $slave_serve_stale_data,
   Boolean                                            $slave_read_only,
   Optional[Stdlib::Host]                             $repl_announce_ip,
@@ -411,6 +412,18 @@ dir <%= $workdir %>
 # masterauth <master-password>
 <% if $masterauth { -%>masterauth <%= $masterauth %><% } -%>
 
+# However this is not enough if you are using Redis ACLs (for Redis version
+# 6 or greater), and the default user is not capable of running the PSYNC
+# command and/or other commands needed for replication. In this case it's
+# better to configure a special user to use with replication, and specify the
+# username configuration as such:
+#
+# masteruser <master-username>
+<% if $masteruser { -%>masteruser <%= $masteruser %><% } -%>
+
+# When masteruser is specified, the replica will authenticate against its
+# master using the new AUTH form: AUTH <username> <password>.
+
 # When a slave loses the connection with the master, or when the replication
 # is still in progress, the slave can act in two different ways:
 #