Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

jeon1226
Copy link
Collaborator

No description provided.

pattern_len--;
}
if (pattern_len == 1) {
return 1; /* match */
Copy link
Collaborator

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

Copy link
Collaborator

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?

Copy link
Collaborator Author

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?

Copy link
Collaborator

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);
Copy link
Collaborator

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 =
Copy link
Collaborator

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 {
Copy link
Collaborator

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.

Copy link
Member

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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
Copy link
Collaborator

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(
Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Member

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.

Copy link
Collaborator Author

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

Copy link
Collaborator

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.

@jeon1226 jeon1226 linked an issue Feb 26, 2025 that may be closed by this pull request
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,
Copy link
Collaborator

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:

  1. 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.
  2. To simplify mock usage, we can represent the expected reply as a json string.
  3. 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));
Copy link
Collaborator

@yairgott yairgott Feb 27, 2025

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,
Copy link
Member

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.

Copy link
Collaborator Author

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] == '*') {
Copy link
Member

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?

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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) {
Copy link
Member

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisModule_Assert

Copy link
Collaborator

@yairgott yairgott Mar 3, 2025

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return true?

Copy link
Collaborator Author

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(
Copy link
Member

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?

Copy link
Collaborator Author

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(
Copy link
Member

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 {
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Implement Valkey 7.2 compatible ACL support
3 participants