diff options
author | Justin Sievenpiper <justin@sievenpiper.co> | 2022-03-30 12:12:02 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-30 21:12:02 +0200 |
commit | a2c20a6cab8666c5d4dcdb04b6a64a77a55bfc71 (patch) | |
tree | 283a51c686d6ea8abb6a74c62fd9b573d59b2fd2 /modules/nosql | |
parent | 1d332342db6d5bd4e1552d8d46720bf1b948c26b (diff) | |
download | gitea-a2c20a6cab8666c5d4dcdb04b6a64a77a55bfc71.tar.gz gitea-a2c20a6cab8666c5d4dcdb04b6a64a77a55bfc71.zip |
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.
Diffstat (limited to 'modules/nosql')
-rw-r--r-- | modules/nosql/manager_redis.go | 157 | ||||
-rw-r--r-- | modules/nosql/manager_redis_test.go | 64 |
2 files changed, 159 insertions, 62 deletions
diff --git a/modules/nosql/manager_redis.go b/modules/nosql/manager_redis.go index b4852cecc8..0ff01dcac2 100644 --- a/modules/nosql/manager_redis.go +++ b/modules/nosql/manager_redis.go @@ -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 - } - } + if uri.Host != "" { + opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) + } - 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 - } + // 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.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 } - for _, name := range client.name { - m.RedisConnections[name] = 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 + } } - client.count++ + insecureskipverify := uri.Query().Get("insecureskipverify") - return client + if len(insecureskipverify) > 0 { + insecureskipverify, err := strconv.ParseBool(insecureskipverify) + if err != nil { + tlsConfig.InsecureSkipVerify = insecureskipverify + } + } + + return tlsConfig } diff --git a/modules/nosql/manager_redis_test.go b/modules/nosql/manager_redis_test.go new file mode 100644 index 0000000000..3d94532135 --- /dev/null +++ b/modules/nosql/manager_redis_test.go @@ -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() + } +} |