feat: add ACL command support#1144
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
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 intoRedisCommands. - Implemented ACL operations in
BaseRedis(effects module), including parsing forACL LOGandACL GETUSER, plus typedACL SETUSERrules. - Added an
acl apiintegration test scenario and enabled it inRedisSpec.
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>
| 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 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds first-class ACL support to
RedisCommandsvia a newAclCommands[F]algebra that wraps Lettuce'sRedisAclAsyncCommands. Previously redis4cats exposed no ACL commands at all (onlyAUTH).Commands
Management (
AclManagement[F])aclWhoAmI,aclCat/aclCat(category),aclGenPass/aclGenPass(bits)aclList,aclLoad,aclSave,aclLog/aclLog(count),aclLogResetUser management (
AclUserManagement[F])aclUsers,aclGetUser,aclSetUser,aclDelUserDesign notes
ACL SETUSERrules are a typed sum type (AclSetUserRule:On/Off/Reset, key & channel patterns,AddCommand/RemoveCommand,AddCategory/RemoveCategory, password ops) folded onto Lettuce'sAclSetuserArgs— no Lettuce type leaks into the public API, consistent with the rest of the wrapper.ACL GETUSERreturnsOption[AclUser]. Redis repliesnilfor 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 alwayson/off).ACL CATwire output) and accepted case-insensitively forSETUSER.CommandTypetoken is read viatoStringrather than the deprecatedProtocolKeyword.name()(which-Wconf:anyrejects).Testing
acl apiintegration scenario inRedisSpecexercising whoami / cat / genpass / setuser (typed rules) / getuser (incl. missing-userNone) / users / list / deluser / log.scalafmtCheckAllclean.🤖 Generated with Claude Code