-
Notifications
You must be signed in to change notification settings - Fork 112
FFI/GO: Add IAM authentication support with automatic token refresh #4892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -34,18 +34,86 @@ func (addr *NodeAddress) toProtobuf() *protobuf.NodeAddress { | |||||||||||||||||||||||||
return &protobuf.NodeAddress{Host: addr.Host, Port: uint32(addr.Port)} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// ServiceType represents the types of AWS services that can be used for IAM authentication. | ||||||||||||||||||||||||||
type ServiceType int | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const ( | ||||||||||||||||||||||||||
// Elasticache represents Amazon ElastiCache service. | ||||||||||||||||||||||||||
Elasticache ServiceType = iota | ||||||||||||||||||||||||||
// MemoryDB represents Amazon MemoryDB service. | ||||||||||||||||||||||||||
MemoryDB | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
Comment on lines
+40
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we follow the existing CamelCase convention? Or, if you think it is better to match now Amazon names then, then should it just be "ElastiCache"?
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// IamAuthConfig represents configuration settings for IAM authentication. | ||||||||||||||||||||||||||
type IamAuthConfig struct { | ||||||||||||||||||||||||||
// The name of the ElastiCache/MemoryDB cluster. | ||||||||||||||||||||||||||
clusterName string | ||||||||||||||||||||||||||
// The type of service being used (ElastiCache or MemoryDB). | ||||||||||||||||||||||||||
service ServiceType | ||||||||||||||||||||||||||
// The AWS region where the ElastiCache/MemoryDB cluster is located. | ||||||||||||||||||||||||||
region string | ||||||||||||||||||||||||||
// Optional refresh interval in seconds for renewing IAM authentication tokens. | ||||||||||||||||||||||||||
// If not provided, defaults to 300 seconds (5 min). | ||||||||||||||||||||||||||
refreshIntervalSeconds *uint32 | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the reason for making |
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// NewIamAuthConfig returns an [IamAuthConfig] struct with the given configuration. | ||||||||||||||||||||||||||
func NewIamAuthConfig(clusterName string, service ServiceType, region string) *IamAuthConfig { | ||||||||||||||||||||||||||
defaultRefresh := uint32(300) | ||||||||||||||||||||||||||
return &IamAuthConfig{ | ||||||||||||||||||||||||||
clusterName: clusterName, | ||||||||||||||||||||||||||
service: service, | ||||||||||||||||||||||||||
region: region, | ||||||||||||||||||||||||||
refreshIntervalSeconds: &defaultRefresh, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// WithRefreshIntervalSeconds sets the refresh interval in seconds for IAM token renewal. | ||||||||||||||||||||||||||
func (config *IamAuthConfig) WithRefreshIntervalSeconds(seconds uint32) *IamAuthConfig { | ||||||||||||||||||||||||||
config.refreshIntervalSeconds = &seconds | ||||||||||||||||||||||||||
return config | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func (config *IamAuthConfig) toProtobuf() *protobuf.IamCredentials { | ||||||||||||||||||||||||||
iamCreds := &protobuf.IamCredentials{ | ||||||||||||||||||||||||||
ClusterName: config.clusterName, | ||||||||||||||||||||||||||
Region: config.region, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if config.service == Elasticache { | ||||||||||||||||||||||||||
iamCreds.ServiceType = protobuf.ServiceType_ELASTICACHE | ||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||
iamCreds.ServiceType = protobuf.ServiceType_MEMORYDB | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if config.refreshIntervalSeconds != nil { | ||||||||||||||||||||||||||
iamCreds.RefreshIntervalSeconds = config.refreshIntervalSeconds | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return iamCreds | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// ServerCredentials represents the credentials for connecting to servers. | ||||||||||||||||||||||||||
// Supports two authentication modes: | ||||||||||||||||||||||||||
// - Password-based authentication: Use username and password | ||||||||||||||||||||||||||
// - IAM authentication: Use username (required) and iamConfig | ||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||
// These modes are mutually exclusive. | ||||||||||||||||||||||||||
type ServerCredentials struct { | ||||||||||||||||||||||||||
// The username that will be used for authenticating connections to the servers. If not supplied, "default" | ||||||||||||||||||||||||||
// will be used. | ||||||||||||||||||||||||||
// will be used for password-based authentication. Required for IAM authentication. | ||||||||||||||||||||||||||
username string | ||||||||||||||||||||||||||
// The password that will be used for authenticating connections to the servers. | ||||||||||||||||||||||||||
// Mutually exclusive with iamConfig. | ||||||||||||||||||||||||||
password string | ||||||||||||||||||||||||||
// IAM authentication configuration. Mutually exclusive with password. | ||||||||||||||||||||||||||
// The client will automatically generate and refresh the authentication token based on the provided configuration. | ||||||||||||||||||||||||||
iamConfig *IamAuthConfig | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// NewServerCredentials returns a [ServerCredentials] struct with the given username and password. | ||||||||||||||||||||||||||
func NewServerCredentials(username string, password string) *ServerCredentials { | ||||||||||||||||||||||||||
return &ServerCredentials{username, password} | ||||||||||||||||||||||||||
return &ServerCredentials{username: username, password: password} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// NewServerCredentialsWithDefaultUsername returns a [ServerCredentials] struct with a default username of "default" and the | ||||||||||||||||||||||||||
|
@@ -54,8 +122,34 @@ func NewServerCredentialsWithDefaultUsername(password string) *ServerCredentials | |||||||||||||||||||||||||
return &ServerCredentials{password: password} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// NewServerCredentialsWithIam returns a [ServerCredentials] struct configured for IAM authentication. | ||||||||||||||||||||||||||
// The username is required for IAM authentication. | ||||||||||||||||||||||||||
func NewServerCredentialsWithIam(username string, iamConfig *IamAuthConfig) (*ServerCredentials, error) { | ||||||||||||||||||||||||||
if username == "" { | ||||||||||||||||||||||||||
return nil, errors.New("username is required for IAM authentication") | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
if iamConfig == nil { | ||||||||||||||||||||||||||
return nil, errors.New("iamConfig cannot be nil") | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return &ServerCredentials{username: username, iamConfig: iamConfig}, nil | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func (creds *ServerCredentials) toProtobuf() *protobuf.AuthenticationInfo { | ||||||||||||||||||||||||||
return &protobuf.AuthenticationInfo{Username: creds.username, Password: creds.password} | ||||||||||||||||||||||||||
authInfo := &protobuf.AuthenticationInfo{ | ||||||||||||||||||||||||||
Username: creds.username, | ||||||||||||||||||||||||||
Password: creds.password, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if creds.iamConfig != nil { | ||||||||||||||||||||||||||
authInfo.IamCredentials = creds.iamConfig.toProtobuf() | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return authInfo | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// IsIamAuth returns true if this credential is configured for IAM authentication. | ||||||||||||||||||||||||||
func (creds *ServerCredentials) IsIamAuth() bool { | ||||||||||||||||||||||||||
return creds.iamConfig != nil | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// ReadFrom represents the client's read from strategy. | ||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -197,15 +197,60 @@ func TestServerCredentials(t *testing.T) { | |
}, | ||
} | ||
|
||
for i, parameter := range parameters { | ||
t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This creates a subtest. This allows you to treat each iteration as a separate test and documents each iteration in the output. Why remove it? |
||
result := parameter.input.toProtobuf() | ||
|
||
assert.Equal(t, parameter.expected, result) | ||
}) | ||
for _, param := range parameters { | ||
result := param.input.toProtobuf() | ||
assert.Equal(t, param.expected, result) | ||
} | ||
} | ||
|
||
func TestServerCredentialsWithIam(t *testing.T) { | ||
iamConfig := NewIamAuthConfig("my-cluster", Elasticache, "us-east-1") | ||
creds, err := NewServerCredentialsWithIam("myUser", iamConfig) | ||
|
||
assert.Nil(t, err) | ||
assert.NotNil(t, creds) | ||
assert.True(t, creds.IsIamAuth()) | ||
|
||
authInfo := creds.toProtobuf() | ||
assert.Equal(t, "myUser", authInfo.Username) | ||
assert.Equal(t, "", authInfo.Password) | ||
assert.NotNil(t, authInfo.IamCredentials) | ||
assert.Equal(t, "my-cluster", authInfo.IamCredentials.ClusterName) | ||
assert.Equal(t, "us-east-1", authInfo.IamCredentials.Region) | ||
assert.Equal(t, protobuf.ServiceType_ELASTICACHE, authInfo.IamCredentials.ServiceType) | ||
assert.Equal(t, uint32(300), *authInfo.IamCredentials.RefreshIntervalSeconds) | ||
} | ||
|
||
func TestServerCredentialsWithIamCustomRefresh(t *testing.T) { | ||
iamConfig := NewIamAuthConfig("my-cluster", MemoryDB, "us-west-2"). | ||
WithRefreshIntervalSeconds(600) | ||
creds, err := NewServerCredentialsWithIam("myUser", iamConfig) | ||
|
||
assert.Nil(t, err) | ||
assert.NotNil(t, creds) | ||
|
||
authInfo := creds.toProtobuf() | ||
assert.Equal(t, protobuf.ServiceType_MEMORYDB, authInfo.IamCredentials.ServiceType) | ||
assert.Equal(t, uint32(600), *authInfo.IamCredentials.RefreshIntervalSeconds) | ||
} | ||
|
||
func TestServerCredentialsWithIamRequiresUsername(t *testing.T) { | ||
iamConfig := NewIamAuthConfig("my-cluster", Elasticache, "us-east-1") | ||
creds, err := NewServerCredentialsWithIam("", iamConfig) | ||
|
||
assert.NotNil(t, err) | ||
assert.Nil(t, creds) | ||
assert.Contains(t, err.Error(), "username is required") | ||
} | ||
|
||
func TestServerCredentialsWithIamRequiresConfig(t *testing.T) { | ||
creds, err := NewServerCredentialsWithIam("myUser", nil) | ||
|
||
assert.NotNil(t, err) | ||
assert.Nil(t, creds) | ||
assert.Contains(t, err.Error(), "iamConfig cannot be nil") | ||
} | ||
|
||
func TestConfig_AzAffinity(t *testing.T) { | ||
hosts := []string{"host1", "host2"} | ||
ports := []int{1234, 5678} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs documentation