Skip to content

Commit 56ebcd0

Browse files
authored
[SYNPY-1525] Verify expiration param in url (#1139)
* Verify expiration param in url
1 parent 1918ec2 commit 56ebcd0

File tree

4 files changed

+82
-4
lines changed

4 files changed

+82
-4
lines changed

synapseclient/core/download/download_async.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def _generate_chunk_ranges(
214214
def _pre_signed_url_expiration_time(url: str) -> datetime:
215215
"""
216216
Returns time at which a presigned url will expire
217+
for an AWS S3 pre-signed url
217218
218219
Arguments:
219220
url: A pre-signed download url from AWS

synapseclient/core/download/download_functions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,10 @@ def _ftp_report_hook(
743743
)
744744

745745
try:
746-
url_has_expiration = "Expires" in urllib_urlparse.urlparse(url).query
746+
url_query = urllib_urlparse.urlparse(url).query
747+
url_has_expiration = (
748+
"Expires" in url_query and "X-Amz-Expires" in url_query
749+
)
747750
url_is_expired = False
748751
if url_has_expiration:
749752
url_is_expired = datetime.datetime.now(
@@ -773,8 +776,9 @@ def _ftp_report_hook(
773776
exceptions._raise_for_status(response, verbose=client.debug)
774777
except SynapseHTTPError as err:
775778
if err.response.status_code == 403:
779+
url_query = urllib_urlparse.urlparse(url).query
776780
url_has_expiration = (
777-
"Expires" in urllib_urlparse.urlparse(url).query
781+
"Expires" in url_query and "X-Amz-Expires" in url_query
778782
)
779783
url_is_expired = False
780784
if url_has_expiration:

tests/unit/synapseclient/core/download/unit_test_download_async.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ async def test_pre_signed_url_expiration_time(self) -> None:
133133
"&X-Amz-Expires=86400"
134134
"&X-Amz-SignedHeaders=host"
135135
"&X-Amz-Signature=signature-value"
136+
"&Expires=1715000000"
136137
)
137138

138139
expected = (

tests/unit/synapseclient/core/unit_test_download.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,8 +834,16 @@ async def test_download_md5_mismatch_local_file(self) -> None:
834834
assert not mocked_remove.called
835835

836836
async def test_download_expired_url(self, syn: Synapse) -> None:
837-
url = "http://www.ayy.lmao/filerino.txt?Expires=0"
838-
new_url = "http://www.ayy.lmao/new_url.txt?Expires=1715000000"
837+
url = (
838+
"http://www.ayy.lmao/filerino.txt?Expires=0"
839+
"X-Amz-Date=20240509T180000Z"
840+
"&X-Amz-Expires=1000"
841+
)
842+
new_url = (
843+
"http://www.ayy.lmao/new_url.txt?Expires=1715000000"
844+
"X-Amz-Date=20240509T180000Z"
845+
"&X-Amz-Expires=1000"
846+
)
839847
contents = "\n".join(str(i) for i in range(1000))
840848
temp_destination = os.path.normpath(
841849
os.path.expanduser("~/fake/path/filerino.txt.temp")
@@ -899,6 +907,70 @@ async def test_download_expired_url(self, syn: Synapse) -> None:
899907
auth=None,
900908
)
901909

910+
async def test_download_no_aws_expiration(self, syn: Synapse) -> None:
911+
url = "http://www.ayy.lmao/filerino.txt?Expires=0"
912+
contents = "\n".join(str(i) for i in range(1000))
913+
temp_destination = os.path.normpath(
914+
os.path.expanduser("~/fake/path/filerino.txt.temp")
915+
)
916+
destination = os.path.normpath(os.path.expanduser("~/fake/path/filerino.txt"))
917+
918+
mock_requests_get = MockRequestGetFunction(
919+
[
920+
create_mock_response(
921+
url,
922+
"stream",
923+
contents=contents,
924+
buffer_size=1024,
925+
partial_end=len(contents),
926+
status_code=200,
927+
),
928+
]
929+
)
930+
with patch.object(
931+
syn._requests_session, "get", side_effect=mock_requests_get
932+
) as mocked_get, patch(
933+
"synapseclient.core.download.download_functions._pre_signed_url_expiration_time",
934+
return_value=datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc),
935+
) as mocked_pre_signed_url_expiration_time, patch(
936+
"synapseclient.core.download.download_functions.get_file_handle_for_download",
937+
) as mocked_get_file_handle_for_download, patch.object(
938+
Synapse, "_generate_headers", side_effect=mock_generate_headers
939+
), patch.object(
940+
utils, "temp_download_filename", return_value=temp_destination
941+
), patch(
942+
"synapseclient.core.download.download_functions.open",
943+
new_callable=mock_open(),
944+
create=True,
945+
), patch.object(
946+
hashlib, "new"
947+
) as mocked_hashlib_new, patch.object(
948+
shutil, "move"
949+
), patch.object(
950+
os, "remove"
951+
):
952+
mocked_hashlib_new.return_value.hexdigest.return_value = "fake md5 is fake"
953+
# WHEN I call download_from_url with an expired url
954+
download_from_url(
955+
url=url,
956+
destination=destination,
957+
entity_id=OBJECT_ID,
958+
file_handle_associate_type=OBJECT_TYPE,
959+
expected_md5="fake md5 is fake",
960+
)
961+
# I expect the expired url to be identified
962+
mocked_pre_signed_url_expiration_time.assert_not_called()
963+
# AND I expect the URL to be refreshed
964+
mocked_get_file_handle_for_download.assert_not_called()
965+
# AND I expect the download to be retried with the new URL
966+
mocked_get.assert_called_with(
967+
url=url,
968+
headers=mock_generate_headers(self),
969+
stream=True,
970+
allow_redirects=False,
971+
auth=None,
972+
)
973+
902974
async def test_download_url_no_expiration(self, syn: Synapse) -> None:
903975
url = "http://www.ayy.lmao/filerino.txt"
904976
contents = "\n".join(str(i) for i in range(1000))

0 commit comments

Comments
 (0)