Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/rcapi/api/convertor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
read_spectrum_native, plot_spectrum, preprocess_spectrum, x4search
)
from rcapi.services.kc import get_token
from rcapi.services.hsds_endpoint import reject_hsds_endpoint_override
import h5py
import h5pyd
from rcapi.services.solr_query import SOLR_ROOT, SOLR_COLLECTIONS
Expand Down Expand Up @@ -60,6 +61,11 @@ async def convert_get(
if not domain:
# tr.set_error("missing domain")
raise HTTPException(status_code=400, detail=str("missing domain"))
if what == "h5":
try:
reject_hsds_endpoint_override(domain)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid HSDS domain")

solr_url, collection_param, dropped = SOLR_COLLECTIONS.get_url(
SOLR_ROOT, data_source, drop_private=token is None)
Expand Down
3 changes: 2 additions & 1 deletion src/rcapi/api/hsds_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pynanomapper.clients.datamodel_simple import StudyRaman
from rcapi.services.kc import get_token
from rcapi.services.convertor_service import x4search
from rcapi.services.hsds_endpoint import reject_hsds_endpoint_override
router = APIRouter()


Expand Down Expand Up @@ -177,7 +178,7 @@ async def read_solr_study4dataset(


def read_cha(domain, result, read_values=False, filter={"sample": None}, token=None):

reject_hsds_endpoint_override(domain)
with h5pyd.File(domain, api_key=token) as file:
tmp, datasets = get_file_annotations(file, read_values, filter)
if tmp is None or datasets is None:
Expand Down
2 changes: 2 additions & 0 deletions src/rcapi/services/convertor_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rcapi.services.solr_query import (
solr_query_get, SOLR_VECTOR, solr_doc_filter
)
from rcapi.services.hsds_endpoint import reject_hsds_endpoint_override
import traceback
import tempfile
import shutil
Expand Down Expand Up @@ -354,6 +355,7 @@ def plot_mol(mol, title=None, thumbnail=True, figsize=None, **draw_kwargs):


def knnquery(domain, dataset="raw"):
reject_hsds_endpoint_override(domain)
try:
with h5pyd.File(domain, mode="r") as h5:
x = h5[dataset][0]
Expand Down
7 changes: 7 additions & 0 deletions src/rcapi/services/hsds_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
_ENDPOINT_URL_PREFIXES = ("http://", "https://", "http+unix://")


def reject_hsds_endpoint_override(value: str) -> None:
"""Reject URL forms that make h5pyd replace its configured endpoint."""
if value.lower().startswith(_ENDPOINT_URL_PREFIXES):
raise ValueError("invalid HSDS domain")
97 changes: 97 additions & 0 deletions tests/test_hsds_endpoint_override.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from unittest.mock import Mock

import pytest
from fastapi.testclient import TestClient

from rcapi.api import convertor, hsds_dataset
from rcapi.main import app
from rcapi.services import convertor_service
from rcapi.services.hsds_endpoint import reject_hsds_endpoint_override


client = TestClient(app)


@pytest.mark.parametrize(
"value",
[
"http://example.test/file.nxs",
"hTtPs://example.test/file.nxs",
"http+unix://%2Ftmp%2Fhsds.sock/file.nxs",
],
)
def test_reject_hsds_endpoint_override_rejects_endpoint_urls(value):
with pytest.raises(ValueError, match="invalid HSDS domain"):
reject_hsds_endpoint_override(value)


@pytest.mark.parametrize(
"value",
[
"//example.test/file.nxs",
"hdf5://RRUF/example.nxs",
],
)
def test_reject_hsds_endpoint_override_allows_non_endpoint_h5pyd_forms(value):
assert reject_hsds_endpoint_override(value) is None


def test_download_rejects_invalid_domain_before_h5pyd(monkeypatch):
open_file = Mock(side_effect=AssertionError("h5pyd.File must not be called"))
monkeypatch.setattr(convertor.h5pyd, "File", open_file)

response = client.get(
"/db/download",
params={"what": "h5", "domain": "https://example.test/file.nxs"},
)

assert response.status_code == 400
assert response.json() == {"detail": "Invalid HSDS domain"}
open_file.assert_not_called()


def test_download_passes_non_endpoint_domain_to_h5pyd_unchanged(monkeypatch):
remote_file = Mock()
remote_file.__enter__ = Mock(return_value=Mock())
remote_file.__exit__ = Mock(return_value=False)
open_file = Mock(return_value=remote_file)
monkeypatch.setattr(convertor.h5pyd, "File", open_file)
monkeypatch.setattr(convertor, "recursive_copy", Mock())

response = client.get(
"/db/download",
params={
"what": "h5",
"domain": "/RRUF/example.nxs",
},
)

assert response.status_code == 200
open_file.assert_called_once_with(
"/RRUF/example.nxs",
mode="r",
api_key=None,
)


def test_legacy_reader_rejects_invalid_domain_before_h5pyd(monkeypatch):
open_file = Mock(side_effect=AssertionError("h5pyd.File must not be called"))
monkeypatch.setattr(hsds_dataset.h5pyd, "File", open_file)

with pytest.raises(ValueError, match="invalid HSDS domain"):
hsds_dataset.read_cha(
"https://example.test/file.nxs",
{"annotation": [], "datasets": []},
)

open_file.assert_not_called()


def test_unused_knnquery_rejects_invalid_domain_before_h5pyd(monkeypatch):
open_file = Mock(side_effect=AssertionError("h5pyd.File must not be called"))
monkeypatch.setattr(convertor_service.h5pyd, "File", open_file)

with pytest.raises(ValueError, match="invalid HSDS domain"):
convertor_service.knnquery("https://example.test/file.nxs")

open_file.assert_not_called()