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
66 changes: 53 additions & 13 deletions app/platforms/implementations/ogc_api_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,26 +319,60 @@ def _extract_download_link_from_asset(self, asset: dict) -> str | None:
return href
return None

def _generate_signed_url(self, href: str, user_token: str) -> str:
def _generate_signed_url(self, href: str, user_token: str) -> str | None:
"""
Generate a signed URL for the given href using the provided user token.
This is a placeholder implementation and should be replaced with
actual logic to generate signed URLs.
The endpoint is expected to answer with a redirect and a `Location`
header that points to the signed resource.

Args:
href (str): The original href.
user_token (str): The user token to be used for signing.

Returns:
str | None: The signed URL if it can be extracted, otherwise None.
"""
# TODO - Add implementation
logger.debug(f"Generating signed URL for href: {href} with user token.")
response = requests.get(
href,
headers={"Authorization": f"Bearer {user_token}"},
allow_redirects=False,
try:
response = requests.get(
href,
headers={"Authorization": f"Bearer {user_token}"},
allow_redirects=False,
timeout=20,
)
response.raise_for_status()
except requests.RequestException as exc:
logger.warning(
"Could not generate signed URL due to HTTP/network error "
f"for href '{href}': {exc}"
)
return None

location_header = response.headers.get("location") or response.headers.get(
"Location"
)
if location_header:
logger.debug(f"Signed URL generated for href '{href}'.")
return location_header

# Some providers may return 200 with a direct link instead of redirecting.
if response.url and response.url != href:
logger.warning(
"Missing Location header while generating signed URL for "
f"href '{href}'. Falling back to response URL '{response.url}'."
)
return response.url

response_content_type = response.headers.get("content-type", "unknown")
response_body_preview = (response.text or "")[:250].replace("\n", " ")
logger.warning(
"Missing Location header while generating signed URL for "
f"href '{href}'. Status={response.status_code}, "
f"content-type='{response_content_type}', "
f"headers={dict(response.headers)}, "
f"body-preview='{response_body_preview}'."
)
signed_url = response.headers["location"]
logger.debug(f"Signed URL: {signed_url}")
return signed_url
return None

def _update_assets_hrefs(self, assets: dict, user_token: str) -> dict:
"""
Expand All @@ -358,8 +392,14 @@ def _update_assets_hrefs(self, assets: dict, user_token: str) -> dict:
"Skipping asset..."
)
else:
href = self._generate_signed_url(href, user_token)
updated_asset["href"] = href
signed_url = self._generate_signed_url(href, user_token)
if not signed_url:
logger.warning(
f"Could not sign asset href for '{asset_name}'. Keeping "
"original HTTPS href."
)
signed_url = href
updated_asset["href"] = signed_url
updated_assets[asset_name] = updated_asset

return updated_assets
Expand Down
81 changes: 81 additions & 0 deletions tests/platforms/test_ogc_api_process_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import requests


def _install_ogc_api_processes_client_stub():
Expand Down Expand Up @@ -258,6 +259,86 @@ def test_transform_bbox_parameter_invalid_value(platform, value):
platform._transform_bbox_parameter("bbox", value)


@patch("app.platforms.implementations.ogc_api_process.requests.get")
def test_generate_signed_url_returns_location_header(mock_requests_get, platform):
mock_response = MagicMock()
mock_response.headers = {"location": "https://signed.example.com/file.tif"}
mock_response.url = "https://download.example.com/resource"
mock_response.status_code = 302
mock_response.raise_for_status.return_value = None
mock_requests_get.return_value = mock_response

result = platform._generate_signed_url(
"https://download.example.com/resource", "token"
)

assert result == "https://signed.example.com/file.tif"


@patch("app.platforms.implementations.ogc_api_process.requests.get")
def test_generate_signed_url_falls_back_to_response_url(mock_requests_get, platform):
mock_response = MagicMock()
mock_response.headers = {}
mock_response.url = "https://signed.example.com/direct-link.tif"
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None
mock_requests_get.return_value = mock_response

result = platform._generate_signed_url(
"https://download.example.com/resource", "token"
)

assert result == "https://signed.example.com/direct-link.tif"


@patch("app.platforms.implementations.ogc_api_process.requests.get")
def test_generate_signed_url_returns_none_when_location_is_missing(
mock_requests_get, platform
):
mock_response = MagicMock()
mock_response.headers = {}
mock_response.url = "https://download.example.com/resource"
mock_response.status_code = 200
mock_response.text = '{"detail":"missing redirect"}'
mock_response.raise_for_status.return_value = None
mock_requests_get.return_value = mock_response

result = platform._generate_signed_url(
"https://download.example.com/resource", "token"
)

assert result is None


@patch("app.platforms.implementations.ogc_api_process.requests.get")
def test_generate_signed_url_returns_none_on_request_exception(
mock_requests_get, platform
):
mock_requests_get.side_effect = requests.RequestException("connection error")

result = platform._generate_signed_url(
"https://download.example.com/resource", "token"
)

assert result is None


def test_update_assets_hrefs_keeps_original_href_when_signing_fails(platform):
assets = {
"result": {
"href": "s3://bucket/result.tif",
"alternate": {
"https": {"href": "https://download.example.com/resource"}
},
}
}

with patch.object(platform, "_generate_signed_url", return_value=None):
updated = platform._update_assets_hrefs(assets, "token")

assert updated["result"]["href"] == "https://download.example.com/resource"


@pytest.mark.parametrize(
("job_id", "expected"),
[
Expand Down
Loading