Skip to content

Commit a2c20a6

Browse files
authored
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.
1 parent 1d33234 commit a2c20a6

File tree

4 files changed

+169
-68
lines changed

4 files changed

+169
-68
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ require (
3232
github.com/go-git/go-billy/v5 v5.3.1
3333
github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4
3434
github.com/go-ldap/ldap/v3 v3.4.2
35-
github.com/go-redis/redis/v8 v8.11.4
35+
github.com/go-redis/redis/v8 v8.11.5
3636
github.com/go-sql-driver/mysql v1.6.0
3737
github.com/go-swagger/go-swagger v0.29.0
3838
github.com/go-testfixtures/testfixtures/v3 v3.6.1

go.sum

+9-5
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,8 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
593593
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
594594
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
595595
github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
596-
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
597-
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
596+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
597+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
598598
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
599599
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
600600
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -758,6 +758,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
758758
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
759759
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
760760
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
761+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
761762
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
762763
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
763764
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
@@ -1233,15 +1234,18 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
12331234
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
12341235
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
12351236
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
1236-
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
12371237
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
1238+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
1239+
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
1240+
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
12381241
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
12391242
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
12401243
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
12411244
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
12421245
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
1243-
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
1244-
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
1246+
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
1247+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
1248+
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
12451249
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
12461250
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
12471251
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=

modules/nosql/manager_redis.go

+95-62
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ package nosql
66

77
import (
88
"crypto/tls"
9+
"net/url"
910
"path"
1011
"strconv"
1112
"strings"
1213

14+
"code.gitea.io/gitea/modules/log"
15+
1316
"github.com/go-redis/redis/v8"
1417
)
1518

@@ -59,8 +62,59 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
5962
name: []string{connection, uri.String()},
6063
}
6164

65+
opts := getRedisOptions(uri)
66+
tlsConfig := getRedisTLSOptions(uri)
67+
68+
clientName := uri.Query().Get("clientname")
69+
70+
if len(clientName) > 0 {
71+
client.name = append(client.name, clientName)
72+
}
73+
74+
switch uri.Scheme {
75+
case "redis+sentinels":
76+
fallthrough
77+
case "rediss+sentinel":
78+
opts.TLSConfig = tlsConfig
79+
fallthrough
80+
case "redis+sentinel":
81+
client.UniversalClient = redis.NewFailoverClient(opts.Failover())
82+
case "redis+clusters":
83+
fallthrough
84+
case "rediss+cluster":
85+
opts.TLSConfig = tlsConfig
86+
fallthrough
87+
case "redis+cluster":
88+
client.UniversalClient = redis.NewClusterClient(opts.Cluster())
89+
case "redis+socket":
90+
simpleOpts := opts.Simple()
91+
simpleOpts.Network = "unix"
92+
simpleOpts.Addr = path.Join(uri.Host, uri.Path)
93+
client.UniversalClient = redis.NewClient(simpleOpts)
94+
case "rediss":
95+
opts.TLSConfig = tlsConfig
96+
fallthrough
97+
case "redis":
98+
client.UniversalClient = redis.NewClient(opts.Simple())
99+
default:
100+
return nil
101+
}
102+
103+
for _, name := range client.name {
104+
m.RedisConnections[name] = client
105+
}
106+
107+
client.count++
108+
109+
return client
110+
}
111+
112+
// getRedisOptions pulls various configuration options based on the RedisUri format and converts them to go-redis's
113+
// UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is
114+
// conditionally attached to this options struct before being converted to the specific type for the redis scheme being
115+
// used, and only in scenarios where TLS is applicable (e.g. rediss://, redis+clusters://).
116+
func getRedisOptions(uri *url.URL) *redis.UniversalOptions {
62117
opts := &redis.UniversalOptions{}
63-
tlsConfig := &tls.Config{}
64118

65119
// Handle username/password
66120
if password, ok := uri.User.Password(); ok {
@@ -131,75 +185,54 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
131185
fallthrough
132186
case "mastername":
133187
opts.MasterName = v[0]
134-
case "skipverify":
135-
fallthrough
136-
case "insecureskipverify":
137-
insecureSkipVerify, _ := strconv.ParseBool(v[0])
138-
tlsConfig.InsecureSkipVerify = insecureSkipVerify
139-
case "clientname":
140-
client.name = append(client.name, v[0])
188+
case "sentinelusername":
189+
opts.SentinelUsername = v[0]
190+
case "sentinelpassword":
191+
opts.SentinelPassword = v[0]
141192
}
142193
}
143194

144-
switch uri.Scheme {
145-
case "redis+sentinels":
146-
fallthrough
147-
case "rediss+sentinel":
148-
opts.TLSConfig = tlsConfig
149-
fallthrough
150-
case "redis+sentinel":
151-
if uri.Host != "" {
152-
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
153-
}
154-
if uri.Path != "" {
155-
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
156-
opts.DB = db
157-
}
158-
}
195+
if uri.Host != "" {
196+
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
197+
}
159198

160-
client.UniversalClient = redis.NewFailoverClient(opts.Failover())
161-
case "redis+clusters":
162-
fallthrough
163-
case "rediss+cluster":
164-
opts.TLSConfig = tlsConfig
165-
fallthrough
166-
case "redis+cluster":
167-
if uri.Host != "" {
168-
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
169-
}
170-
if uri.Path != "" {
171-
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
172-
opts.DB = db
173-
}
199+
// A redis connection string uses the path section of the URI in two different ways. In a TCP-based connection, the
200+
// path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the
201+
// file path. We only want to try to coerce this to the database index when we're not expecting a file path so that
202+
// the error log stays clean.
203+
if uri.Path != "" && uri.Scheme != "redis+socket" {
204+
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
205+
opts.DB = db
206+
} else {
207+
log.Error("Provided database identifier '%s' is not a valid integer. Gitea will ignore this option.", uri.Path)
174208
}
175-
client.UniversalClient = redis.NewClusterClient(opts.Cluster())
176-
case "redis+socket":
177-
simpleOpts := opts.Simple()
178-
simpleOpts.Network = "unix"
179-
simpleOpts.Addr = path.Join(uri.Host, uri.Path)
180-
client.UniversalClient = redis.NewClient(simpleOpts)
181-
case "rediss":
182-
opts.TLSConfig = tlsConfig
183-
fallthrough
184-
case "redis":
185-
if uri.Host != "" {
186-
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
187-
}
188-
if uri.Path != "" {
189-
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
190-
opts.DB = db
191-
}
192-
}
193-
client.UniversalClient = redis.NewClient(opts.Simple())
194-
default:
195-
return nil
196209
}
197210

198-
for _, name := range client.name {
199-
m.RedisConnections[name] = client
211+
return opts
212+
}
213+
214+
// getRedisTlsOptions parses RedisUri TLS configuration parameters and converts them to the go TLS configuration
215+
// equivalent fields.
216+
func getRedisTLSOptions(uri *url.URL) *tls.Config {
217+
tlsConfig := &tls.Config{}
218+
219+
skipverify := uri.Query().Get("skipverify")
220+
221+
if len(skipverify) > 0 {
222+
skipverify, err := strconv.ParseBool(skipverify)
223+
if err != nil {
224+
tlsConfig.InsecureSkipVerify = skipverify
225+
}
200226
}
201227

202-
client.count++
228+
insecureskipverify := uri.Query().Get("insecureskipverify")
203229

204-
return client
230+
if len(insecureskipverify) > 0 {
231+
insecureskipverify, err := strconv.ParseBool(insecureskipverify)
232+
if err != nil {
233+
tlsConfig.InsecureSkipVerify = insecureskipverify
234+
}
235+
}
236+
237+
return tlsConfig
205238
}

modules/nosql/manager_redis_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package nosql
6+
7+
import (
8+
"net/url"
9+
"testing"
10+
)
11+
12+
func TestRedisUsernameOpt(t *testing.T) {
13+
uri, _ := url.Parse("redis://redis:password@myredis/0")
14+
opts := getRedisOptions(uri)
15+
16+
if opts.Username != "redis" {
17+
t.Fail()
18+
}
19+
}
20+
21+
func TestRedisPasswordOpt(t *testing.T) {
22+
uri, _ := url.Parse("redis://redis:password@myredis/0")
23+
opts := getRedisOptions(uri)
24+
25+
if opts.Password != "password" {
26+
t.Fail()
27+
}
28+
}
29+
30+
func TestRedisSentinelUsernameOpt(t *testing.T) {
31+
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass")
32+
opts := getRedisOptions(uri).Failover()
33+
34+
if opts.SentinelUsername != "suser" {
35+
t.Fail()
36+
}
37+
}
38+
39+
func TestRedisSentinelPasswordOpt(t *testing.T) {
40+
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass")
41+
opts := getRedisOptions(uri).Failover()
42+
43+
if opts.SentinelPassword != "spass" {
44+
t.Fail()
45+
}
46+
}
47+
48+
func TestRedisDatabaseIndexTcp(t *testing.T) {
49+
uri, _ := url.Parse("redis://redis:password@myredis/12")
50+
opts := getRedisOptions(uri)
51+
52+
if opts.DB != 12 {
53+
t.Fail()
54+
}
55+
}
56+
57+
func TestRedisDatabaseIndexUnix(t *testing.T) {
58+
uri, _ := url.Parse("redis+socket:///var/run/redis.sock?database=12")
59+
opts := getRedisOptions(uri)
60+
61+
if opts.DB != 12 {
62+
t.Fail()
63+
}
64+
}

0 commit comments

Comments
 (0)