diff --git a/main_test.go b/main_test.go index 5414310..b2978c7 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 7b53fd4..ec6221d 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 754f33d..c62950e 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