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)
+       })
+}

Reply via email to