mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-22 16:18:28 -05:00
3193f086ca
Motivation: for applications that use RM ACL verification functions, they would want to return errors back to the user, in ways that are consistent with Redis. While investigating how we should return ACL errors to the user, we realized that Redis isn't consistent, and currently returns ACL error strings in 3 primary ways. [For the actual implications of this change, see the "Impact" section at the bottom] 1. how it returns an error when calling a command normally ACL_DENIED_CMD -> "this user has no permissions to run the '%s' command" ACL_DENIED_KEY -> "this user has no permissions to access one of the keys used as arguments" ACL_DENIED_CHANNEL -> "this user has no permissions to access one of the channels used as arguments" 2. how it returns an error when calling via 'acl dryrun' command ACL_DENIED_CMD -> "This user has no permissions to run the '%s' command" ACL_DENIED_KEY -> "This user has no permissions to access the '%s' key" ACL_DENIED_CHANNEL -> "This user has no permissions to access the '%s' channel" 3. how it returns an error via RM_Call (and scripting is similar). ACL_DENIED_CMD -> "can't run this command or subcommand"; ACL_DENIED_KEY -> "can't access at least one of the keys mentioned in the command arguments"; ACL_DENIED_CHANNEL -> "can't publish to the channel mentioned in the command"; In addition, if one wants to use RM_Call's "dry run" capability instead of the RM ACL functions directly, one also sees a different problem than it returns ACL errors with a -ERR, not a -PERM, so it can't be returned directly to the caller. This PR modifies the code to generate a base message in a common manner with the ability to set verbose flag for acl dry run errors, and keep it unset for normal/rm_call/script cases ```c sds getAclErrorMessage(int acl_res, user *user, struct redisCommand *cmd, sds errored_val, int verbose) { switch (acl_res) { case ACL_DENIED_CMD: return sdscatfmt(sdsempty(), "User %S has no permissions to run " "the '%S' command", user->name, cmd->fullname); case ACL_DENIED_KEY: if (verbose) { return sdscatfmt(sdsempty(), "User %S has no permissions to access " "the '%S' key", user->name, errored_val); } else { return sdsnew("No permissions to access a key"); } case ACL_DENIED_CHANNEL: if (verbose) { return sdscatfmt(sdsempty(), "User %S has no permissions to access " "the '%S' channel", user->name, errored_val); } else { return sdsnew("No permissions to access a channel"); } } ``` The caller can append/prepend the message (adding NOPERM for normal/RM_Call or indicating it's within a script). Impact: - Plain commands, as well as scripts and RM_Call now include the user name. - ACL DRYRUN remains the only one that's verbose (mentions the offending channel or key name) - Changes RM_Call ACL errors from being a `-ERR` to being `-NOPERM` (besides for textual changes) **This somewhat a breaking change, but it only affects the RM_Call with both `C` and `E`, or `D`** - Changes ACL errors in scripts textually from being `The user executing the script <old non unified text>` to `ACL failure in script: <new unified text>`
161 lines
9.1 KiB
Tcl
161 lines
9.1 KiB
Tcl
set testmodule [file normalize tests/modules/keyspecs.so]
|
|
|
|
start_server {tags {"modules"}} {
|
|
r module load $testmodule
|
|
|
|
test "Module key specs: No spec, only legacy triple" {
|
|
set reply [lindex [r command info kspec.none] 0]
|
|
# Verify (first, last, step) and not movablekeys
|
|
assert_equal [lindex $reply 2] {module}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] -1
|
|
assert_equal [lindex $reply 5] 2
|
|
# Verify key-spec auto-generated from the legacy triple
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [llength $keyspecs] 1
|
|
assert_equal [lindex $keyspecs 0] {flags {RW access update} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey -1 keystep 2 limit 0}}}
|
|
assert_equal [r command getkeys kspec.none key1 val1 key2 val2] {key1 key2}
|
|
}
|
|
|
|
test "Module key specs: No spec, only legacy triple with getkeys-api" {
|
|
set reply [lindex [r command info kspec.nonewithgetkeys] 0]
|
|
# Verify (first, last, step) and movablekeys
|
|
assert_equal [lindex $reply 2] {module movablekeys}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] -1
|
|
assert_equal [lindex $reply 5] 2
|
|
# Verify key-spec auto-generated from the legacy triple
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [llength $keyspecs] 1
|
|
assert_equal [lindex $keyspecs 0] {flags {RW access update variable_flags} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey -1 keystep 2 limit 0}}}
|
|
assert_equal [r command getkeys kspec.nonewithgetkeys key1 val1 key2 val2] {key1 key2}
|
|
}
|
|
|
|
test "Module key specs: Two ranges" {
|
|
set reply [lindex [r command info kspec.tworanges] 0]
|
|
# Verify (first, last, step) and not movablekeys
|
|
assert_equal [lindex $reply 2] {module}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] 2
|
|
assert_equal [lindex $reply 5] 1
|
|
# Verify key-specs
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [lindex $keyspecs 0] {flags {RO access} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 1] {flags {RW update} begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [r command getkeys kspec.tworanges foo bar baz quux] {foo bar}
|
|
}
|
|
|
|
test "Module key specs: Two ranges with gap" {
|
|
set reply [lindex [r command info kspec.tworangeswithgap] 0]
|
|
# Verify (first, last, step) and movablekeys
|
|
assert_equal [lindex $reply 2] {module movablekeys}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] 1
|
|
assert_equal [lindex $reply 5] 1
|
|
# Verify key-specs
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [lindex $keyspecs 0] {flags {RO access} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 1] {flags {RW update} begin_search {type index spec {index 3}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [r command getkeys kspec.tworangeswithgap foo bar baz quux] {foo baz}
|
|
}
|
|
|
|
test "Module key specs: Keyword-only spec clears the legacy triple" {
|
|
set reply [lindex [r command info kspec.keyword] 0]
|
|
# Verify (first, last, step) and movablekeys
|
|
assert_equal [lindex $reply 2] {module movablekeys}
|
|
assert_equal [lindex $reply 3] 0
|
|
assert_equal [lindex $reply 4] 0
|
|
assert_equal [lindex $reply 5] 0
|
|
# Verify key-specs
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [lindex $keyspecs 0] {flags {RO access} begin_search {type keyword spec {keyword KEYS startfrom 1}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}
|
|
assert_equal [r command getkeys kspec.keyword foo KEYS bar baz] {bar baz}
|
|
}
|
|
|
|
test "Module key specs: Complex specs, case 1" {
|
|
set reply [lindex [r command info kspec.complex1] 0]
|
|
# Verify (first, last, step) and movablekeys
|
|
assert_equal [lindex $reply 2] {module movablekeys}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] 1
|
|
assert_equal [lindex $reply 5] 1
|
|
# Verify key-specs
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [lindex $keyspecs 0] {flags RO begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 1] {flags {RW update} begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 2] {flags {RO access} begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}
|
|
assert_equal [r command getkeys kspec.complex1 foo dummy KEYS 1 bar baz STORE quux] {foo quux bar}
|
|
}
|
|
|
|
test "Module key specs: Complex specs, case 2" {
|
|
set reply [lindex [r command info kspec.complex2] 0]
|
|
# Verify (first, last, step) and movablekeys
|
|
assert_equal [lindex $reply 2] {module movablekeys}
|
|
assert_equal [lindex $reply 3] 1
|
|
assert_equal [lindex $reply 4] 2
|
|
assert_equal [lindex $reply 5] 1
|
|
# Verify key-specs
|
|
set keyspecs [lindex $reply 8]
|
|
assert_equal [lindex $keyspecs 0] {flags {RW update} begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 1] {flags {RO access} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 2] {flags {RO access} begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
|
|
assert_equal [lindex $keyspecs 3] {flags {RW update} begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}
|
|
assert_equal [lindex $keyspecs 4] {flags {RW update} begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}
|
|
assert_equal [r command getkeys kspec.complex2 foo bar 2 baz quux banana STORE dst dummy MOREKEYS hey ho] {dst foo bar baz quux hey ho}
|
|
}
|
|
|
|
test "Module command list filtering" {
|
|
;# Note: we piggyback this tcl file to test the general functionality of command list filtering
|
|
set reply [r command list filterby module keyspecs]
|
|
assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.keyword kspec.none kspec.nonewithgetkeys kspec.tworanges kspec.tworangeswithgap}
|
|
assert_equal [r command getkeys kspec.complex2 foo bar 2 baz quux banana STORE dst dummy MOREKEYS hey ho] {dst foo bar baz quux hey ho}
|
|
}
|
|
|
|
test {COMMAND GETKEYSANDFLAGS correctly reports module key-spec without flags} {
|
|
r command getkeysandflags kspec.none key1 val1 key2 val2
|
|
} {{key1 {RW access update}} {key2 {RW access update}}}
|
|
|
|
test {COMMAND GETKEYSANDFLAGS correctly reports module key-spec with flags} {
|
|
r command getkeysandflags kspec.nonewithgetkeys key1 val1 key2 val2
|
|
} {{key1 {RO access}} {key2 {RO access}}}
|
|
|
|
test {COMMAND GETKEYSANDFLAGS correctly reports module key-spec flags} {
|
|
r command getkeysandflags kspec.keyword keys key1 key2 key3
|
|
} {{key1 {RO access}} {key2 {RO access}} {key3 {RO access}}}
|
|
|
|
# user that can only read from "read" keys, write to "write" keys, and read+write to "RW" keys
|
|
r ACL setuser testuser +@all %R~read* %W~write* %RW~rw*
|
|
|
|
test "Module key specs: No spec, only legacy triple - ACL" {
|
|
# legacy triple didn't provide flags, so they require both read and write
|
|
assert_equal "OK" [r ACL DRYRUN testuser kspec.none rw val1]
|
|
assert_match {*has no permissions to access the 'read' key*} [r ACL DRYRUN testuser kspec.none read val1]
|
|
assert_match {*has no permissions to access the 'write' key*} [r ACL DRYRUN testuser kspec.none write val1]
|
|
}
|
|
|
|
test "Module key specs: tworanges - ACL" {
|
|
assert_equal "OK" [r ACL DRYRUN testuser kspec.tworanges read write]
|
|
assert_equal "OK" [r ACL DRYRUN testuser kspec.tworanges rw rw]
|
|
assert_match {*has no permissions to access the 'read' key*} [r ACL DRYRUN testuser kspec.tworanges rw read]
|
|
assert_match {*has no permissions to access the 'write' key*} [r ACL DRYRUN testuser kspec.tworanges write rw]
|
|
}
|
|
|
|
foreach cmd {kspec.none kspec.tworanges} {
|
|
test "$cmd command will not be marked with movablekeys" {
|
|
set info [lindex [r command info $cmd] 0]
|
|
assert_no_match {*movablekeys*} [lindex $info 2]
|
|
}
|
|
}
|
|
|
|
foreach cmd {kspec.keyword kspec.complex1 kspec.complex2 kspec.nonewithgetkeys} {
|
|
test "$cmd command is marked with movablekeys" {
|
|
set info [lindex [r command info $cmd] 0]
|
|
assert_match {*movablekeys*} [lindex $info 2]
|
|
}
|
|
}
|
|
|
|
test "Unload the module - keyspecs" {
|
|
assert_equal {OK} [r module unload keyspecs]
|
|
}
|
|
}
|