Skip to content

Commit

Permalink
Add Redis Sentinel Authentication Support (go-gitea#19213)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jsievenpiper committed Mar 30, 2022
1 parent 1d33234 commit a2c20a6
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 68 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4
github.com/go-ldap/ldap/v3 v3.4.2
github.com/go-redis/redis/v8 v8.11.4
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/go-swagger/go-swagger v0.29.0
github.com/go-testfixtures/testfixtures/v3 v3.6.1
Expand Down
14 changes: 9 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -593,8 +593,8 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
Expand Down Expand Up @@ -758,6 +758,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
Expand Down Expand Up @@ -1233,15 +1234,18 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
Expand Down
157 changes: 95 additions & 62 deletions modules/nosql/manager_redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
64 changes: 64 additions & 0 deletions modules/nosql/manager_redis_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit a2c20a6

Please sign in to comment.