diff --git a/cluster.go b/cluster.go index 2cf6e01b..f02fcdfc 100644 --- a/cluster.go +++ b/cluster.go @@ -57,6 +57,7 @@ type ClusterOptions struct { OnConnect func(*Conn) error + Username string Password string MaxRetries int @@ -130,6 +131,7 @@ func (opt *ClusterOptions) clientOptions() *Options { MaxRetries: opt.MaxRetries, MinRetryBackoff: opt.MinRetryBackoff, MaxRetryBackoff: opt.MaxRetryBackoff, + Username: opt.Username, Password: opt.Password, readOnly: opt.ReadOnly, diff --git a/commands.go b/commands.go index d4447c4d..da5ceda1 100644 --- a/commands.go +++ b/commands.go @@ -302,6 +302,7 @@ type Cmdable interface { type StatefulCmdable interface { Cmdable Auth(password string) *StatusCmd + AuthACL(username, password string) *StatusCmd Select(index int) *StatusCmd SwapDB(index1, index2 int) *StatusCmd ClientSetName(name string) *BoolCmd @@ -324,6 +325,15 @@ func (c statefulCmdable) Auth(password string) *StatusCmd { return cmd } +// Perform an AUTH command, using the given user and pass. +// Should be used to authenticate the current connection with one of the connections defined in the ACL list +// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. +func (c statefulCmdable) AuthACL(username, password string) *StatusCmd { + cmd := NewStatusCmd("auth", username, password) + _ = c(cmd) + return cmd +} + func (c cmdable) Echo(message interface{}) *StringCmd { cmd := NewStringCmd("echo", message) _ = c(cmd) diff --git a/options.go b/options.go index 621d3a37..47dcc29b 100644 --- a/options.go +++ b/options.go @@ -40,8 +40,13 @@ type Options struct { // Hook that is called when new connection is established. OnConnect func(*Conn) error + // Use the specified Username to authenticate the current connection with one of the connections defined in the ACL + // list when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. + Username string + // Optional password. Must match the password specified in the - // requirepass server configuration option. + // requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower), + // or the User Password when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. Password string // Database to be selected after connecting to the server. DB int @@ -187,6 +192,7 @@ func ParseURL(redisURL string) (*Options, error) { } if u.User != nil { + o.Username = u.User.Username() if p, ok := u.User.Password(); ok { o.Password = p } diff --git a/options_test.go b/options_test.go index 9b806f40..f3468ff6 100644 --- a/options_test.go +++ b/options_test.go @@ -15,56 +15,86 @@ func TestParseURL(t *testing.T) { db int tls bool err error + user string + pass string }{ { "redis://localhost:123/1", "localhost:123", 1, false, nil, + "", "", }, { "redis://localhost:123", "localhost:123", 0, false, nil, + "", "", }, { "redis://localhost/1", "localhost:6379", 1, false, nil, + "", "", }, { "redis://12345", "12345:6379", 0, false, nil, + "", "", }, { "rediss://localhost:123", "localhost:123", 0, true, nil, + "", "", + }, + { + "redis://:bar@localhost:123", + "localhost:123", + 0, false, nil, + "", "bar", + }, + { + "redis://foo@localhost:123", + "localhost:123", + 0, false, nil, + "foo", "", + }, + { + "redis://foo:bar@localhost:123", + "localhost:123", + 0, false, nil, + "foo", "bar", }, { "redis://localhost/?abc=123", "", 0, false, errors.New("no options supported"), + "", "", }, { "http://google.com", "", 0, false, errors.New("invalid redis URL scheme: http"), + "", "", }, { "redis://localhost/1/2/3/4", "", 0, false, errors.New("invalid redis URL path: /1/2/3/4"), + "", "", }, { "12345", "", 0, false, errors.New("invalid redis URL scheme: "), + "", "", }, { "redis://localhost/iamadatabase", "", 0, false, errors.New(`invalid redis database number: "iamadatabase"`), + "", "", }, } @@ -90,6 +120,12 @@ func TestParseURL(t *testing.T) { if c.tls && o.TLSConfig == nil { t.Errorf("got nil TLSConfig, expected a TLSConfig") } + if o.Username != c.user { + t.Errorf("got %q, expected %q", o.Username, c.user) + } + if o.Password != c.pass { + t.Errorf("got %q, expected %q", o.Password, c.pass) + } }) } } diff --git a/redis.go b/redis.go index 93032579..3d9dfed7 100644 --- a/redis.go +++ b/redis.go @@ -241,7 +241,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { _, err := conn.Pipelined(func(pipe Pipeliner) error { if c.opt.Password != "" { - pipe.Auth(c.opt.Password) + if c.opt.Username != "" { + pipe.AuthACL(c.opt.Username, c.opt.Password) + } else { + pipe.Auth(c.opt.Password) + } } if c.opt.DB > 0 { diff --git a/sentinel.go b/sentinel.go index 6487ef63..8aa40ef7 100644 --- a/sentinel.go +++ b/sentinel.go @@ -22,6 +22,7 @@ type FailoverOptions struct { MasterName string // A seed list of host:port addresses of sentinel nodes. SentinelAddrs []string + SentinelUsername string SentinelPassword string // Following options are copied from Options struct. @@ -29,6 +30,7 @@ type FailoverOptions struct { Dialer func(ctx context.Context, network, addr string) (net.Conn, error) OnConnect func(*Conn) error + Username string Password string DB int @@ -57,6 +59,7 @@ func (opt *FailoverOptions) options() *Options { OnConnect: opt.OnConnect, DB: opt.DB, + Username: opt.Username, Password: opt.Password, MaxRetries: opt.MaxRetries, @@ -88,6 +91,7 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client { failover := &sentinelFailover{ masterName: failoverOpt.MasterName, sentinelAddrs: failoverOpt.SentinelAddrs, + username: failoverOpt.SentinelUsername, password: failoverOpt.SentinelPassword, opt: opt, @@ -281,6 +285,7 @@ type sentinelFailover struct { sentinelAddrs []string opt *Options + username string password string pool *pool.ConnPool @@ -372,6 +377,7 @@ func (c *sentinelFailover) masterAddr() (string, error) { Addr: sentinelAddr, Dialer: c.opt.Dialer, + Username: c.username, Password: c.password, MaxRetries: c.opt.MaxRetries, diff --git a/universal.go b/universal.go index 21c4d07a..005ca682 100644 --- a/universal.go +++ b/universal.go @@ -22,6 +22,7 @@ type UniversalOptions struct { Dialer func(ctx context.Context, network, addr string) (net.Conn, error) OnConnect func(*Conn) error + Username string Password string MaxRetries int MinRetryBackoff time.Duration @@ -60,6 +61,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { Dialer: o.Dialer, OnConnect: o.OnConnect, + Username: o.Username, Password: o.Password, MaxRedirects: o.MaxRedirects, @@ -99,6 +101,7 @@ func (o *UniversalOptions) Failover() *FailoverOptions { OnConnect: o.OnConnect, DB: o.DB, + Username: o.Username, Password: o.Password, MaxRetries: o.MaxRetries, @@ -133,6 +136,7 @@ func (o *UniversalOptions) Simple() *Options { OnConnect: o.OnConnect, DB: o.DB, + Username: o.Username, Password: o.Password, MaxRetries: o.MaxRetries,