Skip to content

Commit 8300839

Browse files
authored
Go: Add support for updating connection password (#3346)
* Go: Add support for updating connection password Signed-off-by: TJ Zhang <[email protected]>
1 parent f8da0c9 commit 8300839

7 files changed

+433
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Go: Add `GeoAdd` and the Geospatial interface ([#3366](https://github.com/valkey-io/valkey-glide/pull/3366))
2020
* Go: Add `FLUSHALL` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
2121
* Go: Add `FLUSHDB` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
22+
* Go: Add password update api ([#3346](https://github.com/valkey-io/valkey-glide/pull/3346))
2223

2324
#### Breaking Changes
2425

go/api/base_client.go

+88
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,94 @@ func toCStrings(args []string) ([]C.uintptr_t, []C.ulong) {
298298
return cStrings, stringLengths
299299
}
300300

301+
func (client *baseClient) submitConnectionPasswordUpdate(password string, immediateAuth bool) (Result[string], error) {
302+
// Create a channel to receive the result
303+
resultChannel := make(chan payload, 1)
304+
resultChannelPtr := unsafe.Pointer(&resultChannel)
305+
306+
pinner := pinner{}
307+
pinnedChannelPtr := uintptr(pinner.Pin(resultChannelPtr))
308+
defer pinner.Unpin()
309+
310+
client.mu.Lock()
311+
if client.coreClient == nil {
312+
client.mu.Unlock()
313+
return CreateNilStringResult(), &errors.ClosingError{Msg: "UpdatePassword failed. The client is closed."}
314+
}
315+
client.pending[resultChannelPtr] = struct{}{}
316+
317+
C.update_connection_password(
318+
client.coreClient,
319+
C.uintptr_t(pinnedChannelPtr),
320+
C.CString(password),
321+
C._Bool(immediateAuth),
322+
)
323+
client.mu.Unlock()
324+
325+
// Wait for response
326+
payload := <-resultChannel
327+
328+
client.mu.Lock()
329+
if client.pending != nil {
330+
delete(client.pending, resultChannelPtr)
331+
}
332+
client.mu.Unlock()
333+
334+
if payload.error != nil {
335+
return CreateNilStringResult(), payload.error
336+
}
337+
338+
return handleStringOrNilResponse(payload.value)
339+
}
340+
341+
// Update the current connection with a new password.
342+
//
343+
// This method is useful in scenarios where the server password has changed or when utilizing
344+
// short-lived passwords for enhanced security. It allows the client to update its password to
345+
// reconnect upon disconnection without the need to recreate the client instance. This ensures
346+
// that the internal reconnection mechanism can handle reconnection seamlessly, preventing the
347+
// loss of in-flight commands.
348+
//
349+
// Note:
350+
//
351+
// This method updates the client's internal password configuration and does not perform
352+
// password rotation on the server side.
353+
//
354+
// Parameters:
355+
//
356+
// password - The new password to update the connection with.
357+
// immediateAuth - immediateAuth A boolean flag. If true, the client will
358+
// authenticate immediately with the new password against all connections, Using AUTH
359+
// command. If password supplied is an empty string, the client will not perform auth and a warning
360+
// will be returned. The default is `false`.
361+
//
362+
// Return value:
363+
//
364+
// `"OK"` response on success.
365+
func (client *baseClient) UpdateConnectionPassword(password string, immediateAuth bool) (Result[string], error) {
366+
return client.submitConnectionPasswordUpdate(password, immediateAuth)
367+
}
368+
369+
// Update the current connection by removing the password.
370+
//
371+
// This method is useful in scenarios where the server password has changed or when utilizing
372+
// short-lived passwords for enhanced security. It allows the client to update its password to
373+
// reconnect upon disconnection without the need to recreate the client instance. This ensures
374+
// that the internal reconnection mechanism can handle reconnection seamlessly, preventing the
375+
// loss of in-flight commands.
376+
//
377+
// Note:
378+
//
379+
// This method updates the client's internal password configuration and does not perform
380+
// password rotation on the server side.
381+
//
382+
// Return value:
383+
//
384+
// `"OK"` response on success.
385+
func (client *baseClient) ResetConnectionPassword() (Result[string], error) {
386+
return client.submitConnectionPasswordUpdate("", false)
387+
}
388+
301389
// Set the given key with the given value. The return value is a response from Valkey containing the string "OK".
302390
//
303391
// See [valkey.io] for details.

go/api/generic_base_commands.go

+4
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,8 @@ type GenericBaseCommands interface {
8181
Copy(source string, destination string) (bool, error)
8282

8383
CopyWithOptions(source string, destination string, option options.CopyOptions) (bool, error)
84+
85+
UpdateConnectionPassword(password string, immediateAuth bool) (Result[string], error)
86+
87+
ResetConnectionPassword() (Result[string], error)
8488
}
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
package api
4+
5+
import (
6+
"fmt"
7+
)
8+
9+
func ExampleGlideClient_UpdateConnectionPassword() {
10+
var client *GlideClient = getExampleGlideClient() // example helper function
11+
response, err := client.UpdateConnectionPassword("", false)
12+
if err != nil {
13+
fmt.Println("Glide example failed with an error: ", err)
14+
}
15+
fmt.Println(response.Value())
16+
17+
// Output: OK
18+
}
19+
20+
func ExampleGlideClient_ResetConnectionPassword() {
21+
var client *GlideClient = getExampleGlideClient() // example helper function
22+
response, err := client.ResetConnectionPassword()
23+
if err != nil {
24+
fmt.Println("Glide example failed with an error: ", err)
25+
}
26+
fmt.Println(response.Value())
27+
28+
// Output: OK
29+
}
30+
31+
func ExampleGlideClusterClient_UpdateConnectionPassword() {
32+
var client *GlideClusterClient = getExampleGlideClusterClient() // example helper function
33+
response, err := client.UpdateConnectionPassword("", false)
34+
if err != nil {
35+
fmt.Println("Glide example failed with an error: ", err)
36+
}
37+
fmt.Println(response.Value())
38+
39+
// Output: OK
40+
}
41+
42+
func ExampleGlideClusterClient_ResetConnectionPassword() {
43+
var client *GlideClusterClient = getExampleGlideClusterClient() // example helper function
44+
response, err := client.ResetConnectionPassword()
45+
if err != nil {
46+
fmt.Println("Glide example failed with an error: ", err)
47+
}
48+
fmt.Println(response.Value())
49+
50+
// Output: OK
51+
}

go/integTest/cluster_commands_test.go

+128
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package integTest
44

55
import (
6+
"math/rand"
67
"strings"
78

89
"github.com/google/uuid"
@@ -971,3 +972,130 @@ func (suite *GlideTestSuite) TestFlushDBWithOptions_AsyncMode() {
971972
assert.NoError(suite.T(), err)
972973
assert.Empty(suite.T(), val.Value())
973974
}
975+
976+
func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster() {
977+
suite.T().Skip("Skipping update connection password cluster test")
978+
// Create admin client
979+
adminClient := suite.defaultClusterClient()
980+
defer adminClient.Close()
981+
982+
// Create test client
983+
testClient := suite.defaultClusterClient()
984+
defer testClient.Close()
985+
986+
// Generate random password
987+
pwd := uuid.NewString()
988+
989+
// Validate that we can use the test client
990+
_, err := testClient.Info()
991+
assert.NoError(suite.T(), err)
992+
993+
// Update password without re-authentication
994+
_, err = testClient.UpdateConnectionPassword(pwd, false)
995+
assert.NoError(suite.T(), err)
996+
997+
// Verify client still works with old auth
998+
_, err = testClient.Info()
999+
assert.NoError(suite.T(), err)
1000+
1001+
// Update server password and kill all other clients to force reconnection
1002+
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", pwd})
1003+
assert.NoError(suite.T(), err)
1004+
1005+
_, err = adminClient.CustomCommand([]string{"CLIENT", "KILL", "TYPE", "NORMAL"})
1006+
assert.NoError(suite.T(), err)
1007+
1008+
// Verify client auto-reconnects with new password
1009+
_, err = testClient.Info()
1010+
assert.NoError(suite.T(), err)
1011+
1012+
// test reset connection password
1013+
_, err = testClient.ResetConnectionPassword()
1014+
assert.NoError(suite.T(), err)
1015+
1016+
// Cleanup: config set reset password
1017+
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", ""})
1018+
assert.NoError(suite.T(), err)
1019+
}
1020+
1021+
func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_InvalidParameters() {
1022+
// Create test client
1023+
testClient := suite.defaultClusterClient()
1024+
defer testClient.Close()
1025+
1026+
// Test empty password
1027+
_, err := testClient.UpdateConnectionPassword("", true)
1028+
assert.NotNil(suite.T(), err)
1029+
assert.IsType(suite.T(), &errors.RequestError{}, err)
1030+
}
1031+
1032+
func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_NoServerAuth() {
1033+
// Create test client
1034+
testClient := suite.defaultClusterClient()
1035+
defer testClient.Close()
1036+
1037+
// Validate that we can use the client
1038+
_, err := testClient.Info()
1039+
assert.NoError(suite.T(), err)
1040+
1041+
// Test immediate re-authentication fails when no server password is set
1042+
pwd := uuid.NewString()
1043+
_, err = testClient.UpdateConnectionPassword(pwd, true)
1044+
assert.NotNil(suite.T(), err)
1045+
assert.IsType(suite.T(), &errors.RequestError{}, err)
1046+
}
1047+
1048+
func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_LongPassword() {
1049+
// Create test client
1050+
testClient := suite.defaultClusterClient()
1051+
defer testClient.Close()
1052+
1053+
// Generate long random password (1000 chars)
1054+
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
1055+
pwd := make([]byte, 1000)
1056+
for i := range pwd {
1057+
pwd[i] = letters[rand.Intn(len(letters))]
1058+
}
1059+
1060+
// Validate that we can use the client
1061+
_, err := testClient.Info()
1062+
assert.NoError(suite.T(), err)
1063+
1064+
// Test replacing connection password with a long password string
1065+
_, err = testClient.UpdateConnectionPassword(string(pwd), false)
1066+
assert.NoError(suite.T(), err)
1067+
}
1068+
1069+
func (suite *GlideTestSuite) TestUpdateConnectionPasswordCluster_ImmediateAuthWrongPassword() {
1070+
// Create admin client
1071+
adminClient := suite.defaultClusterClient()
1072+
defer adminClient.Close()
1073+
1074+
// Create test client
1075+
testClient := suite.defaultClusterClient()
1076+
defer testClient.Close()
1077+
1078+
pwd := uuid.NewString()
1079+
notThePwd := uuid.NewString()
1080+
1081+
// Validate that we can use the client
1082+
_, err := testClient.Info()
1083+
assert.NoError(suite.T(), err)
1084+
1085+
// Set the password to something else
1086+
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", notThePwd})
1087+
assert.NoError(suite.T(), err)
1088+
1089+
// Test that re-authentication fails when using wrong password
1090+
_, err = testClient.UpdateConnectionPassword(pwd, true)
1091+
assert.NotNil(suite.T(), err)
1092+
assert.IsType(suite.T(), &errors.RequestError{}, err)
1093+
1094+
// But using correct password returns OK
1095+
_, err = testClient.UpdateConnectionPassword(notThePwd, true)
1096+
assert.NoError(suite.T(), err)
1097+
1098+
// Cleanup: Reset password
1099+
_, err = adminClient.CustomCommand([]string{"CONFIG", "SET", "requirepass", ""})
1100+
assert.NoError(suite.T(), err)
1101+
}

0 commit comments

Comments
 (0)