Skip to content
Open
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
1 change: 1 addition & 0 deletions sdk/storage/azure-storage-blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
28 changes: 28 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_common_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions sdk/storage/azure-storage-file-datalake/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,12 @@ def generate_directory_sas(
:return: A Shared Access Signature (sas) token.
:rtype: str
"""
depth = len(directory_name.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,
Expand Down
32 changes: 32 additions & 0 deletions sdk/storage/azure-storage-file-datalake/tests/test_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,38 @@ 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()

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),
)

# sdd must match the depth of the canonicalized resource, not the raw name
assert "sdd=2" in token

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):
Expand Down
3 changes: 3 additions & 0 deletions sdk/storage/azure-storage-file-share/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions sdk/storage/azure-storage-file-share/tests/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down