Skip to content
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

Go: Add support for updating connection password #3346

Merged
merged 6 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Go: Add `GeoAdd` and the Geospatial interface ([#3366](https://github.com/valkey-io/valkey-glide/pull/3366))
* Go: Add `FLUSHALL` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
* Go: Add `FLUSHDB` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
* Go: Add password update api ([#3346](https://github.com/valkey-io/valkey-glide/pull/3346))

#### Breaking Changes

Expand Down
88 changes: 88 additions & 0 deletions go/api/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,94 @@ func toCStrings(args []string) ([]C.uintptr_t, []C.ulong) {
return cStrings, stringLengths
}

func (client *baseClient) submitConnectionPasswordUpdate(password string, immediateAuth bool) (Result[string], error) {
// Create a channel to receive the result
resultChannel := make(chan payload, 1)
resultChannelPtr := unsafe.Pointer(&resultChannel)

pinner := pinner{}
pinnedChannelPtr := uintptr(pinner.Pin(resultChannelPtr))
defer pinner.Unpin()

client.mu.Lock()
if client.coreClient == nil {
client.mu.Unlock()
return CreateNilStringResult(), &errors.ClosingError{Msg: "UpdatePassword failed. The client is closed."}
}
client.pending[resultChannelPtr] = struct{}{}

C.update_connection_password(
client.coreClient,
C.uintptr_t(pinnedChannelPtr),
C.CString(password),
C._Bool(immediateAuth),
)
client.mu.Unlock()

// Wait for response
payload := <-resultChannel

client.mu.Lock()
if client.pending != nil {
delete(client.pending, resultChannelPtr)
}
client.mu.Unlock()

if payload.error != nil {
return CreateNilStringResult(), payload.error
}

return handleStringOrNilResponse(payload.value)
}

// Update the current connection with a new password.
//
// This method is useful in scenarios where the server password has changed or when utilizing
// short-lived passwords for enhanced security. It allows the client to update its password to
// reconnect upon disconnection without the need to recreate the client instance. This ensures
// that the internal reconnection mechanism can handle reconnection seamlessly, preventing the
// loss of in-flight commands.
//
// Note:
//
// This method updates the client's internal password configuration and does not perform
// password rotation on the server side.
//
// Parameters:
//
// password - The new password to update the connection with.
// immediateAuth - immediateAuth A boolean flag. If true, the client will
// authenticate immediately with the new password against all connections, Using AUTH
// command. If password supplied is an empty string, the client will not perform auth and a warning
// will be returned. The default is `false`.
//
// Return value:
//
// `"OK"` response on success.
func (client *baseClient) UpdateConnectionPassword(password string, immediateAuth bool) (Result[string], error) {
return client.submitConnectionPasswordUpdate(password, immediateAuth)
}

// Update the current connection by removing the password.
//
// This method is useful in scenarios where the server password has changed or when utilizing
// short-lived passwords for enhanced security. It allows the client to update its password to
// reconnect upon disconnection without the need to recreate the client instance. This ensures
// that the internal reconnection mechanism can handle reconnection seamlessly, preventing the
// loss of in-flight commands.
//
// Note:
//
// This method updates the client's internal password configuration and does not perform
// password rotation on the server side.
//
// Return value:
//
// `"OK"` response on success.
func (client *baseClient) ResetConnectionPassword() (Result[string], error) {
return client.submitConnectionPasswordUpdate("", false)
}

// Set the given key with the given value. The return value is a response from Valkey containing the string "OK".
//
// See [valkey.io] for details.
Expand Down
4 changes: 4 additions & 0 deletions go/api/generic_base_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,8 @@ type GenericBaseCommands interface {
Copy(source string, destination string) (bool, error)

CopyWithOptions(source string, destination string, option options.CopyOptions) (bool, error)

UpdateConnectionPassword(password string, immediateAuth bool) (Result[string], error)

ResetConnectionPassword() (Result[string], error)
}
51 changes: 51 additions & 0 deletions go/api/update_connection_password_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0

package api

import (
"fmt"
)

func ExampleGlideClient_UpdateConnectionPassword() {
var client *GlideClient = getExampleGlideClient() // example helper function
response, err := client.UpdateConnectionPassword("", false)
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println(response.Value())

// Output: OK
}

func ExampleGlideClient_ResetConnectionPassword() {
var client *GlideClient = getExampleGlideClient() // example helper function
response, err := client.ResetConnectionPassword()
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println(response.Value())

// Output: OK
}

func ExampleGlideClusterClient_UpdateConnectionPassword() {
var client *GlideClusterClient = getExampleGlideClusterClient() // example helper function
response, err := client.UpdateConnectionPassword("", false)
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println(response.Value())

// Output: OK
}

func ExampleGlideClusterClient_ResetConnectionPassword() {
var client *GlideClusterClient = getExampleGlideClusterClient() // example helper function
response, err := client.ResetConnectionPassword()
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println(response.Value())

// Output: OK
}
128 changes: 128 additions & 0 deletions go/integTest/cluster_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package integTest

import (
"math/rand"
"strings"

"github.com/google/uuid"
Expand Down Expand Up @@ -971,3 +972,130 @@ func (suite *GlideTestSuite) TestFlushDBWithOptions_AsyncMode() {
assert.NoError(suite.T(), err)
assert.Empty(suite.T(), val.Value())
}

func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster() {
suite.T().Skip("Skipping update connection password cluster test")
// Create admin client
adminClient := suite.defaultClusterClient()
defer adminClient.Close()

// Create test client
testClient := suite.defaultClusterClient()
defer testClient.Close()

// Generate random password
pwd := uuid.NewString()

// Validate that we can use the test client
_, err := testClient.Info()
assert.NoError(suite.T(), err)

// Update password without re-authentication
_, err = testClient.UpdateConnectionPassword(pwd, false)
assert.NoError(suite.T(), err)

// Verify client still works with old auth
_, err = testClient.Info()
assert.NoError(suite.T(), err)

// Update server password and kill all other clients to force reconnection
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", pwd})
assert.NoError(suite.T(), err)

_, err = adminClient.CustomCommand([]string{"CLIENT", "KILL", "TYPE", "NORMAL"})
assert.NoError(suite.T(), err)

// Verify client auto-reconnects with new password
_, err = testClient.Info()
assert.NoError(suite.T(), err)

// test reset connection password
_, err = testClient.ResetConnectionPassword()
assert.NoError(suite.T(), err)

// Cleanup: config set reset password
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", ""})
assert.NoError(suite.T(), err)
}

func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_InvalidParameters() {
// Create test client
testClient := suite.defaultClusterClient()
defer testClient.Close()

// Test empty password
_, err := testClient.UpdateConnectionPassword("", true)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &errors.RequestError{}, err)
}

func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_NoServerAuth() {
// Create test client
testClient := suite.defaultClusterClient()
defer testClient.Close()

// Validate that we can use the client
_, err := testClient.Info()
assert.NoError(suite.T(), err)

// Test immediate re-authentication fails when no server password is set
pwd := uuid.NewString()
_, err = testClient.UpdateConnectionPassword(pwd, true)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &errors.RequestError{}, err)
}

func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_LongPassword() {
// Create test client
testClient := suite.defaultClusterClient()
defer testClient.Close()

// Generate long random password (1000 chars)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
pwd := make([]byte, 1000)
for i := range pwd {
pwd[i] = letters[rand.Intn(len(letters))]
}

// Validate that we can use the client
_, err := testClient.Info()
assert.NoError(suite.T(), err)

// Test replacing connection password with a long password string
_, err = testClient.UpdateConnectionPassword(string(pwd), false)
assert.NoError(suite.T(), err)
}

func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_ImmediateAuthWrongPassword() {
// Create admin client
adminClient := suite.defaultClusterClient()
defer adminClient.Close()

// Create test client
testClient := suite.defaultClusterClient()
defer testClient.Close()

pwd := uuid.NewString()
notThePwd := uuid.NewString()

// Validate that we can use the client
_, err := testClient.Info()
assert.NoError(suite.T(), err)

// Set the password to something else
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", notThePwd})
assert.NoError(suite.T(), err)

// Test that re-authentication fails when using wrong password
_, err = testClient.UpdateConnectionPassword(pwd, true)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &errors.RequestError{}, err)

// But using correct password returns OK
_, err = testClient.UpdateConnectionPassword(notThePwd, true)
assert.NoError(suite.T(), err)

// Cleanup: Reset password
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", ""})
assert.NoError(suite.T(), err)
}
Loading
Loading