fix: fixed router access to redis; added tests

This commit is contained in:
2025-02-18 20:16:36 +01:00
parent 3ee7c0f652
commit 7569bc920a
7 changed files with 632 additions and 53 deletions

View File

@ -86,6 +86,11 @@ def backend(redis_server):
return fakeredis.FakeStrictRedis(server=redis_server)
mongo_client = mongomock.MongoClient("localhost", 27027)
fake_async_redis = fakeredis.FakeAsyncRedis(
server=redis_server, username="ingestor", password="ingestor"
)
fake_async_redis.connection_pool.connection_kwargs["username"] = "ingestor"
fake_async_redis.connection_pool.connection_kwargs["password"] = "ingestor"
config = {
"redis": {
@ -94,7 +99,7 @@ def backend(redis_server):
"username": "ingestor",
"password": "ingestor",
"sync_instance": RedisConnector("localhost:1", redis_cls=_fake_redis),
"async_instance": fakeredis.FakeAsyncRedis(server=redis_server),
"async_instance": fake_async_redis,
},
"mongodb": {"host": "localhost", "port": 27027, "mongodb_client": mongo_client},
}
@ -105,13 +110,7 @@ def backend(redis_server):
class PatchedBECAsyncRedisManager(BECAsyncRedisManager):
def _redis_connect(self):
self.redis = fakeredis.FakeAsyncRedis(
server=redis_server,
username=config["redis"]["username"],
password=config["redis"]["password"],
)
self.redis.connection_pool.connection_kwargs["username"] = config["redis"]["username"]
self.redis.connection_pool.connection_kwargs["password"] = config["redis"]["password"]
self.redis = fake_async_redis
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
with mock.patch(

View File

@ -1,3 +1,5 @@
from unittest import mock
import pytest
from bec_atlas.model.model import DeploymentAccess
@ -42,10 +44,11 @@ def test_deployment_access_router(logged_in_client):
out = DeploymentAccess(**out)
def test_patch_deployment_access(logged_in_client):
def test_patch_deployment_access(logged_in_client, backend):
"""
Test that the deployment access endpoint returns a 200 when the deployment id is valid.
"""
_, app = backend
deployments = logged_in_client.get(
"/api/v1/deployments/realm", params={"realm": "demo_beamline_1"}
).json()
@ -58,18 +61,19 @@ def test_patch_deployment_access(logged_in_client):
out = response.json()
out = DeploymentAccess(**out)
response = logged_in_client.patch(
"/api/v1/deployment_access",
params={"deployment_id": deployment_id},
json={
"user_read_access": ["test1"],
"user_write_access": ["test2"],
"su_read_access": ["test3"],
"su_write_access": ["test4"],
"remote_read_access": ["test5"],
"remote_write_access": ["test6"],
},
)
with mock.patch.object(app.deployment_access_router, "_is_valid_user", return_value=True):
response = logged_in_client.patch(
"/api/v1/deployment_access",
params={"deployment_id": deployment_id},
json={
"user_read_access": ["test1"],
"user_write_access": ["test2"],
"su_read_access": ["test3"],
"su_write_access": ["test4"],
"remote_read_access": ["test5"],
"remote_write_access": ["test6"],
},
)
assert response.status_code == 200
out = response.json()
out = DeploymentAccess(**out)

View File

@ -0,0 +1,370 @@
import asyncio
from unittest import mock
import pytest
from bec_lib import messages
from bec_lib.serialization import MsgpackSerialization
from bec_atlas.model.model import BECAccessProfile, DeploymentAccess, User
from bec_atlas.router.redis_router import RemoteAccess
@pytest.fixture
def logged_in_client(backend):
client, _ = backend
response = client.post(
"/api/v1/user/login", json={"username": "admin@bec_atlas.ch", "password": "admin"}
)
assert response.status_code == 200
token = response.json()
assert isinstance(token, str)
assert len(token) > 20
return client
@pytest.fixture
def deployment(logged_in_client):
client = logged_in_client
response = client.get("/api/v1/deployments/realm", params={"realm": "demo_beamline_1"})
assert response.status_code == 200
return response.json()[0]
@pytest.mark.parametrize(
"key, patterns, access",
[
(
"public/some/key",
[
"%R~public/*", # Read-only access
"%R~info/*", # Read-only access
"%RW~personal/test_username/*", # Read/Write access
"%RW~user/*", # Read/Write access
],
RemoteAccess.READ,
),
(
"info/some/key",
[
"%R~public/*", # Read-only access
"%R~info/*", # Read-only access
"%RW~personal/test_username/*", # Read/Write access
"%RW~user/*", # Read/Write access
],
RemoteAccess.READ,
),
(
"personal/test_username/some/key",
[
"%R~public/*", # Read-only access
"%R~info/*", # Read-only access
"%RW~personal/test_username/*", # Read/Write access
"%RW~user/*", # Read/Write access
],
RemoteAccess.READ_WRITE,
),
(
"user/some/key",
[
"%R~public/*", # Read-only access
"%R~info/*", # Read-only access
"%RW~personal/test_username/*", # Read/Write access
"%RW~user/*", # Read/Write access
],
RemoteAccess.READ_WRITE,
),
("user/some/key", ["*"], RemoteAccess.READ_WRITE),
("public/some/key", ["%W~public/*"], RemoteAccess.WRITE),
("some/key", ["%W~public/*"], RemoteAccess.NONE),
],
)
def test_get_key_pattern_access(backend, key, patterns, access):
_, app = backend
assert app.redis_router.get_key_pattern_access(key, patterns) == access
@pytest.mark.parametrize(
"channel, patterns, access",
[
(
"public/some/channel",
["public/*", "info/*", "personal/test_username/*", "user/*"],
RemoteAccess.READ_WRITE,
),
("some/channel", ["public/*"], RemoteAccess.NONE),
],
)
def test_get_channel_pattern_access(backend, channel, patterns, access):
_, app = backend
assert app.redis_router.get_channel_pattern_access(channel, patterns) == access
@pytest.mark.parametrize(
"user, deployment_access, expected_access",
[
(
User(
owner_groups=["admin"],
access_groups=["admin"],
email="admin@bec_atlas.ch",
groups=["admin"],
first_name="admin",
last_name="admin",
),
DeploymentAccess(
owner_groups=["admin"], access_groups=["admin"], user_read_access=["admin"]
),
RemoteAccess.NONE,
),
(
User(
owner_groups=["admin"],
access_groups=["admin"],
email="admin@bec_atlas.ch",
groups=["admin"],
first_name="admin",
last_name="admin",
),
DeploymentAccess(
owner_groups=["admin"], access_groups=["admin"], remote_read_access=["admin"]
),
RemoteAccess.READ,
),
(
User(
owner_groups=["admin"],
access_groups=["admin"],
email="admin@bec_atlas.ch",
groups=["admin"],
first_name="admin",
last_name="admin",
),
DeploymentAccess(
owner_groups=["admin"], access_groups=["admin"], remote_write_access=["admin"]
),
RemoteAccess.READ_WRITE,
),
],
)
def test_get_access(backend, user, deployment_access, expected_access):
_, app = backend
assert app.redis_router.get_access(user, deployment_access) == expected_access
@pytest.mark.parametrize(
"bec_access, key, redis_op, raise_exception",
[
# Full access profile - should allow all operations
(
BECAccessProfile(
deployment_id="test_id",
username="admin",
owner_groups=["admin"],
keys=["*"],
channels=["*"],
commands=["*"],
),
"some/key",
"get",
False,
),
# Read-only access to keys
(
BECAccessProfile(
deployment_id="test_id",
username="reader",
owner_groups=["readers"],
keys=["%R~data/*"],
channels=["*"],
commands=["*"],
),
"data/sensor1",
"get",
False,
),
# Write operation with read-only access should fail
(
BECAccessProfile(
deployment_id="test_id",
username="reader",
owner_groups=["readers"],
keys=["%R~data/*"],
channels=["*"],
commands=["*"],
),
"data/sensor1",
"set",
True,
),
# Send operation to allowed channel
(
BECAccessProfile(
deployment_id="test_id",
username="writer",
owner_groups=["writers"],
keys=["*"],
channels=["commands/*"],
commands=["*"],
),
"commands/motor1",
"send",
False,
),
# Testing set_and_publish with mixed permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["%RW~status/*"],
channels=["status/*"],
commands=["*"],
),
"status/device1",
"set_and_publish",
False,
),
# Testing set_and_publish with insufficient key permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["%R~status/*"],
channels=["status/*"],
commands=["*"],
),
"status/device1",
"set_and_publish",
True,
),
# Testing invalid operation
(
BECAccessProfile(
deployment_id="test_id",
username="admin",
owner_groups=["admin"],
keys=["*"],
channels=["*"],
commands=["*"],
),
"some/key",
"invalid_op",
True,
),
# Test send operation with insufficient channel permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["*"],
channels=["internal/*"],
commands=["*"],
),
"status/device1",
"send",
True,
),
# Test set_and_publish with insufficient write permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["%R~status/*"],
channels=["status/*"],
commands=["*"],
),
"status/device1",
"set_and_publish",
True,
),
# Test set_and_publish with insufficient channel permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["%RW~status*"],
channels=["internal/*"],
commands=["*"],
),
"status/device1",
"set_and_publish",
True,
),
# Test get operation with insufficient read permissions
(
BECAccessProfile(
deployment_id="test_id",
username="user",
owner_groups=["users"],
keys=["%W~status/*"],
channels=["status/*"],
commands=["*"],
),
"status/device1",
"get",
True,
),
],
ids=[
"Full access profile - should allow all operations",
"Read-only access to keys",
"Write operation with read-only access should fail",
"Send operation to allowed channel",
"Testing set_and_publish with mixed permissions",
"Testing set_and_publish with insufficient key permissions",
"Testing invalid operation",
"Test send operation with insufficient channel permissions",
"Test set_and_publish with insufficient write permissions",
"Test set_and_publish with insufficient channel permissions",
"Test get operation with insufficient read permissions",
],
)
def test_bec_access_profile_allows_op(backend, bec_access, key, redis_op, raise_exception):
_, app = backend
if raise_exception:
with pytest.raises(Exception):
app.redis_router.bec_access_profile_allows_op(bec_access, key, redis_op)
else:
app.redis_router.bec_access_profile_allows_op(bec_access, key, redis_op)
# @pytest.mark.asyncio
def test_redis_get(logged_in_client, deployment, backend):
client = logged_in_client
_, app = backend
response = client.patch(
"/api/v1/deployment_access",
params={"deployment_id": deployment["_id"]},
json={
"user_read_access": ["admin@bec_atlas.ch"],
"remote_read_access": ["admin@bec_atlas.ch"],
},
)
assert response.status_code == 200
with mock.patch.object(app.redis_router.redis, "pubsub") as pubsub_mock:
msg = MsgpackSerialization.dumps(
messages.RawMessage(data={"test_key": "test"}, metadata={"message": "test"})
)
response = {
"type": "message",
"pattern": None,
"channel": "internal/deployment",
"data": msg,
}
pubsub_mock().subscribe = mock.AsyncMock()
ret_msg = pubsub_mock().get_message = mock.AsyncMock()
ret_msg.side_effect = [None, response]
response = client.get(
"/api/v1/redis", params={"deployment": deployment["_id"], "key": "test_key"}
)
assert response.status_code == 200
assert response.json() == {
"data": {"data": {"test_key": "test"}},
"metadata": {"message": "test"},
}

View File

@ -28,7 +28,7 @@ def backend_client(backend):
async def connected_ws(backend_client):
client, app = backend_client
deployment = client.get("/api/v1/deployments/realm", params={"realm": "demo_beamline_1"}).json()
with mock.patch.object(app.redis_websocket, "get_access", return_value=RemoteAccess.READ):
with mock.patch.object(app.redis_router, "get_access", return_value=RemoteAccess.READ):
await app.redis_websocket.socket.handlers["/"]["connect"](
"sid",
{