-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ACL support to Valkey Search. #49
base: main
Are you sure you want to change the base?
Conversation
pattern_len--; | ||
} | ||
if (pattern_len == 1) { | ||
return 1; /* match */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: here and in other places, return true/false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed that valkey has fuzz test: https://github.com/valkey-io/valkey/blob/unstable/src/util.c#L199
Can we add it as a unittest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of the fuzztest in valkey is to see if there's a crash. The fuzztest we want to add here for ValkeySearch ACL has the same purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. If such exists, I don't see a reason not to just wrap it in a unittest.
RedisModuleCtx *ctx, | ||
const std::unordered_set<absl::string_view> &module_allowed_cmds, | ||
const std::vector<std::string> &module_prefixes) { | ||
RedisModuleString *username = RedisModule_GetCurrentUserName(ctx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use vmsdk::UniquePtrRedisString to manage the lifecycle of username, instead of calling RedisModule_FreeString: https://github.com/valkey-io/valkey-search/blob/main/vmsdk/src/managed_pointers.h#L52C26-L52C46
const std::unordered_set<absl::string_view> &module_allowed_cmds, | ||
const std::vector<std::string> &module_prefixes) { | ||
RedisModuleString *username = RedisModule_GetCurrentUserName(ctx); | ||
RedisModuleCallReply *reply = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also vmsdk::UniquePtrRedisCallReply for managing the reply object.
// to access ALL keys. | ||
if (module_prefixes.empty() && IsPrefixAllowed("", acl_keys)) { | ||
return absl::OkStatus(); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: else is not required and just adds indentation to the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, in situations like this, I like what was done. The extra indentation isn't excessive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, this may be related to code styling but note that throughout the code base this guideline is followed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be useful to port also the valkey unittests to make sure that our implementation is aligned with the engine.
return AclPrefixCheck(ctx, module_allowed_cmds, module_prefixes); | ||
} | ||
|
||
} // namespace valkey_search |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's saying that an empty line is missing.
return acl_views; | ||
} | ||
|
||
absl::Status AclManager::AclPrefixCheck( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how expensive this function is. Maybe there is a way to cache the results and be notified on acl changes to flush the cache?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably more applicable while we'll support non-vector queries. For now, I suggest that we just run sanity performance regression test to make sure there are no surprises. This perf test shouldn't take it to the extreme, decent amount of ACLs with an index using a dozen or so prefixes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to get notified on acl changes - is there a built in topic for acl change? If not we need to change the server code.
The ValkeySearch ACL RFC states that no security information is cached. https://github.com/valkey-io/valkey-rfc/pull/18/files#diff-ecbdf00816a3a420fd7225875f5d296283e9fada434cef7baf279184f34b9679R43 So I didn't consider to store any acl information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you could hook out the ACL update functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@allenss-amazon Do you mean the hook (a server event) should be added to Valkey? I cannot find the current Module API for an ACL update event.
https://valkey.io/topics/modules-api-ref/#section-server-hooks-implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if there is a better way but one approach is to override the ACL command and proxy it between the engine and the caller. Before returning the response back to the caller, fetch and cache the ACLs from the engine using ValkeyModule_Call.
Internally, we use RedisModule_Call API to get the ACL ruls from the server, | ||
i.e. "ACL GETUSER alice" | ||
|
||
The reply of RedisModule_Call API is complicated, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the main reason for AclManager
is to avoid mocking the RedisModule_* APIs. In general, testing dictating the design is suboptimal. In addition, by overriding AclManager::GetAclViewFromCallReply
we're missing coverage on key code areas, which will force more integration tests later.
I think that a better approach is to:
- mock RedisModule_* . Note that we already have solid framework for doing so. Most of those APIs already have mocking support and adding mocks for the remaining APIs should be relatively simple.
- To simplify mock usage, we can represent the expected reply as a json string.
- During the mock API calls, we can use the parsing context to generate JSON path queries and validate the calls against the JSON.
Happy to further discuss this.
@@ -40,6 +41,8 @@ absl::Status FTCreateCmd(RedisModuleCtx *ctx, RedisModuleString **argv, | |||
int argc) { | |||
VMSDK_ASSIGN_OR_RETURN(auto index_schema_proto, | |||
ParseFTCreateArgs(ctx, argv + 1, argc - 1)); | |||
VMSDK_RETURN_IF_ERROR(AclManager::Instance().AclPrefixCheck( | |||
ctx, kCommandCategories.at(kCreate), index_schema_proto)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and in all other AclPrefixCheck calls: I wonder if using ctx
might lead RedisModule_Call to fail due to ACL enforced by the engine. The alternative is to use valkey_search::ctx_
match. It returns true when (1) the pattern matches the string, AND (2) the | ||
pattern ends with wildcards. | ||
*/ | ||
bool StringEndsWithWildCardMatch(const char *pattern, int pattern_len, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code look quite brittle. Tracking the pointers and lengths should be combined into convenience functions. I'd recommend using string_view here with it's associated manipulation functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My biggest worry is, a hand crafted pattern matching code is doing something weird, while the original code is verified to work well. In this case I want to use the library code as much as possible. If that is not an option, I'll copy and paste the library code and modify it as small as possible.
The code here is from valkey/src/util.c. I agree that the pointer and length approach is scary, but deviating from the original code too much could be more dangerous, considering this is supposed to be a temp solution (after this this should be changed into some valkey module api with the same functionality). The safety issue (crash) can be handled by fuzz testing.
while (pattern_len && string_len) { | ||
switch (pattern[0]) { | ||
case '*': | ||
while (pattern_len && pattern[1] == '*') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a potential memory violation. How do we know if [1] is valid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pattern_len
is not zero, and in this case we believe pattern_len
is maintained properly as a length of pattern
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, it is not a null terminated string, so you're right.
} | ||
match = 0; | ||
while (1) { | ||
if (pattern[0] == '\\' && pattern_len >= 2) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please reorder to avoid accessing [0] before the length check.
} else { | ||
// This should not happen. All ACL command expression starts with +, -, | ||
// or all/no command alias. | ||
abort(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RedisModule_Assert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the recommended way is to use absl CHECK
bool result = false; | ||
for (const auto &acl_key : acl_keys) { | ||
if (acl_key == "allkeys") { | ||
result = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key patterns of Valkey Acl rule is cumulative and path dependent.
"allkeys nokeys allkeys" is all key allowed, however,
"allkeys nokeys allkeys nokeys" is no key allowed
so we need to see all items in acl_keys
through the end.
continue; | ||
} else { | ||
int offset = acl_key.find('~') + 1; // either one of ~, %R~, and %RW~ | ||
result = result || StringEndsWithWildCardMatch( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once you get a true, why bother continuing in the loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same logic as above. Even if we got all permissions at this point, if the last acl_key
is nokeys
or -@all
no keys are allowed.
return acl_views; | ||
} | ||
|
||
absl::Status AclManager::AclPrefixCheck( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you could hook out the ACL update functions.
// to access ALL keys. | ||
if (module_prefixes.empty() && IsPrefixAllowed("", acl_keys)) { | ||
return absl::OkStatus(); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, in situations like this, I like what was done. The extra indentation isn't excessive.
No description provided.