Skip to content

Add ACL role support - #3967

Open
yang-z-o wants to merge 13 commits into
valkey-io:unstablefrom
yang-z-o:acl-role
Open

Add ACL role support#3967
yang-z-o wants to merge 13 commits into
valkey-io:unstablefrom
yang-z-o:acl-role

Conversation

@yang-z-o

@yang-z-o yang-z-o commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #3726

Implements ACL roles as described in the issue. A role is a named, reusable set of ACL selectors that can be assigned to multiple users. This allows operators to define permission policies once and apply them to many users, avoiding per-user rule duplication.

Core design

  • A role holds its own list of selectors (same structure as user selectors). Internally a role is a user struct with USER_FLAG_ROLE set, stored in a separate Roles radix tree, so it reuses the existing selector machinery.
  • Users hold pointers to role objects. Every permission check evaluates the user's own selectors first, then each role's selectors (OR logic) — this covers command, key, channel, and unrestricted-key-access checks.
  • Role updates modify selectors in-place, immediately visible to all members (zero-cost propagation).
  • Users can only add permissions on top of roles, not restrict them (consistent with existing multi-selector OR semantics).
  • Roles cannot be nested, cannot have passwords, and cannot be enabled/disabled — those modifiers are rejected.
  • Role names must not collide with an existing command or ACL category name.

New commands

All new in 9.2.0.

Command Description
ACL SETROLE <name> <rules...> Create or update a role
ACL DELROLE <name> [name ...] Delete one or more roles; fails if any role still has members, and returns the number actually deleted (mirrors ACL DELUSER)
ACL GETROLE <name> Show role permissions and members
ACL ROLES List all role names
ACL SETUSER <user> +@role:<name> Assign a role to a user
ACL SETUSER <user> -@role:<name> Remove a role from a user

ACL file and config support

  • Roles can be defined in the ACL file with the role keyword or inline in valkey.conf.
  • ACL file loading uses a two-pass approach (roles first, then users) so definition order doesn't matter. Duplicate role definitions are rejected, as duplicate users already were.
  • Role rules in valkey.conf may reference commands and categories that a module registers later — roles are loaded after modules, same as users.
  • ACL SAVE writes roles before users.
  • ACL LIST outputs roles before users.
  • CONFIG REWRITE persists the in-memory roles, so roles created or deleted at runtime survive a restart.

Tests

1. ACL commands (runtime)

  • Role CRUD (SETROLE, DELROLE, GETROLE, ROLES), including deleting multiple roles at once
  • Role rule validation: rejects passwords, on/off, nested roles, unmatched parenthesis, names with spaces, and names colliding with a command or category
  • Role assignment and removal (+@role:<name>, -@role:<name>), including empty and non-existent role names
  • ACL DRYRUN respects role selectors; role changes are immediately visible to members
  • Multiple roles with OR logic, roles with multiple selectors, user permissions adding on top of a role, user cannot restrict role permissions
  • Channel patterns via roles, and pubsub clients disconnected when SETROLE or SETUSER revokes channel access
  • ACL LIST includes roles; user reset clears role memberships
  • Role and user names are case-sensitive on both sides of the membership
  • SORT BY/GET honour full key access granted only through a role

2. ACL file (aclfile option)

  • Roles and users loaded from a dedicated ACL file; definition order doesn't matter
  • User-level permissions add on top of a role from the ACL file
  • ACL SAVE and ACL LOAD preserve roles
  • The default user keeps its role membership across ACL LOAD, and a role it holds cannot be deleted
  • Error paths: invalid role rules, role line without a name, duplicate role definitions

3. Inline directives in valkey.conf

  • Both role and user directives loaded from the main config; role permissions effective for users defined in the same config
  • Startup fails on duplicate roles, invalid role rules, and role names colliding with a command or category
  • CONFIG REWRITE persists roles created at runtime and drops roles deleted at runtime

4. Module API

  • VM_ACLCheckKeyPermissions and VM_ACLCheckChannelPermissions honour grants that reach the user only through a role
  • Roles pick up module commands in a granted category when the module loads
  • Module unload is blocked while a role references one of its commands
  • A role declared in valkey.conf can reference a module command

Backwards compatibility

  • Existing ACL files without roles load correctly with no changes required.
  • Existing valkey.conf files without role directives work as before.
  • ACL GETUSER output adds a new roles field but all other fields remain unchanged.
  • ACL LIST prepends role entries before user entries; existing user entry format is unchanged.

Signed-off-by: Yang Zhao <zymy701@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds first-class ACL roles with named permission sets, user-role membership, role-aware authorization, ACL/config persistence, role administration commands, updated command schemas, and end-to-end tests.

Changes

ACL Roles Feature

Layer / File(s) Summary
Role model and membership lifecycle
src/server.h, src/acl.c
Adds role storage and flags, bidirectional user membership tracking, role lifecycle handling, selector updates, and role-aware ACL descriptions.
Role assignment and authorization
src/acl.c
Adds ACL SETUSER role membership operations and evaluates role selectors for command and channel permissions.
Configuration and ACL persistence
src/acl.c, src/config.c
Loads roles before users, parses ACL files in role-first passes, rolls back roles and users together, and saves roles before users.
Role administration commands
src/acl.c, src/commands.def, src/commands/acl-*.json
Adds SETROLE, DELROLE, GETROLE, and ROLES, plus output, help, dispatch, history, and reply-schema updates.
Role behavior and persistence tests
tests/assets/role.acl, tests/unit/acl-role.tcl, tests/unit/acl.tcl
Tests role lifecycle, membership, permissions, persistence, configuration loading, validation failures, and updated duplicate-definition errors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ACLCheckAllUserCommandPerm
  participant UserSelectors
  participant RoleSelectors
  Client->>ACLCheckAllUserCommandPerm: Check command permission
  ACLCheckAllUserCommandPerm->>UserSelectors: Evaluate user selectors
  ACLCheckAllUserCommandPerm->>RoleSelectors: Evaluate assigned-role selectors
  UserSelectors-->>ACLCheckAllUserCommandPerm: Permission result
  RoleSelectors-->>ACLCheckAllUserCommandPerm: Permission result
  ACLCheckAllUserCommandPerm-->>Client: Allow or deny
Loading

Suggested reviewers: madolson

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding ACL role support.
Description check ✅ Passed The description directly describes the ACL role implementation and related commands, file support, and tests.
Linked Issues check ✅ Passed The changes implement named ACL roles, role commands, role assignment, loading/saving, and validation as required by #3726.
Out of Scope Changes check ✅ Passed No obvious unrelated code changes stand out; the edits and tests all support ACL role functionality.
Docstring Coverage ✅ Passed Docstring coverage is 96.77% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands.def (1)

7146-7152: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a 10.0.0 history entry for the new ACL SETUSER role syntax.

ACL_SETUSER_History still stops at 9.1.0, but this PR adds +@role:<name> / -@role:<name> handling. That leaves generated metadata and COMMAND DOCS without any record of the new public syntax.

Based on PR objectives: role assignment/removal is exposed through ACL SETUSER using +@role:<name> / -@role:<name>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands.def` around lines 7146 - 7152, ACL_SETUSER_History is missing
the new 10.0.0 entry for the role syntax change; add a new history entry to the
ACL_SETUSER_History array describing the addition of "+@role:<name> /
-@role:<name>" role assignment/removal syntax (use the exact symbol
ACL_SETUSER_History and append a {"10.0.0","Added role assignment/removal via
+@role:<name> and -@role:<name>."} entry so generated metadata and COMMAND DOCS
include the new public syntax).
🧹 Nitpick comments (2)
src/acl.c (2)

2863-2892: 💤 Low value

Consider adding rule validation for consistency with user loading.

Unlike ACLAppendUserForLoading which validates rules against a fake user (tolerating unknown commands/roles), this function only validates selector parenthesis matching via ACLMergeSelectorArguments but doesn't validate the actual rules.

While invalid rules will still be caught during ACLLoadConfiguredRoles at startup, validating here would:

  1. Provide earlier, more precise error reporting (line number from config parsing)
  2. Be consistent with user loading behavior

This is a minor inconsistency since both approaches ultimately prevent startup with invalid ACLs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acl.c` around lines 2863 - 2892, ACLAppendRoleForLoading should validate
the merged rule arguments the same way ACLAppendUserForLoading does (i.e., by
applying the rules to a temporary/fake ACL user) instead of only relying on
ACLMergeSelectorArguments; after acl_args is produced, create a temporary user
object and run the same rule-validation path used by
ACLAppendUserForLoading/ACLLoadConfiguredRoles to detect invalid commands/roles
and parenthesis errors, set argc_err to invalid_idx+2 (or the appropriate
argument index returned by the validator) and return C_ERR on failure, freeing
acl_args before returning; on success continue to copy/store the validated
acl_args as currently done.

729-735: 💤 Low value

Minor: Unreachable code path with confusing error message.

The check at line 731 can only fail if a role with the same name was created between line 703 (where we checked !r) and line 730. In single-threaded command processing, this race condition cannot occur. The error message "Role already exists" is also misleading since the function already determined the role doesn't exist.

This is defensive code and harmless, but consider removing it or updating the message if kept for robustness against future changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acl.c` around lines 729 - 735, The defensive check after ACLCreateRole is
confusing and unreachable in current single-threaded processing; either remove
the entire if (!r) { r = ACLCreateRole(rolename, sdslen(rolename)); if (!r) {
error = sdsnew("Role already exists"); goto cleanup; } } block, or if you want
to keep a defensive fallback, change the sdsnew message to a generic failure
like "Failed to create role" or "ACLCreateRole returned NULL" and ensure the
code still jumps to cleanup; reference ACLCreateRole, r, rolename, sdsnew, error
and cleanup when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands.def`:
- Around line 7215-7216: Update the summary strings for the getrole and getuser
command entries so they mention role-related fields: modify the MAKE_CMD
invocation for "getrole" (the entry using ACL_GETROLE_History, ACL_GETROLE_Tips,
ACL_GETROLE_Args) to say it returns role selectors and members in addition to
ACL rules, and modify the MAKE_CMD invocation for "getuser" (the entry using
ACL_GETUSER_History, ACL_GETUSER_Tips, ACL_GETUSER_Args) to indicate the
response includes a roles field (in addition to listing
rules/passwords/patterns); keep the rest of each command entry unchanged.

In `@src/config.c`:
- Line 580: The error message for duplicate roles is misleading because
ACLSetStringError() returns a user-specific string when errno==EALREADY; update
handling so duplicate-role errors produce a generic or role-aware message.
Either modify ACLSetStringError() (used by ACLAppendRoleForLoading and others)
to return a neutral wording for EALREADY like "Duplicate ACL definition found.
Each user or role can only be defined once in config files", or refactor callers
(e.g., where ACLAppendRoleForLoading invokes ACLSetStringError() in config.c) to
pass contextual information so the error text distinguishes roles from users;
implement the simpler fix by changing the EALREADY branch in ACLSetStringError()
to the generic message.
- Around line 577-584: Add "role" to the allowlist used by
rewriteConfigReadOldFile/lookupConfig so lines starting with "role" are not
commented out during CONFIG REWRITE (preventing data loss), and fix the
duplicate-role error text by ensuring role-specific errors are produced: either
have ACLAppendRoleForLoading set a role-specific errno/message or update
ACLSetStringError to return "Duplicate role found..." when called after
ACLAppendRoleForLoading returns EALREADY; reference the functions
rewriteConfigReadOldFile, lookupConfig (allowlist), ACLAppendRoleForLoading and
ACLSetStringError to locate and apply these changes.

---

Outside diff comments:
In `@src/commands.def`:
- Around line 7146-7152: ACL_SETUSER_History is missing the new 10.0.0 entry for
the role syntax change; add a new history entry to the ACL_SETUSER_History array
describing the addition of "+@role:<name> / -@role:<name>" role
assignment/removal syntax (use the exact symbol ACL_SETUSER_History and append a
{"10.0.0","Added role assignment/removal via +@role:<name> and -@role:<name>."}
entry so generated metadata and COMMAND DOCS include the new public syntax).

---

Nitpick comments:
In `@src/acl.c`:
- Around line 2863-2892: ACLAppendRoleForLoading should validate the merged rule
arguments the same way ACLAppendUserForLoading does (i.e., by applying the rules
to a temporary/fake ACL user) instead of only relying on
ACLMergeSelectorArguments; after acl_args is produced, create a temporary user
object and run the same rule-validation path used by
ACLAppendUserForLoading/ACLLoadConfiguredRoles to detect invalid commands/roles
and parenthesis errors, set argc_err to invalid_idx+2 (or the appropriate
argument index returned by the validator) and return C_ERR on failure, freeing
acl_args before returning; on success continue to copy/store the validated
acl_args as currently done.
- Around line 729-735: The defensive check after ACLCreateRole is confusing and
unreachable in current single-threaded processing; either remove the entire if
(!r) { r = ACLCreateRole(rolename, sdslen(rolename)); if (!r) { error =
sdsnew("Role already exists"); goto cleanup; } } block, or if you want to keep a
defensive fallback, change the sdsnew message to a generic failure like "Failed
to create role" or "ACLCreateRole returned NULL" and ensure the code still jumps
to cleanup; reference ACLCreateRole, r, rolename, sdsnew, error and cleanup when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a1a1111-8d14-43ee-9833-a72088926703

📥 Commits

Reviewing files that changed from the base of the PR and between f3bdf50 and 0b3c583.

📒 Files selected for processing (11)
  • src/acl.c
  • src/commands.def
  • src/commands/acl-delrole.json
  • src/commands/acl-getrole.json
  • src/commands/acl-getuser.json
  • src/commands/acl-roles.json
  • src/commands/acl-setrole.json
  • src/config.c
  • src/server.h
  • tests/assets/role.acl
  • tests/unit/acl-role.tcl

Comment thread src/commands.def Outdated
Comment thread src/config.c
Comment thread src/config.c
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.38849% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.57%. Comparing base (a3c5f44) to head (78ad342).
⚠️ Report is 14 commits behind head on unstable.

Files with missing lines Patch % Lines
src/acl.c 89.23% 58 Missing ⚠️
src/config.c 94.11% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #3967      +/-   ##
============================================
+ Coverage     78.55%   78.57%   +0.02%     
============================================
  Files           166      166              
  Lines         88370    88866     +496     
============================================
+ Hits          69416    69828     +412     
- Misses        18954    19038      +84     
Files with missing lines Coverage Δ
src/commands.def 100.00% <ø> (ø)
src/server.h 100.00% <ø> (ø)
src/config.c 80.05% <94.11%> (+0.15%) ⬆️
src/acl.c 92.28% <89.23%> (-0.45%) ⬇️

... and 20 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@roshkhatri roshkhatri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good. few dedup to start with

Comment thread src/acl.c Outdated
Comment thread src/acl.c Outdated
Comment thread src/acl.c
Comment thread src/server.h Outdated
Comment thread src/acl.c Outdated
Comment thread src/acl.c Outdated
Comment thread src/acl.c Outdated
Signed-off-by: Yang Zhao <zymy701@gmail.com>
@yang-z-o yang-z-o changed the title ACL Role Add ACL role support Jun 16, 2026
@hpatro

hpatro commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@valkey-review-bot Please review this change.

@valkey-review-bot

valkey-review-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

One issue on role-aware permission checks: the command-execution path now honors role selectors, but three sibling permission helpers were not given the same treatment, so they diverge from the new OR-with-roles semantics. Details inline.

The rest of the change — bidirectional member-list bookkeeping across ACLCopyUser/ACLFreeUser/reset, the two-pass ACL-file load with old_roles rollback, the acl_string cache invalidation, and the GETUSER/GETROLE reply schemas — looks consistent, and the prior review threads (CONFIG REWRITE allowlist, duplicate-definition wording, helper extraction) appear addressed.

Comment thread src/acl.c

@hpatro hpatro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lot of code feels redundant due to the CRUD operation already available around User. I think the code would be much simpler if we use a flag at the User struct level to distinguish between User and Role and we could reuse lot of the existing code. What do you think?

Skimmed through the rest of the code, looks decent. Will do a thorough review after we decide on the above.

Comment thread src/acl.c
list *RolesToLoad; /* Similar to UsersToLoad, but for ACL roles. Every list
element is a NULL terminated array of SDS pointers:
the first is the role name, all the remaining pointers
are ACL rules (no passwords/on/off). */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There won't be any passwords, right?

Suggested change
are ACL rules (no passwords/on/off). */
are ACL rules */

@yang-z-o yang-z-o Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes I meant Roles are similar to ACL user but no passwords/on/off
UsersToLoad - contains passwords/on/off
RolesToLoad - no passwords/on/off

Comment thread src/acl.c Outdated

@roshkhatri roshkhatri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @hpatro about using the CRUD operation already available around User

I had few more concerns, let me know wdyt?

Also, the pubsub clients are not being killed/disconnected when the channel access is revoked through roles either via SETROLE restricting the role, or via SETUSER -@role: removing the role from the user.

Comment thread src/acl.c Outdated
Comment thread src/acl.c
Comment thread src/acl.c Outdated
Comment thread src/acl.c Outdated
@yang-z-o

Copy link
Copy Markdown
Contributor Author

I think the code would be much simpler if we use a flag at the User struct level to distinguish between User and Role and we could reuse lot of the existing code. What do you think?

Agree, unifying the struct with a flag while keeping them stored separately makes sense, will rework the PR, example struct would be:

#define USER_FLAG_ROLE (1 << 3)                       

  typedef struct user {
      sds name;
      uint32_t flags;   /* USER_FLAG_ROLE distinguishes roles from users */
      list *passwords;  /* Unused for roles */
      list *selectors;
      list *roles;      /* For users: assigned roles */
      list *members;    /* For roles: member users */
      robj *acl_string;
  } user;

Signed-off-by: Yang Zhao <zymy701@gmail.com>
Comment thread src/commands/acl-setuser.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/acl-role.tcl (1)

255-258: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Verify ordering and restored ACL semantics, not just presence.

These checks would pass if roles were emitted after users, or if ACL SAVE restored role names but lost selectors or memberships. Assert all roles precede users, then after ACL LOAD recheck Alice’s membership and allowed/denied commands.

Also applies to: 318-322, 369-374

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/acl-role.tcl` around lines 255 - 258, Strengthen the ACL LIST
assertions in the role-list tests and the corresponding checks at the other
referenced locations: verify every role entry appears before user entries,
rather than only checking that one role exists. After ACL LOAD, reassert Alice’s
role membership plus her allowed and denied command behavior, preserving the
existing ACL SAVE/LOAD setup and test flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.h`:
- Around line 1309-1312: Remove the unrelated declarations and field from
src/server.h: delete client::qb_applied, getStringObjectLen, sdsEncodedObject,
the object accessor declarations, zzlValidateScores, initLibbacktraceFrameState,
and configInfoCommand at the specified consolidated sites; make no other
changes.

---

Nitpick comments:
In `@tests/unit/acl-role.tcl`:
- Around line 255-258: Strengthen the ACL LIST assertions in the role-list tests
and the corresponding checks at the other referenced locations: verify every
role entry appears before user entries, rather than only checking that one role
exists. After ACL LOAD, reassert Alice’s role membership plus her allowed and
denied command behavior, preserving the existing ACL SAVE/LOAD setup and test
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6fba351-73b3-418a-ae08-ba8dbfbd4d07

📥 Commits

Reviewing files that changed from the base of the PR and between 7f27dc0 and db61042.

📒 Files selected for processing (13)
  • src/acl.c
  • src/commands.def
  • src/commands/acl-delrole.json
  • src/commands/acl-getrole.json
  • src/commands/acl-getuser.json
  • src/commands/acl-roles.json
  • src/commands/acl-setrole.json
  • src/commands/acl-setuser.json
  • src/config.c
  • src/server.h
  • tests/assets/role.acl
  • tests/unit/acl-role.tcl
  • tests/unit/acl.tcl
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/commands/acl-setuser.json
  • src/commands/acl-roles.json
  • tests/assets/role.acl
  • src/commands/acl-getuser.json
  • tests/unit/acl.tcl
  • src/commands/acl-delrole.json
  • src/commands/acl-setrole.json
  • src/commands/acl-getrole.json
  • src/config.c
  • src/commands.def
  • src/acl.c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/acl-role.tcl (1)

255-258: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Verify ordering and restored ACL semantics, not just presence.

These checks would pass if roles were emitted after users, or if ACL SAVE restored role names but lost selectors or memberships. Assert all roles precede users, then after ACL LOAD recheck Alice’s membership and allowed/denied commands.

Also applies to: 318-322, 369-374

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/acl-role.tcl` around lines 255 - 258, Strengthen the ACL LIST
assertions in the role-list tests and the corresponding checks at the other
referenced locations: verify every role entry appears before user entries,
rather than only checking that one role exists. After ACL LOAD, reassert Alice’s
role membership plus her allowed and denied command behavior, preserving the
existing ACL SAVE/LOAD setup and test flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.h`:
- Around line 1309-1312: Remove the unrelated declarations and field from
src/server.h: delete client::qb_applied, getStringObjectLen, sdsEncodedObject,
the object accessor declarations, zzlValidateScores, initLibbacktraceFrameState,
and configInfoCommand at the specified consolidated sites; make no other
changes.

---

Nitpick comments:
In `@tests/unit/acl-role.tcl`:
- Around line 255-258: Strengthen the ACL LIST assertions in the role-list tests
and the corresponding checks at the other referenced locations: verify every
role entry appears before user entries, rather than only checking that one role
exists. After ACL LOAD, reassert Alice’s role membership plus her allowed and
denied command behavior, preserving the existing ACL SAVE/LOAD setup and test
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6fba351-73b3-418a-ae08-ba8dbfbd4d07

📥 Commits

Reviewing files that changed from the base of the PR and between 7f27dc0 and db61042.

📒 Files selected for processing (13)
  • src/acl.c
  • src/commands.def
  • src/commands/acl-delrole.json
  • src/commands/acl-getrole.json
  • src/commands/acl-getuser.json
  • src/commands/acl-roles.json
  • src/commands/acl-setrole.json
  • src/commands/acl-setuser.json
  • src/config.c
  • src/server.h
  • tests/assets/role.acl
  • tests/unit/acl-role.tcl
  • tests/unit/acl.tcl
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/commands/acl-setuser.json
  • src/commands/acl-roles.json
  • tests/assets/role.acl
  • src/commands/acl-getuser.json
  • tests/unit/acl.tcl
  • src/commands/acl-delrole.json
  • src/commands/acl-setrole.json
  • src/commands/acl-getrole.json
  • src/config.c
  • src/commands.def
  • src/acl.c
🛑 Comments failed to post (1)
src/server.h (1)

1309-1312: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove unrelated changes from this PR.

These additions appear to be unrelated to the ACL role support feature. As per coding guidelines, avoid unrelated refactors in the same change to keep the PR focused and easy to backport.

  • src/server.h#L1309-L1312: Remove the qb_applied field from the client struct if it belongs to a different feature.
  • src/server.h#L3156-L3156: Remove the getStringObjectLen declaration.
  • src/server.h#L3187-L3187: Remove the sdsEncodedObject macro.
  • src/server.h#L3201-L3210: Remove the object accessor function declarations.
  • src/server.h#L3436-L3436: Remove the zzlValidateScores declaration.
  • src/server.h#L3482-L3484: Remove the initLibbacktraceFrameState declaration.
  • src/server.h#L4149-L4149: Remove the configInfoCommand declaration.
📍 Affects 1 file
  • src/server.h#L1309-L1312 (this comment)
  • src/server.h#L3156-L3156
  • src/server.h#L3187-L3187
  • src/server.h#L3201-L3210
  • src/server.h#L3436-L3436
  • src/server.h#L3482-L3484
  • src/server.h#L4149-L4149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.h` around lines 1309 - 1312, Remove the unrelated declarations and
field from src/server.h: delete client::qb_applied, getStringObjectLen,
sdsEncodedObject, the object accessor declarations, zzlValidateScores,
initLibbacktraceFrameState, and configInfoCommand at the specified consolidated
sites; make no other changes.

Source: Coding guidelines

yang-z-o added 3 commits July 16, 2026 17:25
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
yang-z-o added 2 commits July 20, 2026 11:14
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>

@hpatro hpatro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great progress so far. I've left few comments. Need to take another pass at the src/ changes and then the tests. Claude found few issues, please have a look at those as well.

Critical: reproduced crashes

  1. Wrong dictType — stringSetDictType is case-insensitive, ACL names are case-sensitive

  src/acl.c:466,159 use stringSetDictType, which is dictCStrCaseHash / dictCStrKeyCaseCompare (src/server.c:803). Role and user names in Users/Roles are compared case-sensitively (rax), so the two views disagree.

  Reproduced (ACL SETROLE Cache … + ACL SETROLE cache …, both listed by ACL ROLES):
  ACL SETUSER u1 on >p +@role:Cache +@role:cache
  ACL GETUSER u1  →  roles: Cache          # "cache" silently dropped
  And on the members side it is a use-after-free:
  ACL SETUSER alice on >p +@role:rr
  ACL SETUSER ALICE on >p +@role:rr
  ACL GETROLE rr   →  members: alice       # ALICE missing (dictAdd collided, sdsdup leaked)
  ACL DELUSER alice
  ACL DELROLE rr   →  1                    # should fail: ALICE still references rr
  ACL LIST         →  server crash
  Fix: use a case-sensitive sds-keyed dictType (same shape as migrateCacheDictType: dictSdsHash / dictSdsKeyCompare / dictEntryDestructorSdsKey), for both roles and members.

  2. ACL LOAD with a role on the default user → dangling role pointer

  ACLCopyUser(DefaultUser, new_default) then ACLFreeUser(new_default) (src/acl.c:3163-3164) both key members by the name "default":

  - ACLCopyRoles does dictAdd(r->members, sdsdup("default"), DefaultUser) → DICT_ERR, because new_default already registered "default". Return value is ignored; the sds leaks.
  - ACLFreeUser(new_default) → ACLUserClearRoles → dictDelete(r->members, "default") removes the entry outright.

  Net: DefaultUser->roles = {r} but r->members = {}. Reproduced with a 2-line ACL file (role r ~* &* +@all / user default on nopass ~* &* +@all +@role:r):
  ACL GETROLE r        →  members: (empty)   # wrong
  ACL DELROLE r        →  1                  # should fail
  ACL GETUSER default  →  SIGSEGV in aclCommand  (si_code 2, addr 0xc044e33f03f853c5)
  Root cause is broader than the default user: ACLCopyRoles (src/acl.c:97-98) ignores both dictAdd return values. Keying members by the user * pointer, or asserting on dictAdd andhandling the replace case, would
  make this class of bug impossible. (Other ACLCopyUser callers escape only because ACLCreateUnlinkedUser gives the temp user a distinct __fakeuser:N__ name — that's an accidentalinvariant worth a comment at
  minimum.)

  High severity

  Net: DefaultUser->roles = {r} but r->members = {}. Reproduced with a 2-line ACL file (role r ~* &* +@all / user default on nopass ~* &* +@all +@role:r):
  ACL GETROLE r        →  members: (empty)   # wrong
  ACL DELROLE r        →  1                  # should fail
  ACL GETUSER default  →  SIGSEGV in aclCommand  (si_code 2, addr 0xc044e33f03f853c5)
  Root cause is broader than the default user: ACLCopyRoles (src/acl.c:97-98) ignores both dictAdd return values. Keying members by the user * pointer, or asserting on dictAdd andhandling the replace case, would make this class of bug impossible. (Other ACLCopyUser callers escape only because ACLCreateUnlinkedUser gives the temp user a distinct __fakeuser:N__
  name — that's an accidental invariant worth a comment at minimum.)


 3. Role selectors are skipped by ACLRecomputeCommandBitsFromCommandRulesAllUsers

  src/acl.c:895 iterates only the Users rax. It's called on module load and unload (src/module.c:13427,13682) precisely to keep per-selector command bits in sync with command-ID
  churn. Role selectors are never recomputed, so:

  - a role granting +@read won't pick up newly registered module commands in that category;
  - after a module unload, a role's stale command bits can survive and grant access to a different command that later reuses the freed command ID — a silent privilege escalation.

  Fix: iterate Roles too (and rename the function or add a role loop).

@@ -0,0 +1,38 @@
{
"DELROLE": {
"summary": "Deletes one or more ACL roles. Fails if any role has members.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"summary": "Deletes one or more ACL roles. Fails if any role has members.",
"summary": "Deletes one or more ACL roles. Fails if any role has users.",

Comment thread src/config.c
Comment thread src/acl.c
Comment on lines +4116 to +4117
"DELROLE <rolename>",
" Delete a role (must have no members).",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to avoid a new noun member. I would rather call out user.

Suggested change
"DELROLE <rolename>",
" Delete a role (must have no members).",
"DELROLE <rolename>",
" Delete a role (must have no users).",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Wondering whether we should update the user-facing text only or the struct field as well?
Roles and users share the same user struct, so if we rename the field to users we would end up with user->users, which might be a bit ambiguous? That's why I went with u->roles & r->members to keep the two directions of the membership distinguishable.

Comment thread src/acl.c Outdated
Comment thread src/acl.c
Comment thread src/acl.c
Comment thread src/acl.c
Comment thread src/acl.c
@hpatro
hpatro requested a review from dvkashapov August 5, 2026 17:01
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
Signed-off-by: Yang Zhao <zymy701@gmail.com>
@madolson madolson added the major-decision-pending Major decision pending by TSC team label Aug 17, 2026
@madolson

Copy link
Copy Markdown
Member

@valkey-io/core-team Please 👍 / 👎 for the major decision here. The new permission model is simpler, these are really just named selectors now, which is probably a bit more straightforward of an idea.

@madolson madolson moved this from Todo to Needs Review in Valkey 9.2 Aug 17, 2026
@melancholictheory

Copy link
Copy Markdown
Contributor

Not a review of the whole PR, only the part that overlaps with ACL DIGEST (#4446), which @zuiderkwast raised over there. The digest hashes each user as the line ACL LIST reports for it and XORs the per user results together, and there are two places where that and roles do not fit together yet.

A role rule change does not reach the digest. ACLDescribeUser renders a membership as +@role:<name>, which stays the same when the role's own rules change, and ACLDigest only walks Users. I cherry-picked #4446 onto this branch and ran it:

role reader ~data:* resetchannels -@all +get
user alice on #148de9c5... resetchannels -@all +@role:reader
digest before = 343fc51f2ee2422307fa79481a871f478eac1c2e8e0a2c73c0db142f315d7558

ACL SETROLE reader ~data:* +get +set
ACL DRYRUN alice SET data:1 x  ->  OK

role reader ~data:* resetchannels -@all +get +set
user alice on #148de9c5... resetchannels -@all +@role:reader
digest after  = 343fc51f2ee2422307fa79481a871f478eac1c2e8e0a2c73c0db142f315d7558

Alice gained SET, ACL LIST shows it, the digest stays put. A client using the digest to confirm which revision of an ACL a server is running would miss that edit.

The second one only turns up once you fix the first. Role names are checked against commands and categories but not against user names, so role dup and user dup can both exist. Folding roles into the same XOR by name and rules alone would let those two cancel each other out.

Both are covered by hashing the full ACL LIST line, keyword included, and walking Roles as well:

static void ACLDigestAddTable(rax *table, const char *keyword, unsigned char *digest) {
    raxIterator ri;

    raxStart(&ri, table);
    raxSeek(&ri, "^", NULL, 0);
    while (raxNext(&ri)) {
        user *u = ri.data;
        robj *rules = ACLDescribeUser(u);
        sds rulestr = objectGetVal(rules);
        unsigned char entry[SHA256_BLOCK_SIZE];
        SHA256_CTX ctx;

        sha256_init(&ctx);
        sha256_update(&ctx, (unsigned char *)keyword, strlen(keyword));
        sha256_update(&ctx, (unsigned char *)u->name, sdslen(u->name));
        sha256_update(&ctx, (unsigned char *)" ", 1);
        sha256_update(&ctx, (unsigned char *)rulestr, sdslen(rulestr));
        sha256_final(&ctx, entry);
        decrRefCount(rules);

        for (int j = 0; j < SHA256_BLOCK_SIZE; j++) digest[j] ^= entry[j];
    }
    raxStop(&ri);
}

static sds ACLDigest(void) {
    unsigned char digest[SHA256_BLOCK_SIZE] = {0};

    ACLDigestAddTable(Roles, "role ", digest);
    ACLDigestAddTable(Users, "user ", digest);
    return ACLHexDigest(digest, SHA256_BLOCK_SIZE);
}

With that on top of the cherry-pick the digest moves on a role edit, and unit/acl and unit/acl-role run green together, 203 tests.

There is nothing to do here while the two are separate branches, it just needs picking up by whichever lands second. #4446 is approved and waiting on the major decision clock, so that may well be this one. I can send the change as a patch here, or open it as a follow-up once this merges, whichever you prefer.

@zuiderkwast zuiderkwast added major-decision-approved Major decision approved by TSC team and removed major-decision-pending Major decision pending by TSC team labels Aug 21, 2026
@zuiderkwast

Copy link
Copy Markdown
Contributor

I see 5 thumbs up from TSC members (out of 9) on the vote above at #3967 (comment) so we can consider the API approved. Code review still pending.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

major-decision-approved Major decision approved by TSC team

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

[NEW] ACL Role

6 participants