fix(acl): enforce ACL on the early-intercepted command families (H-3)#258
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
Next review available in: 39 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughACL category mappings now cover early-intercepted commands. Monoio dispatch moves ChangesACL enforcement for intercepted commands
Sequence Diagram(s)sequenceDiagram
participant Client
participant ConnectionHandler
participant ACLTable
participant PubSub
Client->>ConnectionHandler: PUBLISH or SUBSCRIBE
ConnectionHandler->>ACLTable: check command permission
ACLTable-->>ConnectionHandler: allow or NOPERM
ConnectionHandler->>PubSub: publish or enter subscriber mode
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
TXN/WS/MQ/TEMPORAL/CDC.READ and the pub/sub commands are handled by connection-handler intercepts BEFORE the phf command registry, where the per-command/category ACL check normally runs. An adversarial ACL review (v1.0 hardening item H-3) found four gaps: 1. CDC.READ bypassed ACL entirely on the monoio runtime (the default build). It was dispatched ~117 lines before the ACL gate, so any authenticated user — even +get-only — could run `CDC.READ <any-wal-dir> <lsn>` and read arbitrary WAL directories off the server disk. Moved the CDC.READ intercept to AFTER the ACL gate (the tokio/sharded handler already ordered it correctly). 2. Category carve-outs silently didn't cover these families on ANY runtime. They appeared in no get_category_commands() arm, so the common deny-list idiom (+@ALL -@dangerous / -@transaction / -@Write) left them allowed (empty-allowed + non-empty-denied defaults to allow for anything not explicitly denied). Added: ws, cdc.read -> @admin + @dangerous txn, temporal -> @transaction mq, txn -> @Write all five -> @ALL 3. -@PubSub didn't block PUBLISH/SUBSCRIBE at the COMMAND level. The pub/sub intercepts consulted only the &pattern channel rule, so a -@PubSub carve-out was ineffective for a user with &*. Added a command-level check (shared::pubsub_command_acl_deny) to the PUBLISH and SUBSCRIBE/PSUBSCRIBE paths in the monoio and tokio-single handlers. The sharded handler already gates them post-ACL-gate. 4. CLIENT TRACKING ran before the ACL gate on monoio. Moved it to a post-ACL try_handle_client_tracking (it registers server-side invalidation state). CLIENT ID/SETNAME/GETNAME stay pre-ACL as connection-local metadata (Redis NO-AUTH connection commands). Tests (red/green): - src/acl/table.rs: early_intercept_families_carved_out_by_categories pins the category expansion for all five families (deny-list + allow-list). - tests/integration.rs: test_acl_denies_cdc_read_via_dangerous_carveout + test_acl_denies_pubsub_command_carveout prove end-to-end NOPERM on the connection handler. - Existing test_workspace_acl_grant already proves the sharded handler runs the ACL gate before the WS intercept (allow-list path); a sharded deny-list e2e test was omitted (documented in-file) due to a redis-rs multiplexed-handshake limitation orthogonal to ACL. Known follow-ups (unchanged by this commit): CLIENT SETINFO is unsupported (unknown-subcommand); handler_single has no TXN/WS/MQ intercepts (falls through to the generic gate). Neither is an ACL defect. author: Tin Dang <tindang.ht97@gmail.com>
fc4802a to
7c795c2
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/server/conn/handler_monoio/mod.rs (2)
281-331: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winEnforce the command-level
-@pubsubgate in subscriber modeSUBSCRIBE/PSUBSCRIBEhere still only check channel ACLs, so an already-subscribed session can keep adding channels after a mid-session ACL revocation. Apply the same command check used by the subscribe entry path in this branch too; the same gap exists in thehandler_single.rssubscriber loop.🤖 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/conn/handler_monoio/mod.rs` around lines 281 - 331, Add the command-level -@pubsub ACL check to the subscriber-mode SUBSCRIBE and PSUBSCRIBE handling before processing channels, matching the existing subscribe entry-path check and returning the appropriate denial response without subscribing. Apply the same change to the corresponding subscriber loop in handler_single.rs, using the relevant command-dispatch and ACL-check symbols.
970-980: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPersistence commands need ACL before the intercept
BGSAVE/SAVE/LASTSAVE/BGREWRITEAOFare@admincommands, so+@all -@adminusers should be denied. Runningtry_handle_persistence()beforetry_enforce_acl()lets them bypass that deny-list entirely. Move the ACL check ahead of this intercept or pass the ACL context into the handler.🤖 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/conn/handler_monoio/mod.rs` around lines 970 - 980, Reorder the dispatch flow in the monoio connection handler so try_enforce_acl runs before try_handle_persistence, ensuring BGSAVE, SAVE, LASTSAVE, and BGREWRITEAOF honor admin-command permissions. Preserve the existing continue behavior and keep the ACL check ahead of all privileged intercepts, matching handler_sharded ordering.
🧹 Nitpick comments (1)
src/server/conn/handler_single.rs (1)
1069-1080: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winDenied PUBLISH/SUBSCRIBE/PSUBSCRIBE aren't recorded in
conn.acl_log.The generic ACL gate a few dozen lines below (~1417-1434) logs every denial to
conn.acl_logforACL LOGvisibility, but these new pub/sub command-level denials skip that step entirely, silently under-reporting exactly the kind of abuse (-@pubsubcarve-out violations) this PR is meant to catch.♻️ Proposed fix: log pub/sub command-level denials
if let Some(err) = crate::server::conn::shared::pubsub_command_acl_deny( &acl_table, &conn.current_user, cmd, cmd_args, ) { + conn.acl_log.push(crate::acl::AclLogEntry { + reason: "command".to_string(), + object: String::from_utf8_lossy(cmd).to_ascii_lowercase(), + username: conn.current_user.clone(), + client_addr: peer_addr.clone(), + timestamp_ms: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64, + }); let _ = framed.send(err).await; continue; }Apply the analogous change at the PUBLISH site (~1144-1152, pushing to
responsesinstead of sending directly).Also applies to: 1142-1152
🤖 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/conn/handler_single.rs` around lines 1069 - 1080, Record command-level pub/sub ACL denials in conn.acl_log before returning the error. Update the denial branches in the pub/sub handling of the single-command handler, including the direct framed.send path and the PUBLISH path that pushes to responses, to create the same ACL log entry as the generic ACL gate while preserving existing error delivery.
🤖 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.
Outside diff comments:
In `@src/server/conn/handler_monoio/mod.rs`:
- Around line 281-331: Add the command-level -@pubsub ACL check to the
subscriber-mode SUBSCRIBE and PSUBSCRIBE handling before processing channels,
matching the existing subscribe entry-path check and returning the appropriate
denial response without subscribing. Apply the same change to the corresponding
subscriber loop in handler_single.rs, using the relevant command-dispatch and
ACL-check symbols.
- Around line 970-980: Reorder the dispatch flow in the monoio connection
handler so try_enforce_acl runs before try_handle_persistence, ensuring BGSAVE,
SAVE, LASTSAVE, and BGREWRITEAOF honor admin-command permissions. Preserve the
existing continue behavior and keep the ACL check ahead of all privileged
intercepts, matching handler_sharded ordering.
---
Nitpick comments:
In `@src/server/conn/handler_single.rs`:
- Around line 1069-1080: Record command-level pub/sub ACL denials in
conn.acl_log before returning the error. Update the denial branches in the
pub/sub handling of the single-command handler, including the direct framed.send
path and the PUBLISH path that pushes to responses, to create the same ACL log
entry as the generic ACL gate while preserving existing error delivery.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 161f453c-9917-4b13-8b87-ac2c5d855e78
📒 Files selected for processing (10)
CHANGELOG.mdsrc/acl/rules.rssrc/acl/table.rssrc/server/conn/handler_monoio/dispatch.rssrc/server/conn/handler_monoio/mod.rssrc/server/conn/handler_monoio/pubsub.rssrc/server/conn/handler_single.rssrc/server/conn/shared.rstests/integration.rstests/workspace_integration.rs
The default (runtime-monoio) `cargo clippy --lib` gate failed with two `-D warnings` dead-code errors introduced by the H-3 ACL work: 1. `pubsub_command_acl_deny` (src/server/conn/shared.rs) is only called from the tokio single/sharded handlers (`handler_single`/`handler_sharded`, both `#[cfg(feature = "runtime-tokio")]`). The monoio handler inlines the same command-level ACL check in `pubsub.rs`, so under default features the helper had zero callers -> "function is never used". Gate the helper to `#[cfg(feature = "runtime-tokio")]` to match its call sites. 2. `try_handle_client_early` still took a `ctx: &ConnectionContext` param after the CLIENT TRACKING body (its only ctx user) was split out into `try_handle_client_tracking` -> "unused variable: ctx". Drop the now-dead param and its argument at the single call site in mod.rs. Verified clean on both gates: - `cargo clippy --lib` (default monoio) - `cargo clippy --lib --no-default-features --features runtime-tokio,jemalloc` author: Tin Dang <tindang.ht97@gmail.com>
…consumed it (#277) Since the H-3 ACL reorder (#258), CLIENT TRACKING ON|OFF answered "ERR unknown subcommand 'TRACKING'" on the monoio runtime (the production default) at every shard count. try_handle_client_admin runs before try_handle_client_tracking in the frame loop, and its unknown-subcommand fallback consumed TRACKING before the dedicated handler could see it — RESP3 invalidation push was entirely dead. Invisible to CI: the test matrix runs the tokio handler (handler_sharded), whose CLIENT intercept ordering differs, so all 5 client_tracking_invalidation black-box tests kept passing there while failing deterministically against a monoio binary. Fix: try_handle_client_admin falls through (returns false) for TRACKING, letting try_handle_client_tracking own it. Both handlers sit after the ACL gate, so the H-3 deniability guarantee is unchanged. Verification: all 5 client_tracking_invalidation tests re-green against the monoio binary (were 5/5 FAILED at the merge base, control-tested); fmt + clippy clean on default and runtime-tokio,jemalloc feature sets. Refs: regression introduced by #258; tests from #234 author: Tin Dang Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
… replicas (Wave B, task #34 follow-up) (#292) WS and MQ (dispatched as `WS <SUB> ...` / `MQ <SUB> ...`) and the dotted TEMPORAL.SNAPSHOT_AT / TEMPORAL.INVALIDATE commands were entirely absent from command::metadata::COMMAND_META, so metadata::is_write silently returned false for all of them. try_enforce_readonly (handler_monoio::dispatch / handler_sharded::dispatch) is driven purely by is_write, so a client could issue WS CREATE/DROP, MQ CREATE/PUSH/POP/ACK/TRIGGER/PUBLISH, and TEMPORAL.SNAPSHOT_AT/TEMPORAL.INVALIDATE directly against a read-only replica -- mutating its workspace registry, message queues, or graph temporal state (TEMPORAL.INVALIDATE sets valid_to) with no path back to the master. A silent divergence source, same class of bug already fixed for ACL in PR #258. WS and MQ now carry the blanket WRITE flag in COMMAND_META. Because both mix write subcommands (WS CREATE/DROP; MQ CREATE/PUSH/POP/ACK/TRIGGER/ PUBLISH) with read-only ones (WS LIST/INFO/AUTH; MQ DLQLEN) under a single command name, the read-only subcommands are carved out at the try_enforce_readonly call site via two new classifiers -- command::workspace::is_ws_readonly_subcommand and command::mq::is_mq_readonly_subcommand -- mirroring the existing SELECT/GRAPH.QUERY blanket-write-with-carve-out pattern already used there. TEMPORAL.SNAPSHOT_AT and TEMPORAL.INVALIDATE are full dotted command names (no subcommand token) and are unconditionally WRITE. The replica APPLY path is unaffected by construction: it never calls try_enforce_readonly (see the module doc on replication::apply), so replaying a master's own WS/MQ/TEMPORAL records on a replica still works unchanged. Both handler_monoio (monoio, primary replication runtime) and handler_sharded (tokio) dispatch orders already ran try_enforce_readonly before the WS/MQ/TEMPORAL handlers -- the only fix needed was the metadata registration and the subcommand carve-outs, not a reordering. Verified against a real monoio-runtime binary via a stash-and-rebuild A/B (the new integration test fails against the pre-fix binary with a bare Ok(<workspace-id>) instead of READONLY, and passes after re-applying the fix), and separately against a runtime-tokio+jemalloc binary for handler_sharded parity -- closing the "monoio intercept-order bugs are CI-blind" gap called out in the task brief. Tests: - command::metadata: WS/MQ/TEMPORAL.SNAPSHOT_AT/TEMPORAL.INVALIDATE are WRITE-flagged (red before this change, green after). - command::workspace / command::mq: the two new subcommand classifiers, covering every implemented subcommand plus the unknown/empty-args cases. - tests/replication_readonly_ws_mq.rs (new, --ignored, spawns a real moon binary): WS/MQ are only wired in the sharded handlers, so the synthetic single-shard listener::run_with_shutdown harness used by tests/replication_test.rs cannot exercise this fix. Master-side writes succeed, replica-side writes get -READONLY, and the read-only subcommands (WS LIST, MQ DLQLEN) are still served on the replica. Gates run locally (macOS): cargo fmt --check; cargo clippy -- -D warnings (default features); cargo clippy --no-default-features --features runtime-tokio,jemalloc -- -D warnings; cargo test --profile release-fast --lib on both feature sets (4154 + 3350 passed); the new integration test against both a monoio and a runtime-tokio,jemalloc release-fast binary. CHANGELOG updated under [Unreleased]. Scope note: handler_single (single-shard tokio listener) does not wire WS/MQ at all and was left untouched, matching existing precedent (tests/workspace_integration.rs module doc). handler_monoio/ft.rs was not touched, per the parallel Wave B WS-plane / MQ-plane-replication work landing on sibling branches. author: Tin Dang Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
…(v0.7.0 prep) Owner reconciliation 2026-07-14 of docs/PRODUCTION-CONTRACT.md against the tree (main is 67 commits past the v0.6.0 tag, carrying the full v0.6.1 hygiene scope and the v0.7 replication workstreams untagged): Re-ticked with verified evidence: - FUZZ-01: graph_props_record.rs restored, all 12 declared targets exist - ACL-REG-01: TXN/WS/MQ/TEMPORAL/CDC in metadata.rs + monoio intercept reorder (PR #258) - COLD-TTL-01: ColdIndex::sweep_expired + info_reclamation counters - SEC-07: SECURITY.md now states a release-agnostic supported-versions policy (latest minor line pre-1.0, LTS from v1.0.0) Updated honestly: - FT-PARITY-01: FT.AGGREGATE shipped (ft_aggregate.rs); FT.ALTER still absent -- stays unticked - CRASH-01: caveats recorded -- graph-durability g1-g3 never actually ran before PRs #322/#324 (WAL v2 flat-file probe + legacy replay no-op); crash_recovery_disk_offload_no_aof residual red (task #44) - SUPPLY-01: supply-chain.yml workflow in flight (task #63), tick on merge New rows for shipped-but-previously-untracked guarantees: - CRASH-02: 37-cell cross-plane kill-9 matrix (v0.8 G1, PR #298 + the 4 RED-group fixes #52/#53/#57/#60) - MEM-10X-01: 10x RAM datasets G2 acceptance (restart readiness 157s->3.7s, PRs #319/#297/#320) - REPL-PLANES-01: all-plane replication (Waves A/B, PRs #285/#294) - REPL-SOAK-01 (unticked): 24h replication soak gates the v0.7.0 tag Header refreshed (milestone = v0.7.0 Replication GA, tag gated on soak). scripts/check-production-contract.sh parses all new rows; GA-blocking gap is now an honest 17 rows. Refs: tasks #61-#65 author: Tin Dang
…(v0.7.0 prep) (#325) Owner reconciliation 2026-07-14 of docs/PRODUCTION-CONTRACT.md against the tree (main is 67 commits past the v0.6.0 tag, carrying the full v0.6.1 hygiene scope and the v0.7 replication workstreams untagged): Re-ticked with verified evidence: - FUZZ-01: graph_props_record.rs restored, all 12 declared targets exist - ACL-REG-01: TXN/WS/MQ/TEMPORAL/CDC in metadata.rs + monoio intercept reorder (PR #258) - COLD-TTL-01: ColdIndex::sweep_expired + info_reclamation counters - SEC-07: SECURITY.md now states a release-agnostic supported-versions policy (latest minor line pre-1.0, LTS from v1.0.0) Updated honestly: - FT-PARITY-01: FT.AGGREGATE shipped (ft_aggregate.rs); FT.ALTER still absent -- stays unticked - CRASH-01: caveats recorded -- graph-durability g1-g3 never actually ran before PRs #322/#324 (WAL v2 flat-file probe + legacy replay no-op); crash_recovery_disk_offload_no_aof residual red (task #44) - SUPPLY-01: supply-chain.yml workflow in flight (task #63), tick on merge New rows for shipped-but-previously-untracked guarantees: - CRASH-02: 37-cell cross-plane kill-9 matrix (v0.8 G1, PR #298 + the 4 RED-group fixes #52/#53/#57/#60) - MEM-10X-01: 10x RAM datasets G2 acceptance (restart readiness 157s->3.7s, PRs #319/#297/#320) - REPL-PLANES-01: all-plane replication (Waves A/B, PRs #285/#294) - REPL-SOAK-01 (unticked): 24h replication soak gates the v0.7.0 tag Header refreshed (milestone = v0.7.0 Replication GA, tag gated on soak). scripts/check-production-contract.sh parses all new rows; GA-blocking gap is now an honest 17 rows. Refs: tasks #61-#65 author: Tin Dang Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Summary
An adversarial ACL review (v1.0 hardening item H-3) found that Moon's per-family connection-handler intercepts (TXN/WS/MQ/TEMPORAL/CDC.READ + pub/sub) run before the phf command registry where category ACL checks normally fire — leaving four gaps. All four are fixed here.
1. CDC.READ bypassed ACL entirely on the monoio runtime (default build)
CDC.READwas dispatched ~117 lines before the ACL gate, so any authenticated user — even+get-only — could runCDC.READ <any-wal-dir> <lsn>and read arbitrary WAL directories off the server disk. Moved the intercept to after the ACL gate (the tokio/sharded handler already ordered it correctly). This is a real, shippable difference for the defaultruntime-monoiobuild.2. Category carve-outs silently didn't cover the families (all runtimes)
TXN/WS/MQ/TEMPORAL/CDC.READ appeared in no
get_category_commands()arm, so the common deny-list idiom (+@all -@dangerous,+@all -@transaction,+@all -@write) left them allowed (empty-allowed + non-empty-denied → default-allow). Added:ws,cdc.read→@admin+@dangeroustxn,temporal→@transactionmq,txn→@write@all3.
-@pubsubdidn't block PUBLISH/SUBSCRIBE at the command levelThe pub/sub intercepts consulted only the
&patternchannel rule, so a-@pubsubcarve-out was ineffective for a user with&*. Added a command-level check (shared::pubsub_command_acl_deny) to PUBLISH and SUBSCRIBE/PSUBSCRIBE in the monoio and tokio-single handlers (sharded already gated them post-ACL).4.
CLIENT TRACKINGran before the ACL gate on monoioMoved to a post-ACL
try_handle_client_tracking(it registers server-side invalidation state).CLIENT ID/SETNAME/GETNAMEstay pre-ACL as connection-local metadata.Tests (red/green TDD)
src/acl/table.rs::early_intercept_families_carved_out_by_categories— pins the category expansion for all five families (deny-list + allow-list). RED before therules.rschange.tests/integration.rs::test_acl_denies_cdc_read_via_dangerous_carveout+test_acl_denies_pubsub_command_carveout— end-to-end NOPERM on the connection handler.test_workspace_acl_grantalready proves the sharded handler runs the ACL gate before the WS intercept (allow-list). A sharded deny-list e2e test was intentionally omitted (documented in-file): reproducing a restricted user over redis-rs multiplexed connections is unreliable because redis-rs pipelinesCLIENT SETINFO, which Moon answers "unknown subcommand" for a user that holds theclientpermission — an orthogonal test-infra limitation.Verification
cargo test --lib acl::→ 67/67cargo test --test integration test_acl→ 14/14 (incl. 2 new)cargo test --test workspace_integration→ 13/13cargo fmt --check,cargo clippy -- -D warnings(default + tokio,jemalloc) → cleanOut of scope (documented, not ACL defects)
CLIENT SETINFOis unsupported (unknown-subcommand);handler_singlehas no TXN/WS/MQ intercepts (falls through to the generic gate).Summary by CodeRabbit
Security
PUBLISH,SUBSCRIBE, andPSUBSCRIBE.CLIENT TRACKINGand CDC reads to authorized clients.Tests