summaryrefslogtreecommitdiffstats
path: root/modules/nosql
diff options
context:
space:
mode:
authorJustin Sievenpiper <justin@sievenpiper.co>2022-03-30 12:12:02 -0700
committerGitHub <noreply@github.com>2022-03-30 21:12:02 +0200
commita2c20a6cab8666c5d4dcdb04b6a64a77a55bfc71 (patch)
tree283a51c686d6ea8abb6a74c62fd9b573d59b2fd2 /modules/nosql
parent1d332342db6d5bd4e1552d8d46720bf1b948c26b (diff)
downloadgitea-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.go157
-rw-r--r--modules/nosql/manager_redis_test.go64
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()
+ }
+}