feat(mcp): centralize connection access policies

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-20 22:47:40 +08:00
1 parent 29f493f7da
commit d642d01eac
56 files changed
+6393 -617

No files matched your search

+178 -10
View File
@@ -1330,20 +1330,161 @@ pub fn parse_command_argv(command_text: &str) -> Result<Vec<String>, String> {
}
pub fn classify_command(command: &str) -> RedisCommandSafety {
// Read access is an explicit allowlist. Commands added by a newer Redis
// version, module, or proxy remain high-risk until DBX reviews them.
match command.to_ascii_uppercase().as_str() {
"KEYS" | "FLUSHALL" | "SHUTDOWN" | "CONFIG" | "SAVE" | "BGSAVE" | "SLAVEOF" | "REPLICAOF" | "MIGRATE"
| "MODULE" | "SCRIPT" | "EVAL" | "EVALSHA" => RedisCommandSafety::Blocked,
"BITCOUNT"
| "BITFIELD_RO"
| "BITPOS"
| "COMMAND"
| "DBSIZE"
| "DUMP"
| "ECHO"
| "EXISTS"
| "EXPIRETIME"
| "GEODIST"
| "GEOHASH"
| "GEOPOS"
| "GEORADIUS_RO"
| "GEORADIUSBYMEMBER_RO"
| "GEOSEARCH"
| "GET"
| "GETBIT"
| "GETRANGE"
| "HEXISTS"
| "HGET"
| "HGETALL"
| "HKEYS"
| "HLEN"
| "HMGET"
| "HRANDFIELD"
| "HSCAN"
| "HSTRLEN"
| "HVALS"
| "INFO"
| "LASTSAVE"
| "LCS"
| "LINDEX"
| "LLEN"
| "LPOS"
| "LRANGE"
| "MGET"
| "OBJECT"
| "PEXPIRETIME"
| "PFCOUNT"
| "PING"
| "PTTL"
| "PUBSUB"
| "RANDOMKEY"
| "ROLE"
| "SCAN"
| "SCARD"
| "SDIFF"
| "SINTER"
| "SINTERCARD"
| "SISMEMBER"
| "SMEMBERS"
| "SMISMEMBER"
| "SORT_RO"
| "SRANDMEMBER"
| "SSCAN"
| "STRLEN"
| "SUNION"
| "TIME"
| "TTL"
| "TYPE"
| "WAIT"
| "WAITAOF"
| "XINFO"
| "XLEN"
| "XPENDING"
| "XRANGE"
| "XREAD"
| "XREVRANGE"
| "ZCARD"
| "ZCOUNT"
| "ZDIFF"
| "ZINTER"
| "ZINTERCARD"
| "ZLEXCOUNT"
| "ZMSCORE"
| "ZRANDMEMBER"
| "ZRANGE"
| "ZRANGEBYLEX"
| "ZRANGEBYSCORE"
| "ZRANK"
| "ZREVRANGE"
| "ZREVRANGEBYLEX"
| "ZREVRANGEBYSCORE"
| "ZREVRANK"
| "ZSCAN"
| "ZSCORE"
| "ZUNION"
| "BF.CARD"
| "BF.EXISTS"
| "BF.INFO"
| "BF.MEXISTS"
| "CF.COUNT"
| "CF.EXISTS"
| "CF.INFO"
| "CF.MEXISTS"
| "CMS.INFO"
| "CMS.QUERY"
| "FT._LIST"
| "FT.AGGREGATE"
| "FT.DICTDUMP"
| "FT.EXPLAIN"
| "FT.EXPLAINCLI"
| "FT.INFO"
| "FT.PROFILE"
| "FT.SEARCH"
| "FT.SPELLCHECK"
| "FT.SYNDUMP"
| "FT.TAGVALS"
| "GRAPH.RO_QUERY"
| "JSON.ARRINDEX"
| "JSON.ARRLEN"
| "JSON.GET"
| "JSON.MGET"
| "JSON.OBJKEYS"
| "JSON.OBJLEN"
| "JSON.RESP"
| "JSON.STRLEN"
| "JSON.TYPE"
| "TDIGEST.BYRANK"
| "TDIGEST.BYREVRANK"
| "TDIGEST.CDF"
| "TDIGEST.INFO"
| "TDIGEST.MAX"
| "TDIGEST.MIN"
| "TDIGEST.QUANTILE"
| "TDIGEST.RANK"
| "TDIGEST.REVRANK"
| "TDIGEST.TRIMMED_MEAN"
| "TOPK.INFO"
| "TOPK.LIST"
| "TOPK.QUERY"
| "TS.GET"
| "TS.INFO"
| "TS.MGET"
| "TS.MRANGE"
| "TS.QUERYINDEX"
| "TS.RANGE" => RedisCommandSafety::Allowed,
"DEL" | "UNLINK" | "EXPIRE" | "EXPIREAT" | "PEXPIRE" | "PEXPIREAT" | "RENAME" | "RENAMENX" | "GETDEL"
| "HDEL" | "LPOP" | "RPOP" | "LREM" | "LTRIM" | "SPOP" | "SREM" | "ZREM" | "ZPOPMAX" | "ZPOPMIN" | "ZMPOP"
| "HDEL" | "JSON.ARRPOP" | "JSON.ARRTRIM" | "JSON.CLEAR" | "JSON.DEL" | "JSON.FORGET" | "BLMOVE" | "BLMPOP"
| "BLPOP" | "BRPOP" | "BRPOPLPUSH" | "LPOP" | "LMOVE" | "LMPOP" | "RPOP" | "RPOPLPUSH" | "LREM" | "LTRIM"
| "SPOP" | "SREM" | "ZREM" | "ZPOPMAX" | "ZPOPMIN" | "ZMPOP" | "BZMPOP" | "BZPOPMAX" | "BZPOPMIN"
| "ZREMRANGEBYLEX" | "ZREMRANGEBYRANK" | "ZREMRANGEBYSCORE" | "XDEL" | "XTRIM" | "MOVE" | "SORT"
| "SDIFFSTORE" | "SINTERSTORE" | "SUNIONSTORE" | "ZDIFFSTORE" | "ZINTERSTORE" | "ZRANGESTORE"
| "ZUNIONSTORE" | "PFMERGE" | "GEOSEARCHSTORE" | "FLUSHDB" => RedisCommandSafety::Confirm,
| "ZUNIONSTORE" | "PFMERGE" | "GEOSEARCHSTORE" => RedisCommandSafety::Confirm,
"APPEND" | "BITFIELD" | "BITOP" | "COPY" | "DECR" | "DECRBY" | "GEOADD" | "GEORADIUS" | "GEORADIUSBYMEMBER"
| "GETSET" | "INCR" | "INCRBY" | "INCRBYFLOAT" | "SET" | "SETEX" | "PSETEX" | "SETNX" | "SETRANGE" | "MSET"
| "MSETNX" | "PERSIST" | "HSET" | "HMSET" | "HINCRBY" | "HINCRBYFLOAT" | "HSETNX" | "LINSERT" | "LSET"
| "LMOVE" | "LPUSH" | "LPUSHX" | "PFADD" | "RPUSH" | "RPUSHX" | "RESTORE" | "SADD" | "ZADD" | "ZINCRBY"
| "SETBIT" | "XADD" | "XACK" | "XAUTOCLAIM" | "XCLAIM" | "XSETID" => RedisCommandSafety::Write,
_ => RedisCommandSafety::Allowed,
| "GETEX" | "GETSET" | "INCR" | "INCRBY" | "INCRBYFLOAT" | "SET" | "SETEX" | "PSETEX" | "SETNX"
| "SETRANGE" | "MSET" | "MSETNX" | "PERSIST" | "HSET" | "HMSET" | "HINCRBY" | "HINCRBYFLOAT" | "HSETNX"
| "JSON.ARRAPPEND" | "JSON.ARRINSERT" | "JSON.MERGE" | "JSON.MSET" | "JSON.NUMINCRBY" | "JSON.NUMMULTBY"
| "JSON.SET" | "JSON.STRAPPEND" | "JSON.TOGGLE" | "LINSERT" | "LSET" | "LPUSH" | "LPUSHX" | "PFADD"
| "RPUSH" | "RPUSHX" | "RESTORE" | "SADD" | "ZADD" | "ZINCRBY" | "SETBIT" | "SPUBLISH" | "PUBLISH"
| "TOUCH" | "XADD" | "XACK" | "XAUTOCLAIM" | "XCLAIM" | "XREADGROUP" | "XSETID" => RedisCommandSafety::Write,
_ => RedisCommandSafety::Blocked,
}
}
@@ -2971,11 +3112,31 @@ mod tests {
assert_eq!(argv, vec!["SET", "user:1", "Ada \"Lovelace\""]);
}
#[test]
fn parses_quoted_and_escaped_command_names_before_classification() {
for command_text in [r#""JSON.SET" user:1 $ {}"#, r#"JSON\.SET user:1 $ {}"#] {
let argv = parse_command_argv(command_text).unwrap();
assert_eq!(classify_command(&argv[0]), RedisCommandSafety::Write);
}
}
#[test]
fn rejects_empty_command_text() {
assert_eq!(parse_command_argv(" ").unwrap_err(), "Redis command is empty");
}
#[tokio::test]
async fn raw_command_execution_fails_closed_before_sending_invalid_or_unknown_commands() {
let mut con = FakeRedisConnection::new(Vec::new());
let unknown = super::execute_command(&mut con, "VENDOR.WRITE key value", false).await.unwrap_err();
assert!(unknown.contains("blocked for safety"));
let malformed = super::execute_command(&mut con, r#"GET "unterminated"#, true).await.unwrap_err();
assert_eq!(malformed, "Redis command has an unterminated quote");
assert!(con.commands.is_empty());
}
#[test]
fn matches_redis_values_case_insensitively() {
assert!(redis_value_matches_query(&string_value("Hello Redis"), "redis"));
@@ -3040,13 +3201,20 @@ mod tests {
#[test]
fn classifies_safe_confirmed_and_blocked_commands() {
assert_eq!(classify_command("GET"), RedisCommandSafety::Allowed);
assert_eq!(classify_command("JSON.GET"), RedisCommandSafety::Allowed);
assert_eq!(classify_command("set"), RedisCommandSafety::Write);
assert_eq!(classify_command("hset"), RedisCommandSafety::Write);
assert_eq!(classify_command("JSON.SET"), RedisCommandSafety::Write);
assert_eq!(classify_command("GETEX"), RedisCommandSafety::Write);
assert_eq!(classify_command("XREADGROUP"), RedisCommandSafety::Write);
assert_eq!(classify_command("del"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("flushdb"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("flushdb"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("KEYS"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("flushall"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("eval"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("FCALL"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("XGROUP"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("VENDOR.WRITE"), RedisCommandSafety::Blocked);
}
#[test]