diff --git a/go.mod b/go.mod index 4f54660f..52fd9f53 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,15 @@ require ( github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb github.com/a8m/envsubst v1.4.3 github.com/alicebob/miniredis/v2 v2.35.0 + github.com/aws/aws-sdk-go-v2 v1.41.5 + github.com/aws/aws-sdk-go-v2/config v1.32.13 github.com/bitly/go-simplejson v0.5.1 github.com/bsm/redislock v0.9.4 github.com/coreos/go-oidc/v3 v3.17.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/fsnotify/fsnotify v1.9.0 github.com/go-jose/go-jose/v3 v3.0.4 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-cmp v0.7.0 @@ -44,12 +47,23 @@ require ( cloud.google.com/go/auth v0.18.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index ac6b56d4..c7e37529 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,34 @@ github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg= +github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index b57d5aed..9ea8497f 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -159,6 +159,11 @@ func NewFlagSet() *pflag.FlagSet { flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://[USER[:PASSWORD]@]HOST[:PORT]). Used in conjunction with --redis-use-cluster") flagSet.Int("redis-connection-idle-timeout", 0, "Redis connection idle timeout seconds, if Redis timeout option is non-zero, the --redis-connection-idle-timeout must be less then Redis timeout option") + flagSet.Bool("redis-use-iam-auth", false, "Use AWS IAM authentication for Redis/ElastiCache. Requires TLS and IRSA or other AWS credential source") + flagSet.String("redis-iam-user-id", "", "ElastiCache IAM-enabled user ID for AWS IAM authentication") + flagSet.String("redis-iam-replication-group-id", "", "ElastiCache replication group ID (cluster name) for AWS IAM authentication") + flagSet.String("redis-iam-region", "", "AWS region for ElastiCache IAM authentication. If empty, uses AWS_REGION or AWS_DEFAULT_REGION") + flagSet.Bool("redis-iam-serverless", false, "Set when using ElastiCache Serverless (adds ResourceType=ServerlessCache to the IAM auth token)") flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index c90c0ac2..f7837b7a 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -34,6 +34,11 @@ type RedisStoreOptions struct { CAPath string `flag:"redis-ca-path" cfg:"redis_ca_path"` InsecureSkipTLSVerify bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify"` IdleTimeout int `flag:"redis-connection-idle-timeout" cfg:"redis_connection_idle_timeout"` + UseIAMAuth bool `flag:"redis-use-iam-auth" cfg:"redis_use_iam_auth"` + IAMUserID string `flag:"redis-iam-user-id" cfg:"redis_iam_user_id"` + IAMReplicationGroupID string `flag:"redis-iam-replication-group-id" cfg:"redis_iam_replication_group_id"` + IAMRegion string `flag:"redis-iam-region" cfg:"redis_iam_region"` + IAMServerless bool `flag:"redis-iam-serverless" cfg:"redis_iam_serverless"` } func sessionOptionsDefaults() SessionOptions { diff --git a/pkg/sessions/redis/iam_token.go b/pkg/sessions/redis/iam_token.go new file mode 100644 index 00000000..b18a14f8 --- /dev/null +++ b/pkg/sessions/redis/iam_token.go @@ -0,0 +1,69 @@ +package redis + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" +) + +const ( + elasticacheServiceName = "elasticache" + connectAction = "connect" + tokenExpirySeconds = 900 // 15 minutes + // SHA-256 hash of an empty string, required by SigV4 PresignHTTP. + emptyBodySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +) + +// iamTokenGenerator generates short-lived IAM auth tokens for ElastiCache +// by SigV4-presigning a request. Each call to Generate produces a fresh token. +type iamTokenGenerator struct { + userID string + replicationGroupID string + region string + serverless bool + credentials aws.CredentialsProvider +} + +// Generate creates a new IAM auth token by SigV4 presigning a synthetic +// HTTP request. The returned string is used as the Redis AUTH password. +func (g *iamTokenGenerator) Generate(ctx context.Context) (string, error) { + u, err := url.Parse(fmt.Sprintf("http://%s/", g.replicationGroupID)) + if err != nil { + return "", fmt.Errorf("failed to build IAM token URL: %w", err) + } + + query := u.Query() + query.Set("Action", connectAction) + query.Set("User", g.userID) + if g.serverless { + query.Set("ResourceType", "ServerlessCache") + } + query.Set("X-Amz-Expires", strconv.Itoa(tokenExpirySeconds)) + u.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create IAM token request: %w", err) + } + + creds, err := g.credentials.Retrieve(ctx) + if err != nil { + return "", fmt.Errorf("failed to retrieve AWS credentials: %w", err) + } + + signer := v4.NewSigner() + uri, _, err := signer.PresignHTTP(ctx, creds, req, emptyBodySHA256, elasticacheServiceName, g.region, time.Now()) + if err != nil { + return "", fmt.Errorf("failed to presign IAM token: %w", err) + } + + // The auth token is the presigned URL without the "http://" prefix. + return strings.TrimPrefix(uri, "http://"), nil +} diff --git a/pkg/sessions/redis/iam_token_test.go b/pkg/sessions/redis/iam_token_test.go new file mode 100644 index 00000000..2effce0a --- /dev/null +++ b/pkg/sessions/redis/iam_token_test.go @@ -0,0 +1,79 @@ +package redis + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// staticCredentials implements aws.CredentialsProvider with fixed test values. +type staticCredentials struct{} + +func (s staticCredentials) Retrieve(_ context.Context) (aws.Credentials, error) { + return aws.Credentials{ + AccessKeyID: "AKIAIOSFODNN7EXAMPLE", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + SessionToken: "test-session-token", + Source: "test", + CanExpire: true, + Expires: time.Now().Add(1 * time.Hour), + }, nil +} + +func TestIAMTokenGenerator_Generate(t *testing.T) { + gen := &iamTokenGenerator{ + userID: "my-user", + replicationGroupID: "my-cluster", + region: "us-east-1", + credentials: staticCredentials{}, + } + + token, err := gen.Generate(context.Background()) + require.NoError(t, err) + + // The token should not have the http:// prefix + assert.False(t, strings.HasPrefix(token, "http://")) + + // The token must start with the replication group ID + assert.True(t, strings.HasPrefix(token, "my-cluster/")) + + // The token must contain the required query parameters + assert.Contains(t, token, "Action=connect") + assert.Contains(t, token, "User=my-user") + assert.Contains(t, token, "X-Amz-Expires=900") + + // Non-serverless tokens must NOT contain ResourceType + assert.NotContains(t, token, "ResourceType") + + // SigV4 signature fields + assert.Contains(t, token, "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE") + assert.Contains(t, token, "X-Amz-Signature=") + assert.Contains(t, token, "X-Amz-Security-Token=test-session-token") + assert.Contains(t, token, "X-Amz-SignedHeaders=host") +} + +func TestIAMTokenGenerator_Generate_Serverless(t *testing.T) { + gen := &iamTokenGenerator{ + userID: "my-user", + replicationGroupID: "my-cache", + region: "us-east-1", + serverless: true, + credentials: staticCredentials{}, + } + + token, err := gen.Generate(context.Background()) + require.NoError(t, err) + + assert.False(t, strings.HasPrefix(token, "http://")) + assert.True(t, strings.HasPrefix(token, "my-cache/")) + assert.Contains(t, token, "Action=connect") + assert.Contains(t, token, "User=my-user") + assert.Contains(t, token, "ResourceType=ServerlessCache") + assert.Contains(t, token, "X-Amz-Expires=900") + assert.Contains(t, token, "X-Amz-Signature=") +} diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index 79f8f7d1..c1ea128c 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -8,6 +8,7 @@ import ( "os" "time" + awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" @@ -117,7 +118,7 @@ func buildSentinelClient(opts options.RedisStoreOptions) (Client, error) { return nil, err } - client := redis.NewFailoverClient(&redis.FailoverOptions{ + failoverOpts := &redis.FailoverOptions{ MasterName: opts.SentinelMasterName, SentinelAddrs: addrs, SentinelPassword: opts.SentinelPassword, @@ -125,7 +126,19 @@ func buildSentinelClient(opts options.RedisStoreOptions) (Client, error) { Password: opt.Password, TLSConfig: opt.TLSConfig, ConnMaxIdleTime: opt.ConnMaxIdleTime, - }) + } + + if opts.UseIAMAuth { + credsFn, err := newIAMCredentialsProvider(opts) + if err != nil { + return nil, err + } + failoverOpts.Username = "" + failoverOpts.Password = "" + failoverOpts.CredentialsProviderContext = credsFn + } + + client := redis.NewFailoverClient(failoverOpts) return newClient(client), nil } @@ -150,13 +163,25 @@ func buildClusterClient(opts options.RedisStoreOptions) (Client, error) { return nil, err } - client := redis.NewClusterClient(&redis.ClusterOptions{ + clusterOpts := &redis.ClusterOptions{ Addrs: addrs, Username: opt.Username, Password: opt.Password, TLSConfig: opt.TLSConfig, ConnMaxIdleTime: opt.ConnMaxIdleTime, - }) + } + + if opts.UseIAMAuth { + credsFn, err := newIAMCredentialsProvider(opts) + if err != nil { + return nil, err + } + clusterOpts.Username = "" + clusterOpts.Password = "" + clusterOpts.CredentialsProviderContext = credsFn + } + + client := redis.NewClusterClient(clusterOpts) return newClusterClient(client), nil } @@ -182,6 +207,16 @@ func buildStandaloneClient(opts options.RedisStoreOptions) (Client, error) { return nil, err } + if opts.UseIAMAuth { + credsFn, err := newIAMCredentialsProvider(opts) + if err != nil { + return nil, err + } + opt.Username = "" + opt.Password = "" + opt.CredentialsProviderContext = credsFn + } + client := redis.NewClient(opt) return newClient(client), nil } @@ -245,4 +280,34 @@ func parseRedisURLs(urls []string) ([]string, *redis.Options, error) { return addrs, redisOptions, nil } +// newIAMCredentialsProvider creates a go-redis CredentialsProviderContext +// that generates fresh IAM auth tokens on each new Redis connection. +func newIAMCredentialsProvider(opts options.RedisStoreOptions) (func(ctx context.Context) (string, string, error), error) { + var cfgOpts []func(*awsconfig.LoadOptions) error + if opts.IAMRegion != "" { + cfgOpts = append(cfgOpts, awsconfig.WithRegion(opts.IAMRegion)) + } + + cfg, err := awsconfig.LoadDefaultConfig(context.Background(), cfgOpts...) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config for IAM Redis auth: %w", err) + } + + gen := &iamTokenGenerator{ + userID: opts.IAMUserID, + replicationGroupID: opts.IAMReplicationGroupID, + region: cfg.Region, + serverless: opts.IAMServerless, + credentials: cfg.Credentials, + } + + return func(ctx context.Context) (string, string, error) { + token, err := gen.Generate(ctx) + if err != nil { + return "", "", err + } + return opts.IAMUserID, token, nil + }, nil +} + var _ persistence.Store = (*SessionStore)(nil) diff --git a/pkg/validation/sessions.go b/pkg/validation/sessions.go index 96ea6d4f..dbd2988e 100644 --- a/pkg/validation/sessions.go +++ b/pkg/validation/sessions.go @@ -39,6 +39,29 @@ func validateSessionCookieMinimal(o *options.Options) []string { return msgs } +// validateRedisIAMAuth checks that IAM auth options are consistent +func validateRedisIAMAuth(o *options.Options) []string { + r := o.Session.Redis + if !r.UseIAMAuth { + return []string{} + } + + var msgs []string + if r.IAMUserID == "" { + msgs = append(msgs, "--redis-iam-user-id must be set when using --redis-use-iam-auth") + } + if r.IAMReplicationGroupID == "" { + msgs = append(msgs, "--redis-iam-replication-group-id must be set when using --redis-use-iam-auth") + } + if r.Password != "" { + msgs = append(msgs, "--redis-password and --redis-use-iam-auth are mutually exclusive") + } + if r.Username != "" { + msgs = append(msgs, "--redis-username and --redis-use-iam-auth are mutually exclusive; IAM auth provides its own username") + } + return msgs +} + // validateRedisSessionStore builds a Redis Client from the options and // attempts to connect, Set, Get and Del a random health check key func validateRedisSessionStore(o *options.Options) []string { @@ -46,6 +69,10 @@ func validateRedisSessionStore(o *options.Options) []string { return []string{} } + if msgs := validateRedisIAMAuth(o); len(msgs) > 0 { + return msgs + } + client, err := redis.NewRedisClient(o.Session.Redis) if err != nil { return []string{fmt.Sprintf("unable to initialize a redis client: %v", err)} diff --git a/pkg/validation/sessions_test.go b/pkg/validation/sessions_test.go index cb54c571..5778cd42 100644 --- a/pkg/validation/sessions_test.go +++ b/pkg/validation/sessions_test.go @@ -209,6 +209,91 @@ var _ = Describe("Sessions", func() { errStrings []string } + type iamAuthTableInput struct { + opts *options.Options + errStrings []string + } + + DescribeTable("validateRedisIAMAuth", + func(o *iamAuthTableInput) { + Expect(validateRedisIAMAuth(o.opts)).To(ConsistOf(o.errStrings)) + }, + Entry("IAM auth disabled is valid", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + }, + }, + errStrings: []string{}, + }), + Entry("IAM auth missing user ID", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + Redis: options.RedisStoreOptions{ + UseIAMAuth: true, + IAMReplicationGroupID: "my-cluster", + }, + }, + }, + errStrings: []string{"--redis-iam-user-id must be set when using --redis-use-iam-auth"}, + }), + Entry("IAM auth missing replication group ID", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + Redis: options.RedisStoreOptions{ + UseIAMAuth: true, + IAMUserID: "my-user", + }, + }, + }, + errStrings: []string{"--redis-iam-replication-group-id must be set when using --redis-use-iam-auth"}, + }), + Entry("IAM auth conflicts with username", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + Redis: options.RedisStoreOptions{ + UseIAMAuth: true, + IAMUserID: "my-user", + IAMReplicationGroupID: "my-cluster", + Username: "some-user", + }, + }, + }, + errStrings: []string{"--redis-username and --redis-use-iam-auth are mutually exclusive; IAM auth provides its own username"}, + }), + Entry("IAM auth conflicts with password", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + Redis: options.RedisStoreOptions{ + UseIAMAuth: true, + IAMUserID: "my-user", + IAMReplicationGroupID: "my-cluster", + Password: "secret", + }, + }, + }, + errStrings: []string{"--redis-password and --redis-use-iam-auth are mutually exclusive"}, + }), + Entry("IAM auth valid config", &iamAuthTableInput{ + opts: &options.Options{ + Session: options.SessionOptions{ + Type: options.RedisSessionStoreType, + Redis: options.RedisStoreOptions{ + UseIAMAuth: true, + IAMUserID: "my-user", + IAMReplicationGroupID: "my-cluster", + IAMRegion: "us-east-1", + }, + }, + }, + errStrings: []string{}, + }), + ) + DescribeTable("validateRedisSessionStore", func(o *redisStoreTableInput) { mr, err := miniredis.Run()