Skip to content

feat: add ACL command support#1144

Merged
yisraelU merged 5 commits into
profunktor:series/2.xfrom
yisraelU:feat/acl-commands
Jun 21, 2026
Merged

feat: add ACL command support#1144
yisraelU merged 5 commits into
profunktor:series/2.xfrom
yisraelU:feat/acl-commands

Conversation

@yisraelU

Copy link
Copy Markdown
Collaborator

What

Adds first-class ACL support to RedisCommands via a new AclCommands[F] algebra that wraps Lettuce's RedisAclAsyncCommands. Previously redis4cats exposed no ACL commands at all (only AUTH).

Commands

Management (AclManagement[F])

  • aclWhoAmI, aclCat / aclCat(category), aclGenPass / aclGenPass(bits)
  • aclList, aclLoad, aclSave, aclLog / aclLog(count), aclLogReset

User management (AclUserManagement[F])

  • aclUsers, aclGetUser, aclSetUser, aclDelUser

Design notes

  • ACL SETUSER rules are a typed sum type (AclSetUserRule: On/Off/Reset, key & channel patterns, AddCommand/RemoveCommand, AddCategory/RemoveCategory, password ops) folded onto Lettuce's AclSetuserArgs — no Lettuce type leaks into the public API, consistent with the rest of the wrapper.
  • ACL GETUSER returns Option[AclUser]. Redis replies nil for a missing user, but Lettuce normalises that into an all-empty structure; absence is detected by the user carrying no flags (every real user is always on/off).
  • Category/command names are returned lowercase (matching ACL CAT wire output) and accepted case-insensitively for SETUSER.
  • CommandType token is read via toString rather than the deprecated ProtocolKeyword.name() (which -Wconf:any rejects).

Testing

  • New acl api integration scenario in RedisSpec exercising whoami / cat / genpass / setuser (typed rules) / getuser (incl. missing-user None) / users / list / deluser / log.
  • Compiles cleanly on Scala 2.12.21, 2.13.18, and 3.3.8; scalafmtCheckAll clean.

🤖 Generated with Claude Code

Adds an `AclCommands[F]` algebra wrapping Lettuce's `RedisAclAsyncCommands`,
mixed into `RedisCommands`. Covers the full ACL surface:

- Management: WHOAMI, CAT (and CAT <category>), GENPASS, LIST, LOAD, SAVE,
  LOG (and LOG <count>), LOGRESET
- User management: USERS, GETUSER, SETUSER, DELUSER

`ACL SETUSER` rules are modeled as a typed `AclSetUserRule` sum type (on/off,
key/channel patterns, command/category grants, passwords, reset) folded onto
Lettuce's `AclSetuserArgs`, so no Lettuce type leaks into the public API.

`ACL GETUSER` returns `Option[AclUser]`; Lettuce normalises Redis's nil reply
for a missing user into an all-empty structure, so absence is detected by the
user carrying no flags (every real user is always `on`/`off`).

Covered by an integration scenario in RedisSpec; compiles on 2.12/2.13/3.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR introduces first-class Redis ACL support in redis4cats by adding a new AclCommands[F] algebra and wiring it into RedisCommands, backed by Lettuce’s ACL async commands. It also adds an integration scenario to validate key ACL flows (whoami/cat/genpass/setuser/getuser/users/list/deluser/log).

Changes:

  • Added AclCommands[F] (management + user-management) algebra and mixed it into RedisCommands.
  • Implemented ACL operations in BaseRedis (effects module), including parsing for ACL LOG and ACL GETUSER, plus typed ACL SETUSER rules.
  • Added an acl api integration test scenario and enabled it in RedisSpec.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
modules/tests/src/test/scala/dev/profunktor/redis4cats/TestScenarios.scala Adds aclScenario integration flow for ACL commands.
modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisSpec.scala Registers the new acl api test.
modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala Implements the ACL API in BaseRedis, including rule folding and response parsing.
modules/effects/src/main/scala/dev/profunktor/redis4cats/effects.scala Introduces public data types for ACL GETUSER results and ACL SETUSER rules.
modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala Mixes AclCommands[F] into RedisCommands.
modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/acl.scala Adds the new ACL algebra traits and method signatures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1654 to +1655
override val aclCat: F[Set[String]] =
async.flatMap(_.aclCat().futureLift.map(_.asScala.toSet.map((c: AclCategory) => c.name().toLowerCase)))
Comment on lines +1657 to +1661
override def aclCat(category: String): F[Set[String]] =
async.flatMap(
_.aclCat(AclCategory.valueOf(category.toUpperCase)).futureLift
.map(_.asScala.toSet.map((c: CommandType) => c.toString.toLowerCase))
)
Comment on lines +1719 to +1722
case AclSetUserRule.AddCommand(c) => args.addCommand(CommandType.valueOf(c.toUpperCase))
case AclSetUserRule.RemoveCommand(c) => args.removeCommand(CommandType.valueOf(c.toUpperCase))
case AclSetUserRule.AddCategory(c) => args.addCategory(AclCategory.valueOf(c.toUpperCase))
case AclSetUserRule.RemoveCategory(c) => args.removeCategory(AclCategory.valueOf(c.toUpperCase))
_ <- IO(assert(!usersAfter.contains(user)))
_ <- redis.aclLogReset
log <- redis.aclLog
_ <- IO(assert(log.forall(_.contains("reason")) || log.isEmpty, s"log: $log"))
Reworks the ACL surface for type-safety, removing the stringly-typed inputs
and the `Any`-based reply parsing from the first cut:

- `ACL SETUSER` rules now take typed values: `AclCategory` (a sealed 29-case
  ADT mirroring Lettuce's enum, with total `asJava`/`fromJava`) and a
  `RawCommand` newtype for the open set of command names. `aclCat` takes/returns
  `AclCategory`.
- Decoding moves to a pure, total `AclDecoder`: Lettuce's untyped
  `java.util.List[Object]` is converted exactly once, at a single boundary, into
  a typed `Resp` ADT; everything downstream is `Any`-free and returns
  `Either[AclError, _]`. The old `show(v: Any)`/`toString` coercion is gone.
- Failures are values: unknown command names and malformed replies surface as
  `AclError` raised via `liftTo[F]`, never a thrown `valueOf` or a silent
  `null -> ""`.
- `aclGetUser` reserves `None` for the genuine nil sentinel (Redis returns nil
  for a missing user; Lettuce surfaces it as null/`[]`/`[null]`); a present
  reply with no `flags` field is a decoding failure, keeping absence and
  malformation distinct.
- Documents that reply-decoding ACL commands assume a string codec.

Adds an offline `AclDecoderSuite` (no Redis) covering the parsing edge cases,
and extends the integration scenario with the unknown-command error path.
Compiles on 2.12/2.13/3.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Normalise Redis command tokens with Locale.ROOT in `aclCat` (command-name
  lowercasing) and in `RawCommand -> CommandType` lookup (uppercasing), so
  parsing is stable across JVM default locales (e.g. Turkish `i`/`I`).
- Drop the redundant `|| log.isEmpty` clause in the ACL integration assertion
  (`forall` on an empty list is already true).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

log <- redis.aclLog
_ <- IO(assert(log.forall(_.contains("reason")) || log.isEmpty, s"log: $log"))
} yield ()
}

override def aclCat(category: AclCategory): F[Set[String]] =
async.flatMap(
_.aclCat(category.asJava).futureLift.map(_.asScala.toSet.map((c: CommandType) => c.toString.toLowerCase))
Comment thread modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala Outdated
Comment on lines +117 to +120
selectors <- f.get("selectors") match {
case Some(Resp.Arr(sels)) => sels.traverse(selector)
case _ => Right(List.empty[AclSelector])
}
/** Revoke access to all keys (`resetkeys`). */
case object ResetKeys extends AclSetUserRule

/** Allow access to keys matching a glob pattern (e.g. `~app:*`). */
/** Revoke access to all pub/sub channels (`resetchannels`). */
case object ResetChannels extends AclSetUserRule

/** Allow access to channels matching a glob pattern (e.g. `&news.*`). */
- AclDecoder: a present-but-non-array `selectors` field now fails with a
  DecodingFailure instead of being treated as absent, keeping malformation and
  absence distinct (the `None` case still maps to no selectors). Covered by a
  new offline test.
- effects: correct the KeyPattern/ChannelPattern scaladoc examples to bare
  globs (`app:*`, `news.*`) — Lettuce prepends the `~`/`&` itself.
- tests: the ACL integration scenario now deletes the test user via a
  `guarantee` finalizer so a mid-scenario failure can't leak the user.

The reviewer's locale comments were already addressed in the prior commit
(d543761); the casing calls use Locale.ROOT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +738 to +740
_ <- redis.aclLogReset
log <- redis.aclLog
_ <- IO(assert(log.forall(_.contains("reason")), s"log: $log"))
After `aclLogReset` the log is empty, so `log.forall(...)` was vacuously true
and verified nothing. Now assert the log is empty right after reset, then
trigger a known ACL event (a denied authentication) and assert `aclLog`
actually decodes an entry with reason "auth" — exercising the real decodeLog
path end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yisraelU yisraelU merged commit 9b7b93a into profunktor:series/2.x Jun 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants