From 54a4012752f7d5c84572472d19d05936c4fe23f3 Mon Sep 17 00:00:00 2001 From: Yuri Zmytrakov Date: Mon, 27 Oct 2025 16:18:59 +0100 Subject: [PATCH] feat: Adding a key for technical/private data in SFEOS --- .../core/private_fields_config.py | 2 ++ .../core/stac_fastapi/core/serializers.py | 23 ++++++++++++ .../tests/resources/test_collection.py | 33 +++++++++++++++++ stac_fastapi/tests/resources/test_item.py | 36 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 stac_fastapi/core/stac_fastapi/core/private_fields_config.py diff --git a/stac_fastapi/core/stac_fastapi/core/private_fields_config.py b/stac_fastapi/core/stac_fastapi/core/private_fields_config.py new file mode 100644 index 00000000..1d1ebbcc --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/private_fields_config.py @@ -0,0 +1,2 @@ +"""Configuration for private fields that should be excluded from API responses.""" +PRIVATE_FIELDS = ["product_location"] diff --git a/stac_fastapi/core/stac_fastapi/core/serializers.py b/stac_fastapi/core/stac_fastapi/core/serializers.py index 973de18d..2d6ec132 100644 --- a/stac_fastapi/core/stac_fastapi/core/serializers.py +++ b/stac_fastapi/core/stac_fastapi/core/serializers.py @@ -14,9 +14,30 @@ from stac_fastapi.types import stac as stac_types from stac_fastapi.types.links import ItemLinks, resolve_links +from .private_fields_config import PRIVATE_FIELDS + logger = logging.getLogger(__name__) +def filter_private_fields(data: dict) -> dict: + """Remove private fields from data dictionary.""" + if not PRIVATE_FIELDS: + return data + + filtered_data = deepcopy(data) + + for field in PRIVATE_FIELDS: + if field in filtered_data: + del filtered_data[field] + + if "properties" in filtered_data and isinstance(filtered_data["properties"], dict): + for field in PRIVATE_FIELDS: + if field in filtered_data["properties"]: + del filtered_data["properties"][field] + + return filtered_data + + @attr.s class Serializer(abc.ABC): """Defines serialization methods between the API and the data model. @@ -92,6 +113,7 @@ def db_to_stac(cls, item: dict, base_url: str) -> stac_types.Item: Returns: stac_types.Item: The STAC item object. """ + item = filter_private_fields(item) item_id = item["id"] collection_id = item["collection"] item_links = ItemLinks( @@ -168,6 +190,7 @@ def db_to_stac( Returns: stac_types.Collection: The STAC collection object. """ + collection = filter_private_fields(collection) # Avoid modifying the input dict in-place ... doing so breaks some tests collection = deepcopy(collection) diff --git a/stac_fastapi/tests/resources/test_collection.py b/stac_fastapi/tests/resources/test_collection.py index f3a6c1d1..4f73e051 100644 --- a/stac_fastapi/tests/resources/test_collection.py +++ b/stac_fastapi/tests/resources/test_collection.py @@ -229,3 +229,36 @@ async def test_links_collection(app_client, ctx, txn_client): len([link for link in response.json()["links"] if link["rel"] == "license"]) == 1 ) + + +@pytest.mark.asyncio +async def test_private_fields_excluded_from_collection( + app_client, txn_client, load_test_data, monkeypatch +): + """Test that private fields are excluded from collection API responses.""" + monkeypatch.setattr( + "stac_fastapi.core.serializers.PRIVATE_FIELDS", ["test_field", "internal_data"] + ) + + test_collection = load_test_data("test_collection.json") + test_collection["id"] = "test-private-fields-collection" + + test_collection["test_field"] = "this should be hidden in collection" + test_collection["internal_data"] = {"secret": "confidential collection info"} + test_collection["description"] = "This should be visible" + + await create_collection(txn_client, test_collection) + await refresh_indices(txn_client) + + resp = await app_client.get(f"/collections/{test_collection['id']}") + assert resp.status_code == 200 + + collection_data = resp.json() + + assert "test_field" not in collection_data + assert "internal_data" not in collection_data + + assert "description" in collection_data + assert collection_data["description"] == "This should be visible" + assert "id" in collection_data + assert "title" in collection_data diff --git a/stac_fastapi/tests/resources/test_item.py b/stac_fastapi/tests/resources/test_item.py index 4231f102..f6b1b0fa 100644 --- a/stac_fastapi/tests/resources/test_item.py +++ b/stac_fastapi/tests/resources/test_item.py @@ -1163,3 +1163,39 @@ async def test_search_datetime_with_null_datetime( await txn_client.delete_collection(test_collection["id"]) except Exception as e: logger.warning(f"Failed to delete collection: {e}") + + +@pytest.mark.asyncio +async def test_private_fields_excluded_from_item( + app_client, txn_client, load_test_data, monkeypatch +): + """Test that private fields are excluded from item API responses.""" + monkeypatch.setattr( + "stac_fastapi.core.serializers.PRIVATE_FIELDS", ["test_field", "internal_data"] + ) + + test_collection = load_test_data("test_collection.json") + test_item = load_test_data("test_item.json") + + test_collection["id"] = "test-private-fields-collection" + test_item["collection"] = test_collection["id"] + test_item["id"] = "test-private-fields-item" + + test_item["test_field"] = "test field to be hidden" + test_item["internal_data"] = {"secret": "confidential"} + + await create_collection(txn_client, test_collection) + + await create_item(txn_client, test_item) + await refresh_indices(txn_client) + + resp = await app_client.get( + f"/collections/{test_item['collection']}/items/{test_item['id']}" + ) + item_data = resp.json() + + assert "test_field" not in item_data + assert "internal_data" not in item_data + + assert "id" in item_data + assert item_data["id"] == "test-private-fields-item"