Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions redisvl/redis/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,12 @@ def get_redis_connection(
except ResponseError:
# Fall back to a simple log echo
if hasattr(client, "echo"):
client.echo(_lib_name)
try:
client.echo(_lib_name)
except ResponseError as e:
logger.debug(
f"Failed to echo lib_name due to ACL restrictions: {e}"
)
return client

@staticmethod
Expand Down Expand Up @@ -607,7 +612,12 @@ async def _get_aredis_connection(
except ResponseError:
# Fall back to a simple log echo
if hasattr(client, "echo"):
await client.echo(_lib_name)
try:
await client.echo(_lib_name)
except ResponseError as e:
logger.debug(
f"Failed to echo lib_name due to ACL restrictions: {e}"
)
return client

@staticmethod
Expand Down Expand Up @@ -736,7 +746,12 @@ def validate_sync_redis(
# Fall back to a simple log echo
# For RedisCluster, echo is not available
if hasattr(redis_client, "echo"):
redis_client.echo(_lib_name)
try:
redis_client.echo(_lib_name)
except ResponseError as e:
logger.debug(
f"Failed to echo lib_name due to ACL restrictions: {e}"
)

# Module validation removed - operations will fail naturally if modules are missing

Expand All @@ -761,7 +776,12 @@ async def validate_async_redis(
except ResponseError:
# Fall back to a simple log echo
if hasattr(redis_client, "echo"):
await redis_client.echo(_lib_name)
try:
await redis_client.echo(_lib_name)
except ResponseError as e:
logger.debug(
f"Failed to echo lib_name due to ACL restrictions: {e}"
)

# Module validation removed - operations will fail naturally if modules are missing

Expand Down
90 changes: 90 additions & 0 deletions tests/unit/test_connection_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from redis import Redis
from redis.asyncio import Redis as AsyncRedis
from redis.exceptions import ResponseError

from redisvl.redis.connection import RedisConnectionFactory


def test_get_redis_connection_acl_fallback_denied():
"""Connection setup must survive an ACL that denies CLIENT SETINFO and ECHO."""
mock_client = MagicMock()
mock_client.client_setinfo.side_effect = ResponseError(
"NOPERM client setinfo denied"
)
mock_client.echo.side_effect = ResponseError("NOPERM echo denied")

with patch("redisvl.redis.connection.Redis.from_url", return_value=mock_client):
client = RedisConnectionFactory.get_redis_connection("redis://localhost:6379")

assert client == mock_client
mock_client.client_setinfo.assert_called_once()
mock_client.echo.assert_called_once()


@pytest.mark.asyncio
async def test_get_aredis_connection_acl_fallback_denied():
"""The async factory must behave identically to the sync one."""
mock_client = MagicMock()
mock_client.client_setinfo = AsyncMock(
side_effect=ResponseError("NOPERM client setinfo denied")
)
mock_client.echo = AsyncMock(side_effect=ResponseError("NOPERM echo denied"))

with patch(
"redisvl.redis.connection.AsyncRedis.from_url", return_value=mock_client
):
client = await RedisConnectionFactory._get_aredis_connection(
"redis://localhost:6379"
)

assert client == mock_client
mock_client.client_setinfo.assert_called_once()
mock_client.echo.assert_called_once()


def test_validate_sync_redis_acl_fallback_denied():
"""validate_sync_redis must not propagate a denied ECHO fallback.

A real ``Redis`` instance is used because ``validate_sync_redis`` gates on
``issubclass(type(redis_client), ...)``, which a ``MagicMock(spec=Redis)``
does not satisfy. Construction does not open a socket, and both commands are
patched, so no server is contacted.
"""
client = Redis(host="localhost", port=6379)

with (
patch.object(
client, "client_setinfo", side_effect=ResponseError("NOPERM setinfo denied")
) as setinfo,
patch.object(
client, "echo", side_effect=ResponseError("NOPERM echo denied")
) as echo,
):
RedisConnectionFactory.validate_sync_redis(client)

setinfo.assert_called_once()
echo.assert_called_once()


@pytest.mark.asyncio
async def test_validate_async_redis_acl_fallback_denied():
"""validate_async_redis must not propagate a denied ECHO fallback."""
client = AsyncRedis(host="localhost", port=6379)

with (
patch.object(
client,
"client_setinfo",
AsyncMock(side_effect=ResponseError("NOPERM setinfo denied")),
) as setinfo,
patch.object(
client, "echo", AsyncMock(side_effect=ResponseError("NOPERM echo denied"))
) as echo,
):
await RedisConnectionFactory.validate_async_redis(client)

setinfo.assert_called_once()
echo.assert_called_once()