fix: let a connection open when CLIENT SETINFO is denied - #685
Open
vishal-bala wants to merge 1 commit into
Open
fix: let a connection open when CLIENT SETINFO is denied#685vishal-bala wants to merge 1 commit into
vishal-bala wants to merge 1 commit into
Conversation
RedisVL announces itself on connect with `CLIENT SETINFO LIB-NAME` and, if that
is refused, sends the same string through `ECHO`. The `ECHO` call was
unguarded, so `NoPermissionError` escaped while the connection was being
created, before any index operation could be attempted:
NoPermissionError: User <name> has no permissions to run the 'echo' command
An application role assembled from `@read`/`@write` hits this. `CLIENT SETINFO`
is tagged `@connection` and `@slow`, `ECHO` is `@connection` and `@fast`, and
neither is in `@read` or `@write` -- so a rule built up from those categories
never grants either. Subtracting `@dangerous` is not what denies them: measured
on Redis 8.4.5, `+@ALL -@dangerous` permits both, which is why the shape that
fails is a hand-written `+@READ +@write` rule rather than a broad rule with
exclusions. Note also that `+@READ +@Write +@slow` permits `CLIENT SETINFO`
while still denying `ECHO`, so the fallback was never a reliable second chance.
The `ECHO` fallback is deleted rather than guarded. It was added in 934d269
(#155) as a telemetry breadcrumb for servers older than Redis 7.2, where
`CLIENT SETINFO` does not exist, so the library name would at least appear in
`MONITOR` or the slowlog. It earns nothing today: it fires only when
`CLIENT SETINFO` errors, its argument reaches nothing that reads `lib-name`
(`CLIENT LIST` and `CLIENT INFO` take that field from `SETINFO` alone, and it
would only enter the slowlog with `slowlog-log-slower-than` near zero), and
redis-py already covers the old-server case one layer down, sending its own
`CLIENT SETINFO` during the connection handshake under
`try/except ResponseError: pass`.
The `hasattr(client, "echo")` guard goes with it, along with the comment
claiming `RedisCluster` has no `echo`; both `redis.cluster.RedisCluster` and
`redis.asyncio.cluster.RedisCluster` expose it.
The four duplicated blocks are now one sync and one async helper. The explicit
`client_setinfo` call is kept deliberately -- redis-py's handshake reports its
own name, and this overwrites it with the composed
`redis-py(redisvl_v...;<wrapper>)` string that adoption metrics read -- and the
helper docstring says so, so it does not read as redundant with the handshake.
`except ResponseError` is also deliberate. In the connection-factory path this
is the first command on a freshly created connection and therefore the de-facto
connectivity check, so broadening to `RedisError` would swallow
`ConnectionError` and defer a real failure to some later command.
`AuthenticationError` subclasses `ConnectionError`, not `ResponseError`, so
`WRONGPASS` and `NOAUTH` keep propagating either way.
## Tests
- `tests/unit/test_client_identification.py` -- 16 cases over both twins.
Mutation-checked: restoring the `ECHO` fallback fails six unit cases plus the
integration test, and broadening the `except` to `Exception` fails two. Both
refusals are covered, since only one is a permission problem: a plain
`ResponseError` stands for a pre-7.2 server, which is the case that lets the
fallback go. Identification is asserted on all three URL shapes -- sentinel,
cluster, standalone -- because it sits after that fan-out and moving it into
one branch otherwise goes unnoticed; live cluster tests need
`--run-cluster-tests` and never run in CI.
- An integration test opening a connection under `+@READ +@write`, with the
premise pinned: `CLIENT SETINFO` must raise `NoPermissionError` for that user,
so the test cannot go vacuous if Redis ever grants it to that role. It lives
in `test_connection.py` beside the other identification tests.
- ACL user setup moves into an `acl_user` fixture, reused by the existing
`-@admin` test in `test_search_index.py`. Rules are applied after `reset`
because `ACL SETUSER` is additive and usernames are derived from the test's
node id; the user is dropped before the connections it authenticated; and the
fixture skips on deployments that reject `ACL SETUSER`.
## Docs
`docs/user_guide/installation.md` said a credential permitted to run neither
command fails at connection time. That is no longer true. The replacement names
`+client|setinfo` for anyone who wants RedisVL attributed in `CLIENT LIST`, and
is explicit that this labels only the connection RedisVL opens -- redis-py
labels the rest of the pool as plain `redis-py`.
It also gains a cluster caveat found while verifying this fix: `CLUSTER SLOTS`
is tagged `@slow` only, so `RedisCluster.from_url` cannot discover the topology
under a `+@READ +@write` rule and the connection fails before identification is
even attempted, reported as `Redis Cluster cannot be connected`. Such a
deployment needs `+cluster|slots` as well.
## Not in scope
The rest of the ACL documentation pass. `installation.md:189`, `:208`, `:210`
and `:223` are stale for a different reason -- they predate the
`create_index=False` opt-out -- and are corrected alongside it, not here.
Identification still reaches only one connection. The explicit call labels
whichever pooled connection it borrows, not the rest of the pool or any
reconnect, and on a cluster redis-py routes it to the default node alone.
Passing the composed name in as redis-py's `lib_name`/`driver_info` would fix
that, but `redis>=5.0,<8.0` straddles the deprecation of `lib_name` in favour
of `driver_info` and needs version-conditional handling.
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.
RedisVLannounces itself on connect withCLIENT SETINFO LIB-NAMEand, if that is refused, sends the same string throughECHO. TheECHOcall was unguarded, soNoPermissionErrorescaped while the connection was being created, before any index operation could be attempted:An application role assembled from
@read/@writehits this.CLIENT SETINFOis tagged@connectionand@slow,ECHOis@connectionand@fast, and neither is in@reador@write— so a rule built up from those categories never grants either.Subtracting
@dangerousis not what denies them. Measured on Redis 8.4.5,+@all -@dangerouspermits both, so the shape that fails is a hand-written+@read +@writerule rather than a broad rule with exclusions — which also means Redis Cloud's predefined Read-Write shape is unaffected, while a@read-shaped Read-Only rule would be affected. Note too that+@read +@write +@slowpermitsCLIENT SETINFOwhile still denyingECHO, so the fallback was never a reliable second chance.Why the fallback is deleted rather than guarded
It was added in 934d269 (#155) as a telemetry breadcrumb for servers older than Redis 7.2, where
CLIENT SETINFOdoes not exist, so the library name would at least appear inMONITORor the slowlog. It earns nothing today:CLIENT SETINFOerrors, and the ACL rule that denies one denies the other.lib-name—CLIENT LISTandCLIENT INFOtake that field fromSETINFOalone, and an O(1)ECHOonly enters the slowlog withslowlog-log-slower-thannear zero.CLIENT SETINFOduring the connection handshake undertry/except ResponseError: pass.The
hasattr(client, "echo")guard goes with it, along with the comment claimingRedisClusterhas noecho; bothredis.cluster.RedisClusterandredis.asyncio.cluster.RedisClusterexpose it.What is kept, deliberately
The four duplicated blocks become one sync and one async helper. The explicit
client_setinfocall stays: redis-py's handshake reports its own name, and this overwrites it with the composedredis-py(redisvl_v…;<wrapper>)string that adoption metrics read. The helper docstring says so, so it does not read as redundant with the handshake.except ResponseErroralso stays narrow. In the connection-factory path this is the first command on a freshly created connection and therefore the de-facto connectivity check, so broadening toRedisErrorwould swallowConnectionErrorand defer a real failure to some later command.AuthenticationErrorsubclassesConnectionError, notResponseError, soWRONGPASSandNOAUTHkeep propagating either way.Tests
tests/unit/test_client_identification.py— 16 cases over both twins. Mutation-checked: restoring theECHOfallback fails six unit cases plus the integration test, and broadening theexcepttoExceptionfails two.Both refusals are covered, since only one is a permission problem: a plain
ResponseErrorstands for a pre-7.2 server, which is the case that lets the fallback go. Identification is asserted on all three URL shapes — sentinel, cluster, standalone — because it sits after that fan-out and moving it into one branch otherwise goes unnoticed; live cluster tests need--run-cluster-testsand never run in CI.One integration test opens a connection under
+@read +@write, with the premise pinned:CLIENT SETINFOmust raiseNoPermissionErrorfor that user, so the test cannot go vacuous if Redis ever grants it to that role.ACL user setup moves into an
acl_userfixture, reused by the existing-@admintest. Rules are applied afterresetbecauseACL SETUSERis additive and usernames are derived from the test's node id; the user is dropped before the connections it authenticated; and the fixture skips on deployments that rejectACL SETUSER.Docs
docs/user_guide/installation.mdsaid a credential permitted to run neither command fails at connection time. That is no longer true. The replacement names+client|setinfofor anyone who wants RedisVL attributed inCLIENT LIST, and is explicit that this labels only the connection RedisVL opens — redis-py labels the rest of the pool as plainredis-py.It also gains a cluster caveat found while verifying this fix:
CLUSTER SLOTSis tagged@slowonly, soRedisCluster.from_urlcannot discover the topology under a+@read +@writerule and the connection fails before identification is attempted, reported as the misleadingRedis Cluster cannot be connected. Such a deployment needs+cluster|slotsas well.Not in scope
The rest of the ACL documentation pass. Four statements in
installation.mdare stale for a different reason — they predate acreate_index=Falseopt-out for the extension constructors — and are corrected alongside it in a follow-up PR.Identification still reaches only one connection: the explicit call labels whichever pooled connection it borrows, not the rest of the pool or any reconnect, and on a cluster redis-py routes it to the default node alone. Passing the composed name in as redis-py's
lib_name/driver_infowould fix that, butredis>=5.0,<8.0straddles the deprecation oflib_namein favour ofdriver_infoand needs version-conditional handling.Note
Medium Risk
Changes default connection behavior for restricted ACL credentials (intentional fix); mis-handling exceptions during identification could mask real connectivity failures, though only ResponseError is caught.
Overview
Fixes connection setup for ACL roles built from
+@read/+@writethat cannot runCLIENT SETINFO. Identification is centralized in_identify_client/_aidentify_client: RedisVL still tries to setLIB-NAME, but aResponseError(ACL denial or pre-7.2 server) is logged at debug and ignored. The oldECHOfallback is removed because it was unguarded and failed with the same permission shape.installation.mdnow states that missing identification permission does not block connect (optional+client|setinfo) and documents cluster topology discovery needing+cluster|slotsunder tight ACLs.Tests add
acl_user, integration coverage for restricted credentials, and unit tests thatECHOis not called andConnectionErroris not swallowed.Reviewed by Cursor Bugbot for commit 6d91657. Bugbot is set up for automated code reviews on this repo. Configure here.