From b1dfc194f4d9c04127a59e33debb3a95b4feefc9 Mon Sep 17 00:00:00 2001 From: Anjali Ratnam Date: Thu, 3 Sep 2026 08:17:37 -0700 Subject: [PATCH 1/3] fixed sas issue --- sdk/storage/azure-storage-blob/CHANGELOG.md | 1 + .../storage/blob/_shared_access_signature.py | 1 + .../tests/test_common_blob.py | 28 +++++++++++++++ .../azure-storage-file-datalake/CHANGELOG.md | 3 ++ .../filedatalake/_shared_access_signature.py | 2 +- .../tests/test_directory.py | 36 +++++++++++++++++++ .../azure-storage-file-share/CHANGELOG.md | 3 ++ .../fileshare/_shared_access_signature.py | 1 + .../tests/test_file.py | 36 +++++++++++++++++++ 9 files changed, 110 insertions(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index 379ea2aa0d61..92d9fd7952f0 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -9,6 +9,7 @@ - Fixed an issue where `destination_snapshot` on a blob's copy properties was always `None` when listing blobs with `response_format="arrow"`. - Fixed an issue with the new generation where listing page ranges for an empty page blob could raise a `ValueError` instead of returning an empty list. +- Fixed an issue where a SAS generated for a blob name containing a backslash (`\`) was invalid because the backslash was not normalized to a forward slash when building the signed resource. ### Other Changes - Added public `SignedIdentifier` model and updated `ContainerClient.get_container_access_policy` diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py index 46cb94f9c886..a596b76b1840 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py @@ -160,6 +160,7 @@ def generate_blob( :return: A Shared Access Signature (sas) token. :rtype: str """ + blob_name = blob_name.replace("\\", "/") resource_path = container_name + "/" + blob_name sas = _BlobSharedAccessHelper() diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob.py b/sdk/storage/azure-storage-blob/tests/test_common_blob.py index 701aa434b842..7bf7aacef7c7 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -1995,6 +1995,34 @@ def test_sas_access_blob(self, **kwargs): # Assert assert self.byte_data == content + @pytest.mark.live_test_only + @BlobPreparer() + def test_sas_access_blob_name_with_backslash(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + blob_name = "dir\\file" + blob = self.bsc.get_blob_client(self.container_name, blob_name) + blob.upload_blob(self.byte_data, length=len(self.byte_data), overwrite=True) + + token = self.generate_sas( + generate_blob_sas, + blob.account_name, + blob.container_name, + blob.blob_name, + account_key=blob.credential.account_key, + permission=BlobSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + ) + + # Act + service = BlobClient.from_blob_url(blob.url, credential=token) + content = service.download_blob().readall() + + # Assert + assert self.byte_data == content + @pytest.mark.live_test_only @BlobPreparer() def test_sas_access_blob_snapshot(self, **kwargs): diff --git a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md index c9f1a8d6d08c..8c584a0f0f3b 100644 --- a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features Added +### Bugs Fixed +- Fixed an issue where a SAS generated for a path containing a backslash (`\`) was invalid because the backslash was not normalized to a forward slash when building the signed resource. + ## 12.26.0b1 (2026-08-10) ### Features Added diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py index 55d3451c9d76..671163c19693 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py @@ -339,7 +339,7 @@ def generate_directory_sas( :return: A Shared Access Signature (sas) token. :rtype: str """ - depth = len(directory_name.strip("/").split("/")) + depth = len(directory_name.replace("\\", "/").strip("/").split("/")) return generate_blob_sas( account_name=account_name, container_name=file_system_name, diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_directory.py b/sdk/storage/azure-storage-file-datalake/tests/test_directory.py index ef6fb8e182d2..9f4ea34a1f70 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_directory.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_directory.py @@ -1576,6 +1576,42 @@ def test_using_directory_sas_to_create_file(self, **kwargs): with pytest.raises(HttpResponseError): directory_client.delete_directory() + @pytest.mark.live_test_only + @DataLakePreparer() + def test_using_directory_sas_with_backslash(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + datalake_storage_account_key = kwargs.pop("datalake_storage_account_key") + + self._setUp(datalake_storage_account_name, datalake_storage_account_key) + # SAS URL is calculated from storage key, so this test runs live only + + directory_name = self._get_directory_reference() + "\\sub" + client = self.dsc.get_directory_client(self.file_system_name, directory_name) + client.create_directory() + + string_to_sign = [] + token = generate_directory_sas( + self.dsc.account_name, + self.file_system_name, + directory_name, + self.dsc.credential.account_key, + permission=DirectorySasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + sts_hook=string_to_sign.append, + ) + + # sdd must match the depth of the canonicalized resource, not the raw name + assert "sdd=2" in token + assert f"/blob/{self.dsc.account_name}/{self.file_system_name}/" in string_to_sign[0] + assert "\\" not in string_to_sign[0] + + directory_client = DataLakeDirectoryClient( + self.dsc.url, self.file_system_name, directory_name, credential=token + ) + access_control = directory_client.get_access_control() + + assert access_control is not None + @DataLakePreparer() @recorded_by_proxy def test_storage_account_audience_dir_client(self, **kwargs): diff --git a/sdk/storage/azure-storage-file-share/CHANGELOG.md b/sdk/storage/azure-storage-file-share/CHANGELOG.md index 20609dda96ef..10117721afa8 100644 --- a/sdk/storage/azure-storage-file-share/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-share/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features Added +### Bugs Fixed +- Fixed an issue where a SAS generated for a file path containing a backslash (`\`) was invalid because the backslash was not normalized to a forward slash when building the signed resource. + ## 12.27.0b1 (2026-08-10) ### Features Added diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared_access_signature.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared_access_signature.py index 0a7f24fa711b..c82ca3135082 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared_access_signature.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared_access_signature.py @@ -141,6 +141,7 @@ def generate_file( resource_path += "/" + str(directory_name) if file_name is not None: resource_path += "/" + str(file_name) + resource_path = resource_path.replace("\\", "/") sas = _FileSharedAccessHelper() sas.add_base(permission, expiry, start, ip, protocol, self.x_ms_version) diff --git a/sdk/storage/azure-storage-file-share/tests/test_file.py b/sdk/storage/azure-storage-file-share/tests/test_file.py index 1c5a3695e2bb..2afe2a35fd8f 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file.py @@ -3511,6 +3511,42 @@ def test_sas_access_file(self, **kwargs): # Assert assert self.short_byte_data == content + @pytest.mark.live_test_only + @FileSharePreparer() + def test_sas_access_file_name_with_backslash(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + share_client = self.fsc.get_share_client(self.share_name) + try: + share_client.create_directory("dir") + except ResourceExistsError: + pass + file_client = self._create_file("dir\\file") + + token = self.generate_sas( + generate_file_sas, + file_client.account_name, + file_client.share_name, + file_client.file_path, + file_client.credential.account_key, + permission=FileSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + ) + + # Act + file_client = ShareFileClient( + self.account_url(storage_account_name, "file"), + share_name=self.share_name, + file_path="dir\\file", + credential=token, + ) + content = file_client.download_file().readall() + + # Assert + assert self.short_byte_data == content + @FileSharePreparer() @recorded_by_proxy def test_sas_signed_identifier(self, **kwargs): From 1c3717fc429d64162e20b7eb8f9ccce7b1c9c6fa Mon Sep 17 00:00:00 2001 From: Anjali Ratnam Date: Thu, 3 Sep 2026 09:04:08 -0700 Subject: [PATCH 2/3] test updates --- .../azure-storage-file-datalake/tests/test_directory.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_directory.py b/sdk/storage/azure-storage-file-datalake/tests/test_directory.py index 9f4ea34a1f70..3cf7c0174d6f 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_directory.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_directory.py @@ -1589,7 +1589,6 @@ def test_using_directory_sas_with_backslash(self, **kwargs): client = self.dsc.get_directory_client(self.file_system_name, directory_name) client.create_directory() - string_to_sign = [] token = generate_directory_sas( self.dsc.account_name, self.file_system_name, @@ -1597,13 +1596,10 @@ def test_using_directory_sas_with_backslash(self, **kwargs): self.dsc.credential.account_key, permission=DirectorySasPermissions(read=True), expiry=datetime.utcnow() + timedelta(hours=1), - sts_hook=string_to_sign.append, ) # sdd must match the depth of the canonicalized resource, not the raw name assert "sdd=2" in token - assert f"/blob/{self.dsc.account_name}/{self.file_system_name}/" in string_to_sign[0] - assert "\\" not in string_to_sign[0] directory_client = DataLakeDirectoryClient( self.dsc.url, self.file_system_name, directory_name, credential=token From 709a9b969ac1245063d734d416cb673271bdb2f1 Mon Sep 17 00:00:00 2001 From: Anjali Ratnam Date: Thu, 3 Sep 2026 17:40:01 -0700 Subject: [PATCH 3/3] updates --- .../azure/storage/filedatalake/_shared_access_signature.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py index 671163c19693..a38025e41cea 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py @@ -339,7 +339,12 @@ def generate_directory_sas( :return: A Shared Access Signature (sas) token. :rtype: str """ - depth = len(directory_name.replace("\\", "/").strip("/").split("/")) + canonicalized_directory_name = directory_name.replace("\\", "/") + depth = ( + 0 + if canonicalized_directory_name in ["", "/"] + else len(canonicalized_directory_name.strip("/").split("/")) + ) return generate_blob_sas( account_name=account_name, container_name=file_system_name,