Skip to content

Commit f27ed60

Browse files
author
Andrzej Pijanowski
committed
feat: Add header filtering for collections and geometry, apply geometry filters to single item access
1 parent 99dfcd8 commit f27ed60

File tree

4 files changed

+86
-1
lines changed

4 files changed

+86
-1
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
126126
- [Managing Elasticsearch Indices](#managing-elasticsearch-indices)
127127
- [Snapshots](#snapshots)
128128
- [Reindexing](#reindexing)
129+
- [Header Filtering](#header-filtering)
129130
- [Auth](#auth)
130131
- [Aggregation](#aggregation)
131132
- [Rate Limiting](#rate-limiting)
@@ -1015,6 +1016,39 @@ pip install stac-fastapi-elasticsearch[redis]
10151016
- This makes the modified Items with lowercase identifiers visible to users accessing my-collection in the STAC API
10161017
- Using aliases allows you to switch between different index versions without changing the API endpoint
10171018
1019+
## Header Filtering
1020+
1021+
SFEOS supports filtering API responses based on HTTP headers. This enables upstream proxies or gateways to restrict access to specific collections and geographic areas.
1022+
1023+
### Headers
1024+
1025+
| Header | Format | Description |
1026+
|--------|--------|-------------|
1027+
| `X-Filter-Collections` | Comma-separated IDs | Restricts access to specified collections only |
1028+
| `X-Filter-Geometry` | GeoJSON geometry | Restricts access to items within the specified geometry |
1029+
1030+
### Affected Endpoints
1031+
1032+
| Endpoint | Collection Filter | Geometry Filter |
1033+
|----------|:-----------------:|:---------------:|
1034+
| `GET /collections` | ✅ | - |
1035+
| `GET /collections/{id}` | ✅ (404 if denied) | - |
1036+
| `GET /collections/{id}/items` | ✅ | ✅ |
1037+
| `GET /collections/{id}/items/{id}` | ✅ (404 if denied) | ✅* (404 if denied) |
1038+
| `GET/POST /search` | ✅ | ✅ |
1039+
1040+
*Requires optional `shapely` dependency.
1041+
1042+
### Optional Dependency
1043+
1044+
For geometry filtering on single item endpoints (`/collections/{id}/items/{id}`), install with the `geo` extra:
1045+
1046+
```bash
1047+
pip install stac-fastapi-core[geo]
1048+
```
1049+
1050+
Without this dependency, geometry filtering on single items is skipped with a warning.
1051+
10181052
## Auth
10191053
10201054
- **Overview**: Authentication is an optional feature that can be enabled through Route Dependencies.

stac_fastapi/core/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ redis = [
4949
"redis~=6.4.0",
5050
"retry~=0.9.2",
5151
]
52+
geo = [
53+
"shapely>=2.0.0",
54+
]
5255

5356
[project.urls]
5457
Homepage = "https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch"

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,19 @@ async def get_item(
693693
item = await self.database.get_one_item(
694694
item_id=item_id, collection_id=collection_id
695695
)
696-
return self.item_serializer.db_to_stac(item, base_url)
696+
stac_item = self.item_serializer.db_to_stac(item, base_url)
697+
698+
# Check if item geometry intersects with allowed geometry filter
699+
header_geometry = parse_filter_geometry(request)
700+
if header_geometry is not None:
701+
item_geometry = stac_item.get("geometry")
702+
if item_geometry:
703+
from stac_fastapi.core.header_filters import geometry_intersects_filter
704+
705+
if not geometry_intersects_filter(item_geometry, header_geometry):
706+
raise HTTPException(status_code=404, detail="Item not found")
707+
708+
return stac_item
697709

698710
async def get_search(
699711
self,

stac_fastapi/core/stac_fastapi/core/header_filters.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,39 @@ def parse_filter_geometry(request: Request) -> Optional[Dict[str, Any]]:
7676
except json.JSONDecodeError as e:
7777
logger.warning(f"Failed to parse geometry header: {e}")
7878
return None
79+
80+
81+
def geometry_intersects_filter(
82+
item_geometry: Dict[str, Any], filter_geometry: Dict[str, Any]
83+
) -> bool:
84+
"""Check if item geometry intersects with the filter geometry.
85+
86+
Args:
87+
item_geometry: GeoJSON geometry dict from the item.
88+
filter_geometry: GeoJSON geometry dict from header filter.
89+
90+
Returns:
91+
True if geometries intersect (or if shapely not available), False otherwise.
92+
93+
Note:
94+
Requires shapely to be installed. If shapely is not available,
95+
this function returns True (allows access) to avoid breaking
96+
deployments without shapely.
97+
"""
98+
try:
99+
from shapely.geometry import shape
100+
except ImportError:
101+
logger.warning(
102+
"shapely not installed - geometry filter check skipped. "
103+
"Install shapely for full geometry filtering support."
104+
)
105+
return True # Allow access if shapely not available
106+
107+
try:
108+
item_shape = shape(item_geometry)
109+
filter_shape = shape(filter_geometry)
110+
return item_shape.intersects(filter_shape)
111+
except Exception as e:
112+
logger.warning(f"Geometry intersection check failed: {e}")
113+
# On error, allow access (fail open)
114+
return True

0 commit comments

Comments
 (0)