This is an automated email from the ASF dual-hosted git repository.
git-hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new b0f3dd49b fix(command): require admin permission for privileged
commands (#3570)
b0f3dd49b is described below
commit b0f3dd49b02f5e64488f19c973b9bcde0c0bdb66
Author: hulk <[email protected]>
AuthorDate: Sat Aug 1 13:32:22 2026 +0800
fix(command): require admin permission for privileged commands (#3570)
DEBUG, FLUSHMEMTABLE, FLUSHBLOCKCACHE, and _db_name operate on global
or cross-namespace state (the shared block cache, all column families,
the process-wide db-size-limit flag, and the configured db name), yet
their command attributes omitted the `admin` flag. A non-admin client
authenticated with a namespace token could invoke them and affect
every namespace on the server.
Add the `admin` flag so these commands are rejected for non-admin
connections, matching Redis (which gates DEBUG behind admin) and
completing the hardening started for APPLYBATCH in #3458.
This is a breaking change from the user side: clients authenticated
with a namespace token can no longer run these commands, so we should
highlight it in the release notes.
Assistant-By Claude Opus 4.8
---
src/commands/cmd_replication.cc | 2 +-
src/commands/cmd_server.cc | 6 ++--
tests/gocase/unit/debug/debug_test.go | 29 +++++++++++++++++
.../unit/flushblockcache/flushblockcache_test.go | 25 +++++++++++++++
.../unit/flushmemtable/flushmemtable_test.go | 26 ++++++++++++++++
tests/gocase/unit/server/poll_updates_test.go | 36 ++++++++++++++++++++++
6 files changed, 120 insertions(+), 4 deletions(-)
diff --git a/src/commands/cmd_replication.cc b/src/commands/cmd_replication.cc
index 9f15ed1e4..791a17919 100644
--- a/src/commands/cmd_replication.cc
+++ b/src/commands/cmd_replication.cc
@@ -486,7 +486,7 @@ REDIS_REGISTER_COMMANDS(Replication,
MakeCmdAttr<CommandReplConf>("replconf", -3
MakeCmdAttr<CommandPSync>("psync", -2, "read-only
no-multi no-script admin", NO_KEY),
MakeCmdAttr<CommandFetchMeta>("_fetch_meta", 1,
"read-only no-multi no-script admin", NO_KEY),
MakeCmdAttr<CommandFetchFile>("_fetch_file", 2,
"read-only no-multi no-script admin", NO_KEY),
- MakeCmdAttr<CommandDBName>("_db_name", 1, "read-only
no-multi", NO_KEY),
+ MakeCmdAttr<CommandDBName>("_db_name", 1, "read-only
no-multi admin", NO_KEY),
MakeCmdAttr<CommandWait>("wait", 3, "read-only
no-multi no-script blocking", NO_KEY), )
} // namespace redis
diff --git a/src/commands/cmd_server.cc b/src/commands/cmd_server.cc
index be5ff20a2..b57382b53 100644
--- a/src/commands/cmd_server.cc
+++ b/src/commands/cmd_server.cc
@@ -1800,7 +1800,7 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandShutdown>("shutdown", 1, "read-only exclusive no-multi
no-script admin", NO_KEY),
MakeCmdAttr<CommandQuit>("quit", 1, "read-only", NO_KEY),
MakeCmdAttr<CommandScan>("scan", -2, "read-only", NO_KEY),
MakeCmdAttr<CommandRandomKey>("randomkey", 1, "read-only", NO_KEY),
- MakeCmdAttr<CommandDebug>("debug", -2, "read-only exclusive", NO_KEY,
CommandDebug::FlagGen),
+ MakeCmdAttr<CommandDebug>("debug", -2, "read-only exclusive admin",
NO_KEY, CommandDebug::FlagGen),
MakeCmdAttr<CommandCommand>("command", -1, "read-only", NO_KEY),
MakeCmdAttr<CommandEcho>("echo", 2, "read-only", NO_KEY),
MakeCmdAttr<CommandTime>("time", 1, "read-only ok-loading", NO_KEY),
@@ -1822,7 +1822,7 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandDump>("dump", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandPollUpdates>("pollupdates", -2, "read-only admin",
NO_KEY),
MakeCmdAttr<CommandSST>("sst", -3, "write exclusive admin", 1, 1, 1),
- MakeCmdAttr<CommandFlushMemTable>("flushmemtable", -1, "exclusive write",
NO_KEY),
- MakeCmdAttr<CommandFlushBlockCache>("flushblockcache", 1, "exclusive
write", NO_KEY),
+ MakeCmdAttr<CommandFlushMemTable>("flushmemtable", -1, "exclusive write
admin", NO_KEY),
+ MakeCmdAttr<CommandFlushBlockCache>("flushblockcache", 1, "exclusive write
admin", NO_KEY),
MakeCmdAttr<CommandLatency>("latency", -2, "read-only admin", NO_KEY), )
} // namespace redis
diff --git a/tests/gocase/unit/debug/debug_test.go
b/tests/gocase/unit/debug/debug_test.go
index c15fd33dc..731461807 100644
--- a/tests/gocase/unit/debug/debug_test.go
+++ b/tests/gocase/unit/debug/debug_test.go
@@ -143,3 +143,32 @@ func TestDebugDBSizeLimit(t *testing.T) {
require.NoError(t, r.Err())
})
}
+
+func TestDebugAdminPermission(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{
+ "requirepass": "admin",
+ })
+ defer srv.Close()
+
+ ctx := context.Background()
+
+ adminClient := srv.NewClientWithOption(&redis.Options{Password:
"admin"})
+ defer func() { require.NoError(t, adminClient.Close()) }()
+
+ require.NoError(t, adminClient.Do(ctx, "NAMESPACE", "ADD", "test_ns",
"test_token").Err())
+
+ userClient := srv.NewClientWithOption(&redis.Options{Password:
"test_token"})
+ defer func() { require.NoError(t, userClient.Close()) }()
+
+ t.Run("Non-admin user should be rejected", func(t *testing.T) {
+ require.ErrorContains(t, userClient.Do(ctx, "DEBUG", "SLEEP",
"0").Err(), "admin")
+ require.ErrorContains(t, userClient.Do(ctx, "DEBUG",
"DBSIZE-LIMIT", "1").Err(), "admin")
+ require.ErrorContains(t, userClient.Do(ctx, "DEBUG",
"PROTOCOL", "string").Err(), "admin")
+ })
+
+ t.Run("Admin user should be allowed", func(t *testing.T) {
+ require.NoError(t, adminClient.Do(ctx, "DEBUG", "SLEEP",
"0").Err())
+ require.NoError(t, adminClient.Do(ctx, "DEBUG", "DBSIZE-LIMIT",
"0").Err())
+ require.NoError(t, adminClient.Do(ctx, "DEBUG", "PROTOCOL",
"string").Err())
+ })
+}
diff --git a/tests/gocase/unit/flushblockcache/flushblockcache_test.go
b/tests/gocase/unit/flushblockcache/flushblockcache_test.go
index 85c8b7fcc..a7d726b8a 100644
--- a/tests/gocase/unit/flushblockcache/flushblockcache_test.go
+++ b/tests/gocase/unit/flushblockcache/flushblockcache_test.go
@@ -80,3 +80,28 @@ func TestFlushBlockCacheInvalid(t *testing.T) {
require.Contains(t, err.Error(), "wrong number of arguments")
})
}
+
+func TestFlushBlockCacheAdminPermission(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{
+ "requirepass": "admin",
+ })
+ defer srv.Close()
+
+ ctx := context.Background()
+
+ adminClient := srv.NewClientWithOption(&redis.Options{Password:
"admin"})
+ defer func() { require.NoError(t, adminClient.Close()) }()
+
+ require.NoError(t, adminClient.Do(ctx, "NAMESPACE", "ADD", "test_ns",
"test_token").Err())
+
+ userClient := srv.NewClientWithOption(&redis.Options{Password:
"test_token"})
+ defer func() { require.NoError(t, userClient.Close()) }()
+
+ t.Run("Non-admin user should be rejected", func(t *testing.T) {
+ require.ErrorContains(t, userClient.Do(ctx,
"FLUSHBLOCKCACHE").Err(), "admin")
+ })
+
+ t.Run("Admin user should be allowed", func(t *testing.T) {
+ require.NoError(t, adminClient.Do(ctx, "FLUSHBLOCKCACHE").Err())
+ })
+}
diff --git a/tests/gocase/unit/flushmemtable/flushmemtable_test.go
b/tests/gocase/unit/flushmemtable/flushmemtable_test.go
index c39a8720e..9fb88c90a 100644
--- a/tests/gocase/unit/flushmemtable/flushmemtable_test.go
+++ b/tests/gocase/unit/flushmemtable/flushmemtable_test.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/apache/kvrocks/tests/gocase/util"
+ "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
@@ -104,3 +105,28 @@ func TestFlushMemTableInvalid(t *testing.T) {
require.Contains(t, err.Error(), "parameter must be 'ASYNC'")
})
}
+
+func TestFlushMemTableAdminPermission(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{
+ "requirepass": "admin",
+ })
+ defer srv.Close()
+
+ ctx := context.Background()
+
+ adminClient := srv.NewClientWithOption(&redis.Options{Password:
"admin"})
+ defer func() { require.NoError(t, adminClient.Close()) }()
+
+ require.NoError(t, adminClient.Do(ctx, "NAMESPACE", "ADD", "test_ns",
"test_token").Err())
+
+ userClient := srv.NewClientWithOption(&redis.Options{Password:
"test_token"})
+ defer func() { require.NoError(t, userClient.Close()) }()
+
+ t.Run("Non-admin user should be rejected", func(t *testing.T) {
+ require.ErrorContains(t, userClient.Do(ctx,
"FLUSHMEMTABLE").Err(), "admin")
+ })
+
+ t.Run("Admin user should be allowed", func(t *testing.T) {
+ require.NoError(t, adminClient.Do(ctx, "FLUSHMEMTABLE").Err())
+ })
+}
diff --git a/tests/gocase/unit/server/poll_updates_test.go
b/tests/gocase/unit/server/poll_updates_test.go
index 4d76499f9..9a607e202 100644
--- a/tests/gocase/unit/server/poll_updates_test.go
+++ b/tests/gocase/unit/server/poll_updates_test.go
@@ -349,3 +349,39 @@ func TestPollUpdates_WithStrict(t *testing.T) {
require.Equal(t, "v0", rdb1.Get(ctx, "k0").Val())
require.Equal(t, "v0", rdb1.HGet(ctx, "h0", "f0").Val())
}
+
+func TestDBNameAdminPermission(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{
+ "requirepass": "admin",
+ })
+ defer srv.Close()
+
+ ctx := context.Background()
+
+ adminClient := srv.NewClientWithOption(&redis.Options{Password:
"admin"})
+ defer func() { require.NoError(t, adminClient.Close()) }()
+
+ require.NoError(t, adminClient.Do(ctx, "NAMESPACE", "ADD", "test_ns",
"test_token").Err())
+
+ // _db_name replies with a raw inline string rather than a typed RESP
reply,
+ // so we use a TCP client to read the response directly.
+ t.Run("Non-admin user should be rejected", func(t *testing.T) {
+ c := srv.NewTCPClient()
+ defer func() { require.NoError(t, c.Close()) }()
+ require.NoError(t, c.WriteArgs("AUTH", "test_token"))
+ c.MustRead(t, "+OK")
+ require.NoError(t, c.WriteArgs("_db_name"))
+ c.MustMatch(t, ".*admin.*")
+ })
+
+ t.Run("Admin user should be allowed", func(t *testing.T) {
+ c := srv.NewTCPClient()
+ defer func() { require.NoError(t, c.Close()) }()
+ require.NoError(t, c.WriteArgs("AUTH", "admin"))
+ c.MustRead(t, "+OK")
+ require.NoError(t, c.WriteArgs("_db_name"))
+ line, err := c.ReadLine()
+ require.NoError(t, err)
+ require.NotEmpty(t, line)
+ })
+}