fix(redis): skip confirmation for regular writes
This commit is contained in:
1 parent
8c8c48f9b1
commit
39d956f2e8
20 files changed
+283
-84
No files matched your search
@@ -1391,7 +1391,7 @@ export interface RedisScanResult {
|
||||
total_keys: number;
|
||||
}
|
||||
|
||||
export type RedisCommandSafety = "allowed" | "confirm" | "blocked";
|
||||
export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked";
|
||||
|
||||
export interface RedisCommandResult {
|
||||
command: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type RedisCommandSafety = "allowed" | "confirm" | "blocked";
|
||||
export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked";
|
||||
|
||||
const BLOCKED_COMMANDS = new Set(["KEYS", "FLUSHALL", "SHUTDOWN", "CONFIG", "SAVE", "BGSAVE", "SLAVEOF", "REPLICAOF", "MIGRATE", "MODULE", "SCRIPT", "EVAL", "EVALSHA"]);
|
||||
|
||||
@@ -9,30 +9,84 @@ const CONFIRM_COMMANDS = new Set([
|
||||
"EXPIREAT",
|
||||
"PEXPIRE",
|
||||
"PEXPIREAT",
|
||||
"PERSIST",
|
||||
"RENAME",
|
||||
"RENAMENX",
|
||||
"GETDEL",
|
||||
"HDEL",
|
||||
"LPOP",
|
||||
"RPOP",
|
||||
"LREM",
|
||||
"LTRIM",
|
||||
"SPOP",
|
||||
"SREM",
|
||||
"ZREM",
|
||||
"ZPOPMAX",
|
||||
"ZPOPMIN",
|
||||
"ZMPOP",
|
||||
"ZREMRANGEBYLEX",
|
||||
"ZREMRANGEBYRANK",
|
||||
"ZREMRANGEBYSCORE",
|
||||
"XDEL",
|
||||
"XTRIM",
|
||||
"MOVE",
|
||||
"SORT",
|
||||
"SDIFFSTORE",
|
||||
"SINTERSTORE",
|
||||
"SUNIONSTORE",
|
||||
"ZDIFFSTORE",
|
||||
"ZINTERSTORE",
|
||||
"ZRANGESTORE",
|
||||
"ZUNIONSTORE",
|
||||
"PFMERGE",
|
||||
"GEOSEARCHSTORE",
|
||||
"FLUSHDB",
|
||||
]);
|
||||
|
||||
const WRITE_COMMANDS = new Set([
|
||||
"APPEND",
|
||||
"BITFIELD",
|
||||
"BITOP",
|
||||
"COPY",
|
||||
"DECR",
|
||||
"DECRBY",
|
||||
"GEOADD",
|
||||
"GEORADIUS",
|
||||
"GEORADIUSBYMEMBER",
|
||||
"GETSET",
|
||||
"INCR",
|
||||
"INCRBY",
|
||||
"INCRBYFLOAT",
|
||||
"SET",
|
||||
"SETEX",
|
||||
"PSETEX",
|
||||
"SETNX",
|
||||
"SETRANGE",
|
||||
"MSET",
|
||||
"MSETNX",
|
||||
"PERSIST",
|
||||
"HSET",
|
||||
"HDEL",
|
||||
"LPUSH",
|
||||
"RPUSH",
|
||||
"LPOP",
|
||||
"RPOP",
|
||||
"HMSET",
|
||||
"HINCRBY",
|
||||
"HINCRBYFLOAT",
|
||||
"HSETNX",
|
||||
"LINSERT",
|
||||
"LSET",
|
||||
"LREM",
|
||||
"LMOVE",
|
||||
"LPUSH",
|
||||
"LPUSHX",
|
||||
"PFADD",
|
||||
"RPUSH",
|
||||
"RPUSHX",
|
||||
"RESTORE",
|
||||
"SADD",
|
||||
"SREM",
|
||||
"ZADD",
|
||||
"ZREM",
|
||||
"ZINCRBY",
|
||||
"SETBIT",
|
||||
"XADD",
|
||||
"XDEL",
|
||||
"FLUSHDB",
|
||||
"XACK",
|
||||
"XAUTOCLAIM",
|
||||
"XCLAIM",
|
||||
"XSETID",
|
||||
]);
|
||||
|
||||
export function firstRedisCommandToken(command: string): string {
|
||||
@@ -64,5 +118,6 @@ export function classifyRedisCommandSafety(command: string): RedisCommandSafety
|
||||
const token = firstRedisCommandToken(command);
|
||||
if (BLOCKED_COMMANDS.has(token)) return "blocked";
|
||||
if (CONFIRM_COMMANDS.has(token)) return "confirm";
|
||||
if (WRITE_COMMANDS.has(token)) return "write";
|
||||
return "allowed";
|
||||
}
|
||||
@@ -2,15 +2,17 @@
|
||||
* Static Redis command metadata, distilled from the official Redis command table
|
||||
* (redis-doc `commands.json` / the `COMMAND` output). Kept offline and lightweight:
|
||||
* we only retain what the editor syntax diagnostics need — `arity` and `group` —
|
||||
* plus a `safety` hint for danger/write-command highlighting.
|
||||
* plus a `safety` hint for command gating and write-command highlighting.
|
||||
*
|
||||
* Arity semantics (matches the Redis `COMMAND` reply, command name INCLUDED in the count):
|
||||
* arity > 0 → the command takes exactly `arity` tokens (e.g. GET key → arity 2).
|
||||
* arity < 0 → the command takes AT LEAST `-arity` tokens (e.g. MSET k v [k v ...] → arity -3).
|
||||
*
|
||||
* `safety` is aligned with the token-level sets in `redisCommandSafety.ts` (used to gate
|
||||
* execution). Here it is recorded per-command (including subcommands) so diagnostics can
|
||||
* be more precise than the first-token classification.
|
||||
* execution). `write` commands mutate data without a confirmation prompt; `confirm`
|
||||
* commands are destructive/structural enough to ask first. Here it is recorded
|
||||
* per-command (including subcommands) so diagnostics can be more precise than the
|
||||
* first-token classification.
|
||||
*
|
||||
* Subcommands are keyed as `"MAIN SUB"` in UPPER CASE (e.g. `"CONFIG GET"`, `"XGROUP CREATE"`).
|
||||
*/
|
||||
@@ -27,6 +29,57 @@ export interface RedisCommandSpec {
|
||||
|
||||
type Spec = [arity: number, group: string, safety?: RedisCommandSafety];
|
||||
|
||||
const WRITE_WITHOUT_CONFIRM = new Set([
|
||||
"APPEND",
|
||||
"BITFIELD",
|
||||
"BITOP",
|
||||
"CLIENT SETINFO",
|
||||
"CLIENT SETNAME",
|
||||
"COPY",
|
||||
"DECR",
|
||||
"DECRBY",
|
||||
"GEOADD",
|
||||
"GEORADIUS",
|
||||
"GEORADIUSBYMEMBER",
|
||||
"GETSET",
|
||||
"HINCRBY",
|
||||
"HINCRBYFLOAT",
|
||||
"HMSET",
|
||||
"HSET",
|
||||
"HSETNX",
|
||||
"INCR",
|
||||
"INCRBY",
|
||||
"INCRBYFLOAT",
|
||||
"LINSERT",
|
||||
"LMOVE",
|
||||
"LPUSH",
|
||||
"LPUSHX",
|
||||
"MSET",
|
||||
"MSETNX",
|
||||
"PERSIST",
|
||||
"PFADD",
|
||||
"PSETEX",
|
||||
"RESTORE",
|
||||
"RPUSH",
|
||||
"RPUSHX",
|
||||
"SADD",
|
||||
"SET",
|
||||
"SETBIT",
|
||||
"SETEX",
|
||||
"SETNX",
|
||||
"SETRANGE",
|
||||
"XACK",
|
||||
"XADD",
|
||||
"XAUTOCLAIM",
|
||||
"XCLAIM",
|
||||
"XGROUP CREATE",
|
||||
"XGROUP CREATECONSUMER",
|
||||
"XGROUP SETID",
|
||||
"XSETID",
|
||||
"ZADD",
|
||||
"ZINCRBY",
|
||||
]);
|
||||
|
||||
// Compact tuple form → expanded into the record below.
|
||||
const RAW_COMMANDS: Record<string, Spec> = {
|
||||
// ---- String ----
|
||||
@@ -367,7 +420,12 @@ const RAW_COMMANDS: Record<string, Spec> = {
|
||||
READWRITE: [1, "cluster"],
|
||||
};
|
||||
|
||||
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: safety ?? "allowed" }]));
|
||||
function commandTableSafety(name: string, safety?: RedisCommandSafety): RedisCommandSafety {
|
||||
if (safety === "confirm" && WRITE_WITHOUT_CONFIRM.has(name.toUpperCase())) return "write";
|
||||
return safety ?? "allowed";
|
||||
}
|
||||
|
||||
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: commandTableSafety(name, safety) }]));
|
||||
|
||||
/**
|
||||
* Resolve a command spec. Handles two-token subcommands (e.g. `XGROUP CREATE`,
|
||||
@@ -442,7 +500,7 @@ const NON_MUTATING_BLOCKED = new Set(["KEYS", "BGSAVE", "SAVE", "SHUTDOWN", "REP
|
||||
* completion for that db is potentially stale and should be dropped.
|
||||
*
|
||||
* Mapping from the diagnostic `safety` field:
|
||||
* - `confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
|
||||
* - `write`/`confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
|
||||
* - `blocked` → mostly destructive/admin; we keep the ones that may touch keys
|
||||
* (FLUSHALL, MIGRATE, EVAL[ESHA]) and exclude the read-only/admin
|
||||
* ones in `NON_MUTATING_BLOCKED`.
|
||||
@@ -455,7 +513,7 @@ export function isRedisMutatingCommand(command: string): boolean {
|
||||
const argv = firstRedisArgvUpper(command);
|
||||
const spec = resolveRedisCommandSpec(argv);
|
||||
if (!spec) return false;
|
||||
if (spec.safety === "confirm") return true;
|
||||
if (spec.safety === "write" || spec.safety === "confirm") return true;
|
||||
if (spec.safety === "blocked") {
|
||||
return !NON_MUTATING_BLOCKED.has(argv[0]);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export function buildRedisSyntaxDiagnostics(source: string): SqlSemanticDiagnost
|
||||
} else if (spec.safety === "confirm") {
|
||||
diagnostics.push({
|
||||
span: lineSpan(lineNo, commandToken.startColumn, commandToken.endColumn),
|
||||
message: `Write command '${argv[0].value}' — will modify data`,
|
||||
message: `Dangerous command '${argv[0].value}' — confirmation recommended`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user