diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47f3e4c7..5fe8b02a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: REDIS_SENTINEL_TEST_MASTER_NAME: redis-1 REDIS_SENTINEL_TEST_PASSWORD: sekret PROTECTED_REDIS_TEST_ENDPOINT: localhost:6380 + PROTECTED_REDIS_TEST_USERNAME: default + REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT: localhost:26380 + REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME: protectedredis-1 ATHENS_PROTECTED_REDIS_PASSWORD: AthensPass1 GA_PULL_REQUEST: ${{github.event.number}} runs-on: ubuntu-latest @@ -69,6 +72,17 @@ jobs: env: REDIS_PORT_NUMBER: 6380 REDIS_PASSWORD: AthensPass1 + redis-sentinel-protected-redis: + image: bitnami/redis-sentinel + env: + REDIS_MASTER_HOST: protectedredis + REDIS_MASTER_PORT_NUMBER: 6380 + REDIS_MASTER_SET: protectedredis-1 + REDIS_SENTINEL_PASSWORD: sekret + REDIS_SENTINEL_QUORUM: "1" + REDIS_SENTINEL_PORT_NUMBER: 26380 + ports: + - 26380:26380 steps: - uses: actions/checkout@v4 - name: Set up Go diff --git a/cmd/proxy/actions/app_proxy.go b/cmd/proxy/actions/app_proxy.go index 86c2a247..4c2025ee 100644 --- a/cmd/proxy/actions/app_proxy.go +++ b/cmd/proxy/actions/app_proxy.go @@ -166,6 +166,8 @@ func getSingleFlight(l *log.Logger, c *config.Config, s storage.Backend, checker c.SingleFlight.RedisSentinel.Endpoints, c.SingleFlight.RedisSentinel.MasterName, c.SingleFlight.RedisSentinel.SentinelPassword, + c.SingleFlight.RedisSentinel.RedisUsername, + c.SingleFlight.RedisSentinel.RedisPassword, checker, c.SingleFlight.RedisSentinel.LockConfig, ) diff --git a/config.dev.toml b/config.dev.toml index 5049a89f..0784be96 100755 --- a/config.dev.toml +++ b/config.dev.toml @@ -369,6 +369,12 @@ ShutdownTimeout = 60 # Env override: ATHENS_REDIS_SENTINEL_PASSWORD SentinelPassword = "sekret" + # The Redis master authorization parameters + # Env override: ATHENS_REDIS_USERNAME + RedisUsername = "" + # Env override: ATHENS_REDIS_PASSWORD + RedisPassword = "" + [SingleFlight.RedisSentinel.LockConfig] # TTL for the lock in seconds. Defaults to 900 seconds (15 minutes). # Env override: ATHENS_REDIS_LOCK_TTL diff --git a/pkg/config/config.go b/pkg/config/config.go index b591b949..0fb5fa8f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -179,6 +179,8 @@ func defaultConfig() *Config { Endpoints: []string{"127.0.0.1:26379"}, MasterName: "redis-1", SentinelPassword: "sekret", + RedisUsername: "", + RedisPassword: "", LockConfig: DefaultRedisLockConfig(), }, GCP: DefaultGCPConfig(), diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 2fad9c5e..4398400b 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -375,7 +375,7 @@ func getEnvMap(config *Config) map[string]string { if singleFlight.Redis != nil { envVars["ATHENS_SINGLE_FLIGHT_TYPE"] = "redis" envVars["ATHENS_REDIS_ENDPOINT"] = singleFlight.Redis.Endpoint - envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.Redis.Endpoint + envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.Redis.Password if singleFlight.Redis.LockConfig != nil { envVars["ATHENS_REDIS_LOCK_TTL"] = strconv.Itoa(singleFlight.Redis.LockConfig.TTL) envVars["ATHENS_REDIS_LOCK_TIMEOUT"] = strconv.Itoa(singleFlight.Redis.LockConfig.Timeout) @@ -386,6 +386,8 @@ func getEnvMap(config *Config) map[string]string { envVars["ATHENS_REDIS_SENTINEL_ENDPOINTS"] = strings.Join(singleFlight.RedisSentinel.Endpoints, ",") envVars["ATHENS_REDIS_SENTINEL_MASTER_NAME"] = singleFlight.RedisSentinel.MasterName envVars["ATHENS_REDIS_SENTINEL_PASSWORD"] = singleFlight.RedisSentinel.SentinelPassword + envVars["ATHENS_REDIS_USERNAME"] = singleFlight.RedisSentinel.RedisUsername + envVars["ATHENS_REDIS_PASSWORD"] = singleFlight.RedisSentinel.RedisPassword if singleFlight.RedisSentinel.LockConfig != nil { envVars["ATHENS_REDIS_LOCK_TTL"] = strconv.Itoa(singleFlight.RedisSentinel.LockConfig.TTL) envVars["ATHENS_REDIS_LOCK_TIMEOUT"] = strconv.Itoa(singleFlight.RedisSentinel.LockConfig.Timeout) diff --git a/pkg/config/singleflight.go b/pkg/config/singleflight.go index b3b6fe04..eae580f7 100644 --- a/pkg/config/singleflight.go +++ b/pkg/config/singleflight.go @@ -31,6 +31,8 @@ type RedisSentinel struct { Endpoints []string `envconfig:"ATHENS_REDIS_SENTINEL_ENDPOINTS"` MasterName string `envconfig:"ATHENS_REDIS_SENTINEL_MASTER_NAME"` SentinelPassword string `envconfig:"ATHENS_REDIS_SENTINEL_PASSWORD"` + RedisUsername string `envconfig:"ATHENS_REDIS_USERNAME"` + RedisPassword string `envconfig:"ATHENS_REDIS_PASSWORD"` LockConfig *RedisLockConfig } diff --git a/pkg/stash/with_redis_sentinel.go b/pkg/stash/with_redis_sentinel.go index bf890b20..fbc554c0 100644 --- a/pkg/stash/with_redis_sentinel.go +++ b/pkg/stash/with_redis_sentinel.go @@ -11,7 +11,7 @@ import ( // WithRedisSentinelLock returns a distributed singleflight // with a redis cluster that utilizes sentinel for quorum and failover. -func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, password string, checker storage.Checker, lockConfig *config.RedisLockConfig) (Wrapper, error) { +func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, sentinelPassword, redisUsername, redisPassword string, checker storage.Checker, lockConfig *config.RedisLockConfig) (Wrapper, error) { redis.SetLogger(l) const op errors.Op = "stash.WithRedisSentinelLock" @@ -23,7 +23,9 @@ func WithRedisSentinelLock(l RedisLogger, endpoints []string, master, password s client := redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: master, SentinelAddrs: endpoints, - SentinelPassword: password, + SentinelPassword: sentinelPassword, + Username: redisUsername, + Password: redisPassword, }) _, err := client.Ping(context.Background()).Result() if err != nil { diff --git a/pkg/stash/with_redis_sentinel_test.go b/pkg/stash/with_redis_sentinel_test.go index ec2ee29d..e5af6ca3 100644 --- a/pkg/stash/with_redis_sentinel_test.go +++ b/pkg/stash/with_redis_sentinel_test.go @@ -18,8 +18,8 @@ import ( func TestWithRedisSentinelLock(t *testing.T) { endpoint := os.Getenv("REDIS_SENTINEL_TEST_ENDPOINT") masterName := os.Getenv("REDIS_SENTINEL_TEST_MASTER_NAME") - password := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD") - if len(endpoint) == 0 || len(masterName) == 0 || len(password) == 0 { + sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD") + if len(endpoint) == 0 || len(masterName) == 0 || len(sentinelPassword) == 0 { t.SkipNow() } strg, err := mem.NewStorage() @@ -29,7 +29,7 @@ func TestWithRedisSentinelLock(t *testing.T) { ms := &mockRedisStasher{strg: strg} l := &testingRedisLogger{t: t} - wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, password, storage.WithChecker(strg), config.DefaultRedisLockConfig()) + wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", "", storage.WithChecker(strg), config.DefaultRedisLockConfig()) if err != nil { t.Fatal(err) } @@ -50,3 +50,109 @@ func TestWithRedisSentinelLock(t *testing.T) { t.Fatal(err) } } + +// TestWithRedisSentinelLockWithRedisPassword verifies WithRedisSentinelLock working with +// password protected redis sentinel and redis master nodes +func TestWithRedisSentinelLockWithRedisPassword(t *testing.T) { + endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT") + masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME") + sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD") + redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD") + if len(endpoint) == 0 || len(masterName) == 0 { + t.SkipNow() + } + strg, err := mem.NewStorage() + if err != nil { + t.Fatal(err) + } + ms := &mockRedisStasher{strg: strg} + l := &testingRedisLogger{t: t} + wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig()) + if err != nil { + t.Fatal(err) + } + s := wrapper(ms) + + var eg errgroup.Group + for i := 0; i < 5; i++ { + eg.Go(func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + _, err := s.Stash(ctx, "mod", "ver") + return err + }) + } + + err = eg.Wait() + if err != nil { + t.Fatal(err) + } +} + +// TestWithRedisSentinelLockWithPassword verifies WithRedisSentinelLock working with +// username & password protected master node under the redis sentinel +func TestWithRedisSentinelLockWithUsernameAndPassword(t *testing.T) { + endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT") + masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME") + sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD") + redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD") + redisUsername := os.Getenv("PROTECTED_REDIS_TEST_USERNAME") + if len(endpoint) == 0 || len(masterName) == 0 { + t.SkipNow() + } + strg, err := mem.NewStorage() + if err != nil { + t.Fatal(err) + } + ms := &mockRedisStasher{strg: strg} + l := &testingRedisLogger{t: t} + wrapper, err := WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, redisUsername, redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig()) + if err != nil { + t.Fatal(err) + } + s := wrapper(ms) + + var eg errgroup.Group + for i := 0; i < 5; i++ { + eg.Go(func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + _, err := s.Stash(ctx, "mod", "ver") + return err + }) + } + + err = eg.Wait() + if err != nil { + t.Fatal(err) + } +} + +// TestWithRedisSentinelLockWithWrongPassword verifies the WithRedisSentinelLock fails +// with the correct error when trying to connect with wrong passwords +func TestWithRedisSentinelLockWithWrongRedisPassword(t *testing.T) { + endpoint := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_ENDPOINT") + masterName := os.Getenv("REDIS_SENTINEL_TEST_PROTECTED_MASTER_NAME") + sentinelPassword := os.Getenv("REDIS_SENTINEL_TEST_PASSWORD") + redisPassword := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD") + if len(endpoint) == 0 || len(masterName) == 0 { + t.SkipNow() + } + strg, err := mem.NewStorage() + if err != nil { + t.Fatal(err) + } + l := &testingRedisLogger{t: t} + + // Test with wrong sentinel password + _, err = WithRedisSentinelLock(l, []string{endpoint}, masterName, "wrong-sentinel-password", "", redisPassword, storage.WithChecker(strg), config.DefaultRedisLockConfig()) + if err == nil { + t.Fatal("Expected Connection Error for wrong sentinel password") + } + + // Test with wrong redis password + _, err = WithRedisSentinelLock(l, []string{endpoint}, masterName, sentinelPassword, "", "wrong-redis-password", storage.WithChecker(strg), config.DefaultRedisLockConfig()) + if err == nil { + t.Fatal("Expected Connection Error for wrong redis password") + } +}