From f66582f44f3dc3a4705a5260f982043fde4aa634 Mon Sep 17 00:00:00 2001 From: Justin Sievenpiper Date: Fri, 17 Sep 2021 23:39:26 -0700 Subject: [PATCH 1/2] feat: add acl auth support for sentinels --- main_test.go | 48 ++++++++++++++++++++++++++++++++++++++- sentinel.go | 9 +++++++- sentinel_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/main_test.go b/main_test.go index 5414310e..b2978c75 100644 --- a/main_test.go +++ b/main_test.go @@ -40,15 +40,27 @@ const ( sentinelPort3 = "9128" ) +const ( + aclSentinelUsername = "sentinel-user" + aclSentinelPassword = "sentinel-pass" + aclSentinelName = "my_server" + aclServerPort = "10001" + aclSentinelPort1 = "10002" + aclSentinelPort2 = "10003" + aclSentinelPort3 = "10004" +) + var ( sentinelAddrs = []string{":" + sentinelPort1, ":" + sentinelPort2, ":" + sentinelPort3} + aclSentinelAddrs = []string {":" + aclSentinelPort1, ":" + aclSentinelPort2, ":" + aclSentinelPort3} processes map[string]*redisProcess - redisMain *redisProcess + redisMain, aclServer *redisProcess ringShard1, ringShard2, ringShard3 *redisProcess sentinelMaster, sentinelSlave1, sentinelSlave2 *redisProcess sentinel1, sentinel2, sentinel3 *redisProcess + aclSentinel1, aclSentinel2, aclSentinel3 *redisProcess ) var cluster = &clusterScenario{ @@ -101,6 +113,18 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(startCluster(ctx, cluster)).NotTo(HaveOccurred()) + + aclServer, err = startRedis(aclServerPort) + Expect(err).NotTo(HaveOccurred()) + + aclSentinel1, err = startSentinelWithAcl(aclSentinelPort1, aclSentinelName, aclServerPort) + Expect(err).NotTo(HaveOccurred()) + + aclSentinel2, err = startSentinelWithAcl(aclSentinelPort2, aclSentinelName, aclServerPort) + Expect(err).NotTo(HaveOccurred()) + + aclSentinel3, err = startSentinelWithAcl(aclSentinelPort3, aclSentinelName, aclServerPort) + Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { @@ -364,6 +388,28 @@ func startSentinel(port, masterName, masterPort string) (*redisProcess, error) { return p, nil } +func startSentinelWithAcl(port, masterName, masterPort string) (*redisProcess, error) { + process, err := startSentinel(port, masterName, masterPort) + if err != nil { + return nil, err + } + + for _, cmd := range []*redis.StatusCmd{ + redis.NewStatusCmd(ctx, "ACL", "SETUSER", aclSentinelUsername, "ON", ">" + aclSentinelPassword, "-@all", + "+auth", "+client|getname", "+client|id", "+client|setname", "+command", "+hello", "+ping", "+role", + "+sentinel|get-master-addr-by-name", "+sentinel|master", "+sentinel|myid", "+sentinel|replicas", + "+sentinel|sentinels"), + } { + process.Client.Process(ctx, cmd) + if err := cmd.Err(); err != nil { + process.Kill() + return nil, err + } + } + + return process, nil +} + //------------------------------------------------------------------------------ type badConnError string diff --git a/sentinel.go b/sentinel.go index 7b53fd4f..ec6221dc 100644 --- a/sentinel.go +++ b/sentinel.go @@ -23,7 +23,13 @@ type FailoverOptions struct { MasterName string // A seed list of host:port addresses of sentinel nodes. SentinelAddrs []string - // Sentinel password from "requirepass " (if enabled) in Sentinel configuration + + // If specified with SentinelPassword, enables ACL-based authentication (via + // AUTH ). + SentinelUsername string + // Sentinel password from "requirepass " (if enabled) in Sentinel + // configuration, or, if SentinelUsername is also supplied, used for ACL-based + // authentication. SentinelPassword string // Allows routing read-only commands to the closest master or slave node. @@ -109,6 +115,7 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { OnConnect: opt.OnConnect, DB: 0, + Username: opt.SentinelUsername, Password: opt.SentinelPassword, MaxRetries: opt.MaxRetries, diff --git a/sentinel_test.go b/sentinel_test.go index 754f33d7..c62950eb 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -212,3 +212,62 @@ var _ = Describe("NewFailoverClusterClient", func() { Expect(err).NotTo(HaveOccurred()) }) }) + +var _ = Describe("SentinelAclAuth", func() { + var client *redis.Client + var server *redis.Client + var sentinel *redis.SentinelClient + + BeforeEach(func() { + client = redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: aclSentinelName, + SentinelAddrs: aclSentinelAddrs, + MaxRetries: -1, + SentinelUsername: aclSentinelUsername, + SentinelPassword: aclSentinelPassword, + }) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + + sentinel = redis.NewSentinelClient(&redis.Options{ + Addr: aclSentinelAddrs[0], + MaxRetries: -1, + Username: aclSentinelUsername, + Password: aclSentinelPassword, + }) + + addr, err := sentinel.GetMasterAddrByName(ctx, aclSentinelName).Result() + Expect(err).NotTo(HaveOccurred()) + + server = redis.NewClient(&redis.Options{ + Addr: net.JoinHostPort(addr[0], addr[1]), + MaxRetries: -1, + }) + + // Wait until sentinels are picked up by each other. + Eventually(func() string { + return aclSentinel1.Info(ctx).Val() + }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + Eventually(func() string { + return aclSentinel2.Info(ctx).Val() + }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + Eventually(func() string { + return aclSentinel3.Info(ctx).Val() + }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + }) + + AfterEach(func() { + _ = client.Close() + _ = server.Close() + _ = sentinel.Close() + }) + + It("should still facilitate operations", func() { + err := client.Set(ctx, "wow", "acl-auth", 0).Err() + Expect(err).NotTo(HaveOccurred()) + + val, err := client.Get(ctx, "wow").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("acl-auth")) + }) +}) \ No newline at end of file From f74ce7de90eac3b1fa9be4092bacfd190851f033 Mon Sep 17 00:00:00 2001 From: Justin Sievenpiper Date: Sat, 18 Sep 2021 22:36:40 -0700 Subject: [PATCH 2/2] chore: swap to acl auth at the test-level --- main_test.go | 48 +----------------------------------------- sentinel_test.go | 54 ++++++++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 67 deletions(-) diff --git a/main_test.go b/main_test.go index b2978c75..5414310e 100644 --- a/main_test.go +++ b/main_test.go @@ -40,27 +40,15 @@ const ( sentinelPort3 = "9128" ) -const ( - aclSentinelUsername = "sentinel-user" - aclSentinelPassword = "sentinel-pass" - aclSentinelName = "my_server" - aclServerPort = "10001" - aclSentinelPort1 = "10002" - aclSentinelPort2 = "10003" - aclSentinelPort3 = "10004" -) - var ( sentinelAddrs = []string{":" + sentinelPort1, ":" + sentinelPort2, ":" + sentinelPort3} - aclSentinelAddrs = []string {":" + aclSentinelPort1, ":" + aclSentinelPort2, ":" + aclSentinelPort3} processes map[string]*redisProcess - redisMain, aclServer *redisProcess + redisMain *redisProcess ringShard1, ringShard2, ringShard3 *redisProcess sentinelMaster, sentinelSlave1, sentinelSlave2 *redisProcess sentinel1, sentinel2, sentinel3 *redisProcess - aclSentinel1, aclSentinel2, aclSentinel3 *redisProcess ) var cluster = &clusterScenario{ @@ -113,18 +101,6 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(startCluster(ctx, cluster)).NotTo(HaveOccurred()) - - aclServer, err = startRedis(aclServerPort) - Expect(err).NotTo(HaveOccurred()) - - aclSentinel1, err = startSentinelWithAcl(aclSentinelPort1, aclSentinelName, aclServerPort) - Expect(err).NotTo(HaveOccurred()) - - aclSentinel2, err = startSentinelWithAcl(aclSentinelPort2, aclSentinelName, aclServerPort) - Expect(err).NotTo(HaveOccurred()) - - aclSentinel3, err = startSentinelWithAcl(aclSentinelPort3, aclSentinelName, aclServerPort) - Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { @@ -388,28 +364,6 @@ func startSentinel(port, masterName, masterPort string) (*redisProcess, error) { return p, nil } -func startSentinelWithAcl(port, masterName, masterPort string) (*redisProcess, error) { - process, err := startSentinel(port, masterName, masterPort) - if err != nil { - return nil, err - } - - for _, cmd := range []*redis.StatusCmd{ - redis.NewStatusCmd(ctx, "ACL", "SETUSER", aclSentinelUsername, "ON", ">" + aclSentinelPassword, "-@all", - "+auth", "+client|getname", "+client|id", "+client|setname", "+command", "+hello", "+ping", "+role", - "+sentinel|get-master-addr-by-name", "+sentinel|master", "+sentinel|myid", "+sentinel|replicas", - "+sentinel|sentinels"), - } { - process.Client.Process(ctx, cmd) - if err := cmd.Err(); err != nil { - process.Kill() - return nil, err - } - } - - return process, nil -} - //------------------------------------------------------------------------------ type badConnError string diff --git a/sentinel_test.go b/sentinel_test.go index c62950eb..3d9b281a 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -214,14 +214,31 @@ var _ = Describe("NewFailoverClusterClient", func() { }) var _ = Describe("SentinelAclAuth", func() { + const ( + aclSentinelUsername = "sentinel-user" + aclSentinelPassword = "sentinel-pass" + ) + var client *redis.Client - var server *redis.Client var sentinel *redis.SentinelClient + var sentinels = func() []*redisProcess { + return []*redisProcess{ sentinel1, sentinel2, sentinel3 } + } BeforeEach(func() { + authCmd := redis.NewStatusCmd(ctx, "ACL", "SETUSER", aclSentinelUsername, "ON", + ">" + aclSentinelPassword, "-@all", "+auth", "+client|getname", "+client|id", "+client|setname", + "+command", "+hello", "+ping", "+role", "+sentinel|get-master-addr-by-name", "+sentinel|master", + "+sentinel|myid", "+sentinel|replicas", "+sentinel|sentinels") + + for _, process := range sentinels() { + err := process.Client.Process(ctx, authCmd) + Expect(err).NotTo(HaveOccurred()) + } + client = redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: aclSentinelName, - SentinelAddrs: aclSentinelAddrs, + MasterName: sentinelName, + SentinelAddrs: sentinelAddrs, MaxRetries: -1, SentinelUsername: aclSentinelUsername, SentinelPassword: aclSentinelPassword, @@ -230,35 +247,32 @@ var _ = Describe("SentinelAclAuth", func() { Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) sentinel = redis.NewSentinelClient(&redis.Options{ - Addr: aclSentinelAddrs[0], + Addr: sentinelAddrs[0], MaxRetries: -1, Username: aclSentinelUsername, Password: aclSentinelPassword, }) - addr, err := sentinel.GetMasterAddrByName(ctx, aclSentinelName).Result() + _, err := sentinel.GetMasterAddrByName(ctx, sentinelName).Result() Expect(err).NotTo(HaveOccurred()) - server = redis.NewClient(&redis.Options{ - Addr: net.JoinHostPort(addr[0], addr[1]), - MaxRetries: -1, - }) - // Wait until sentinels are picked up by each other. - Eventually(func() string { - return aclSentinel1.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) - Eventually(func() string { - return aclSentinel2.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) - Eventually(func() string { - return aclSentinel3.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + for _, process := range sentinels() { + Eventually(func() string { + return process.Info(ctx).Val() + }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + } }) AfterEach(func() { + unauthCommand := redis.NewStatusCmd(ctx, "ACL", "DELUSER", aclSentinelUsername) + + for _, process := range sentinels() { + err := process.Client.Process(ctx, unauthCommand) + Expect(err).NotTo(HaveOccurred()) + } + _ = client.Close() - _ = server.Close() _ = sentinel.Close() })