Add Redis Sentinel Authentication Support (#19213)

Gitea was not able to supply any authentication parameters to it. So this brings support to do that, along with some light extraction of a couple of bits into some separate functions for easier testing.

I looked at other libraries supporting similar RedisUri-style connection strings (e.g. Lettuce), but it looks like this type of configuration is beyond what would typically be done in a connection string. Since gitea doesn't have configuration options for manually specifying all this redis connection detail, I went ahead and just chose straightforward names for these new parameters.
This commit is contained in:
Justin Sievenpiper 2022-03-30 12:12:02 -07:00 committed by GitHub
parent 1d332342db
commit a2c20a6cab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 71 deletions

View file

@ -6,10 +6,13 @@ package nosql
import (
"crypto/tls"
"net/url"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/go-redis/redis/v8"
)
@ -59,8 +62,59 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
name: []string{connection, uri.String()},
}
opts := getRedisOptions(uri)
tlsConfig := getRedisTLSOptions(uri)
clientName := uri.Query().Get("clientname")
if len(clientName) > 0 {
client.name = append(client.name, clientName)
}
switch uri.Scheme {
case "redis+sentinels":
fallthrough
case "rediss+sentinel":
opts.TLSConfig = tlsConfig
fallthrough
case "redis+sentinel":
client.UniversalClient = redis.NewFailoverClient(opts.Failover())
case "redis+clusters":
fallthrough
case "rediss+cluster":
opts.TLSConfig = tlsConfig
fallthrough
case "redis+cluster":
client.UniversalClient = redis.NewClusterClient(opts.Cluster())
case "redis+socket":
simpleOpts := opts.Simple()
simpleOpts.Network = "unix"
simpleOpts.Addr = path.Join(uri.Host, uri.Path)
client.UniversalClient = redis.NewClient(simpleOpts)
case "rediss":
opts.TLSConfig = tlsConfig
fallthrough
case "redis":
client.UniversalClient = redis.NewClient(opts.Simple())
default:
return nil
}
for _, name := range client.name {
m.RedisConnections[name] = client
}
client.count++
return client
}
// getRedisOptions pulls various configuration options based on the RedisUri format and converts them to go-redis's
// UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is
// conditionally attached to this options struct before being converted to the specific type for the redis scheme being
// used, and only in scenarios where TLS is applicable (e.g. rediss://, redis+clusters://).
func getRedisOptions(uri *url.URL) *redis.UniversalOptions {
opts := &redis.UniversalOptions{}
tlsConfig := &tls.Config{}
// Handle username/password
if password, ok := uri.User.Password(); ok {
@ -131,75 +185,54 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
fallthrough
case "mastername":
opts.MasterName = v[0]
case "skipverify":
fallthrough
case "insecureskipverify":
insecureSkipVerify, _ := strconv.ParseBool(v[0])
tlsConfig.InsecureSkipVerify = insecureSkipVerify
case "clientname":
client.name = append(client.name, v[0])
case "sentinelusername":
opts.SentinelUsername = v[0]
case "sentinelpassword":
opts.SentinelPassword = v[0]
}
}
switch uri.Scheme {
case "redis+sentinels":
fallthrough
case "rediss+sentinel":
opts.TLSConfig = tlsConfig
fallthrough
case "redis+sentinel":
if uri.Host != "" {
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
}
if uri.Path != "" {
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
opts.DB = db
}
}
client.UniversalClient = redis.NewFailoverClient(opts.Failover())
case "redis+clusters":
fallthrough
case "rediss+cluster":
opts.TLSConfig = tlsConfig
fallthrough
case "redis+cluster":
if uri.Host != "" {
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
}
if uri.Path != "" {
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
opts.DB = db
}
}
client.UniversalClient = redis.NewClusterClient(opts.Cluster())
case "redis+socket":
simpleOpts := opts.Simple()
simpleOpts.Network = "unix"
simpleOpts.Addr = path.Join(uri.Host, uri.Path)
client.UniversalClient = redis.NewClient(simpleOpts)
case "rediss":
opts.TLSConfig = tlsConfig
fallthrough
case "redis":
if uri.Host != "" {
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
}
if uri.Path != "" {
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
opts.DB = db
}
}
client.UniversalClient = redis.NewClient(opts.Simple())
default:
return nil
if uri.Host != "" {
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
}
for _, name := range client.name {
m.RedisConnections[name] = client
// A redis connection string uses the path section of the URI in two different ways. In a TCP-based connection, the
// path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the
// file path. We only want to try to coerce this to the database index when we're not expecting a file path so that
// the error log stays clean.
if uri.Path != "" && uri.Scheme != "redis+socket" {
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
opts.DB = db
} else {
log.Error("Provided database identifier '%s' is not a valid integer. Gitea will ignore this option.", uri.Path)
}
}
client.count++
return client
return opts
}
// getRedisTlsOptions parses RedisUri TLS configuration parameters and converts them to the go TLS configuration
// equivalent fields.
func getRedisTLSOptions(uri *url.URL) *tls.Config {
tlsConfig := &tls.Config{}
skipverify := uri.Query().Get("skipverify")
if len(skipverify) > 0 {
skipverify, err := strconv.ParseBool(skipverify)
if err != nil {
tlsConfig.InsecureSkipVerify = skipverify
}
}
insecureskipverify := uri.Query().Get("insecureskipverify")
if len(insecureskipverify) > 0 {
insecureskipverify, err := strconv.ParseBool(insecureskipverify)
if err != nil {
tlsConfig.InsecureSkipVerify = insecureskipverify
}
}
return tlsConfig
}

View file

@ -0,0 +1,64 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package nosql
import (
"net/url"
"testing"
)
func TestRedisUsernameOpt(t *testing.T) {
uri, _ := url.Parse("redis://redis:password@myredis/0")
opts := getRedisOptions(uri)
if opts.Username != "redis" {
t.Fail()
}
}
func TestRedisPasswordOpt(t *testing.T) {
uri, _ := url.Parse("redis://redis:password@myredis/0")
opts := getRedisOptions(uri)
if opts.Password != "password" {
t.Fail()
}
}
func TestRedisSentinelUsernameOpt(t *testing.T) {
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass")
opts := getRedisOptions(uri).Failover()
if opts.SentinelUsername != "suser" {
t.Fail()
}
}
func TestRedisSentinelPasswordOpt(t *testing.T) {
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass")
opts := getRedisOptions(uri).Failover()
if opts.SentinelPassword != "spass" {
t.Fail()
}
}
func TestRedisDatabaseIndexTcp(t *testing.T) {
uri, _ := url.Parse("redis://redis:password@myredis/12")
opts := getRedisOptions(uri)
if opts.DB != 12 {
t.Fail()
}
}
func TestRedisDatabaseIndexUnix(t *testing.T) {
uri, _ := url.Parse("redis+socket:///var/run/redis.sock?database=12")
opts := getRedisOptions(uri)
if opts.DB != 12 {
t.Fail()
}
}