From 6aca064d0845207a4950a4d38de0d65a5edcb403 Mon Sep 17 00:00:00 2001
From: Isabelle
Date: Sun, 15 Mar 2026 15:52:36 -0700
Subject: [PATCH 1/4] wip
---
.../implementation/util/BlobSasImplUtil.java | 28 +++++++++++++++
.../sas/BlobServiceSasSignatureValues.java | 22 ++++++++++++
.../blob/BlobServiceSasModelsTests.java | 33 ++++++++++++------
.../azure/storage/blob/SasClientTests.java | 34 +++++++++++++++----
4 files changed, 99 insertions(+), 18 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
index d375ef0d802a..39330dc61fa8 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
@@ -54,6 +54,11 @@ public class BlobSasImplUtil {
*/
private static final String SAS_CONTAINER_CONSTANT = "c";
+ /**
+ * The SAS blob directory constant.
+ */
+ private static final String SAS_BLOB_DIRECTORY_CONSTANT = "d";
+
private static final ClientLogger LOGGER = new ClientLogger(BlobSasImplUtil.class);
private static final String VERSION = Configuration.getGlobalConfiguration()
@@ -81,6 +86,8 @@ public class BlobSasImplUtil {
private String delegatedUserObjectId;
private Map requestHeaders;
private Map requestQueryParameters;
+ private Boolean isDirectory;
+ private Integer directoryDepth;
/**
* Creates a new {@link BlobSasImplUtil} with the specified parameters
@@ -130,6 +137,7 @@ public BlobSasImplUtil(BlobServiceSasSignatureValues sasValues, String container
this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId();
this.requestHeaders = sasValues.getRequestHeaders();
this.requestQueryParameters = sasValues.getRequestQueryParameters();
+ this.isDirectory = sasValues.isDirectory();
}
/**
@@ -256,6 +264,11 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID,
this.delegatedUserObjectId);
}
+
+ if (this.isDirectory) {
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH, this.directoryDepth);
+ }
+
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature);
@@ -303,6 +316,8 @@ public void ensureState() {
resource = SAS_BLOB_SNAPSHOT_CONSTANT;
} else if (versionId != null) {
resource = SAS_BLOB_VERSION_CONSTANT;
+ } else if (isDirectory != null && isDirectory) {
+ resource = SAS_BLOB_DIRECTORY_CONSTANT;
} else {
resource = SAS_BLOB_CONSTANT;
}
@@ -319,6 +334,15 @@ public void ensureState() {
permissions = BlobContainerSasPermission.parse(permissions).toString();
break;
+ case SAS_BLOB_DIRECTORY_CONSTANT:
+ if (!blobName.equalsIgnoreCase("/")) {
+ directoryDepth = blobName.trim().replaceAll("^/+|/+$", "").split("/").length;
+ } else {
+ directoryDepth = 0;
+ }
+ permissions = BlobSasPermission.parse(permissions).toString();
+ break;
+
default:
// We won't reparse the permissions if we don't know the type.
LOGGER.info("Not re-parsing permissions. Resource type '{}' is unknown.", resource);
@@ -507,4 +531,8 @@ public String getResource() {
public String getPermissions() {
return this.permissions;
}
+
+ public Integer getDirectoryDepth() {
+ return directoryDepth;
+ }
}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
index 7e2aee2f3296..7ba4906d50c7 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
@@ -86,6 +86,7 @@ public final class BlobServiceSasSignatureValues {
private String delegatedUserObjectId;
private Map requestHeaders;
private Map requestQueryParameters;
+ private Boolean isDirectory;
/**
* Creates an object with empty values for all fields.
@@ -655,6 +656,27 @@ public BlobServiceSasSignatureValues setRequestQueryParameters(Map ensureStateResourceAndPermissionSupplier() {
return Stream.of(
- Arguments.of("container", null, null, null,
+ // container , blob , snapshot , versionId , isDirectory , directoryDepth , containerSasPermission , blobSasPermission , resource , permissionString
+ Arguments.of("container", null, null, null, false, null,
new BlobContainerSasPermission().setReadPermission(true).setListPermission(true), null, "c", "rl"),
- Arguments.of("container", "blob", null, null, null, new BlobSasPermission().setReadPermission(true), "b",
- "r"),
- Arguments.of("container", "blob", "snapshot", null, null, new BlobSasPermission().setReadPermission(true),
- "bs", "r"),
- Arguments.of("container", "blob", null, "version", null, new BlobSasPermission().setReadPermission(true),
- "bv", "r"));
+ Arguments.of("container", "blob", null, null, false, null, null, new BlobSasPermission().setReadPermission(true),
+ "b", "r"),
+ Arguments.of("container", "blob", "snapshot", null, false, null, null,
+ new BlobSasPermission().setReadPermission(true), "bs", "r"),
+ Arguments.of("container", "blob", null, "version", false, null, null,
+ new BlobSasPermission().setReadPermission(true), "bv", "r"),
+ Arguments.of("container", "foo/bar/hello", null, null, true, 3, null,
+ new BlobSasPermission().setReadPermission(true), "d", "r"),
+ Arguments.of("container", "foo/bar", null, null, true, 2, null,
+ new BlobSasPermission().setReadPermission(true), "d", "r"),
+ Arguments.of("container", "foo/", null, null, true, 1, null,
+ new BlobSasPermission().setReadPermission(true), "d", "r"),
+ Arguments.of("container", "/", null, null, true, 0, null,
+ new BlobSasPermission().setReadPermission(true), "d", "r"));
}
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
index 47b3259e9539..1406e84c3625 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
@@ -1114,6 +1114,12 @@ public void canUseSasToAuthenticate() {
.getProperties());
}
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ public void sasQueryParametersDirectory() {
+
+ }
+
// RBAC replication lag
@Test
@LiveOnly
@@ -1283,17 +1289,21 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim
@ParameterizedTest
@MethodSource("blobSasImplUtilCanonicalizedResourceSupplier")
public void blobSasImplUtilCanonicalizedResource(String containerName, String blobName, String snapId,
- OffsetDateTime expiryTime, String expectedResource, String expectedStringToSign) {
- BlobServiceSasSignatureValues v = new BlobServiceSasSignatureValues(expiryTime, new BlobSasPermission());
+ OffsetDateTime expiryTime, Boolean isDirectory, String expectedResource, String expectedStringToSign) {
+ BlobServiceSasSignatureValues v
+ = new BlobServiceSasSignatureValues(expiryTime, new BlobSasPermission()).setDirectory(isDirectory);
BlobSasImplUtil implUtil = new BlobSasImplUtil(v, containerName, blobName, snapId, null, null);
expectedStringToSign = String.format(expectedStringToSign,
Constants.ISO_8601_UTC_DATE_FORMATTER.format(expiryTime), ENVIRONMENT.getPrimaryAccount().getName());
- String token = implUtil.generateSas(ENVIRONMENT.getPrimaryAccount().getCredential(), Context.NONE);
+ ArrayList stringToSign = new ArrayList<>();
+ String token
+ = implUtil.generateSas(ENVIRONMENT.getPrimaryAccount().getCredential(), stringToSign::add, Context.NONE);
CommonSasQueryParameters queryParams = new CommonSasQueryParameters(SasImplUtils.parseQueryString(token), true);
+ assertEquals(expectedStringToSign, stringToSign.get(0), "String-to-sign mismatch");
assertEquals(queryParams.getSignature(),
ENVIRONMENT.getPrimaryAccount().getCredential().computeHmac256(expectedStringToSign));
assertEquals(expectedResource, queryParams.getResource());
@@ -1410,12 +1420,14 @@ public void commitBlockListWithCreatePermission() {
private static Stream blobSasImplUtilCanonicalizedResourceSupplier() {
return Stream.of(
- Arguments.of("c", "b", "id", OffsetDateTime.now(), "bs",
+ Arguments.of("c", "b", "id", OffsetDateTime.now(), false, "bs",
"\n\n%s\n" + "/blob/%s/c/b\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nbs\nid\n\n\n\n\n\n"),
- Arguments.of("c", "b", null, OffsetDateTime.now(), "b",
+ Arguments.of("c", "b", null, OffsetDateTime.now(), false, "b",
"\n\n%s\n" + "/blob/%s/c/b\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"),
- Arguments.of("c", null, null, OffsetDateTime.now(), "c",
- "\n\n%s\n" + "/blob/%s/c\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nc\n\n\n\n\n\n\n"));
+ Arguments.of("c", null, null, OffsetDateTime.now(), false, "c",
+ "\n\n%s\n" + "/blob/%s/c\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nc\n\n\n\n\n\n\n"),
+ Arguments.of("c", "foo/bar/hello", null, OffsetDateTime.now(), true, "d",
+ "\n\n%s\n" + "/blob/%s/c/foo/bar/hello\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nd\n\n\n\n\n\n\n"));
}
@RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-12-06")
@@ -1473,4 +1485,12 @@ private static Stream accountSasImplUtilStringToSignSupplier() {
.format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC))
+ "\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nencryptionScope\n"));
}
+
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @ParameterizedTest
+ @ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
+ public void directorySasAllPermissions(String blobName) {
+ //wip
+ }
+
}
From 48d66d9efe8d1c53ea946e26e956d512db9b90bc Mon Sep 17 00:00:00 2001
From: Isabelle
Date: Thu, 19 Mar 2026 13:48:20 -0700
Subject: [PATCH 2/4] more tests
---
.../implementation/util/BlobSasImplUtil.java | 5 +-
.../blob/BlobServiceSasModelsTests.java | 12 +-
.../azure/storage/blob/SasClientTests.java | 120 +++++++++++++++++-
3 files changed, 127 insertions(+), 10 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
index 39330dc61fa8..93bcbc27efd6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
@@ -265,9 +265,8 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
this.delegatedUserObjectId);
}
- if (this.isDirectory) {
- tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH, this.directoryDepth);
- }
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH,
+ this.isDirectory != null && this.isDirectory ? this.directoryDepth : 0);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions);
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
index d94c9f51b464..4c0839fa9ee8 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
@@ -188,8 +188,8 @@ public void ensureStateIllegalArgument() {
@ParameterizedTest
@MethodSource("ensureStateResourceAndPermissionSupplier")
public void ensureStateResourceAndPermission(String container, String blob, String snapshot, String versionId,
- boolean isDirectory, Integer directoryDepth, BlobContainerSasPermission blobContainerSasPermission, BlobSasPermission blobSasPermission,
- String resource, String permissionString) {
+ boolean isDirectory, Integer directoryDepth, BlobContainerSasPermission blobContainerSasPermission,
+ BlobSasPermission blobSasPermission, String resource, String permissionString) {
OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1);
BlobServiceSasSignatureValues values = blobContainerSasPermission != null
@@ -209,8 +209,8 @@ private static Stream ensureStateResourceAndPermissionSupplier() {
// container , blob , snapshot , versionId , isDirectory , directoryDepth , containerSasPermission , blobSasPermission , resource , permissionString
Arguments.of("container", null, null, null, false, null,
new BlobContainerSasPermission().setReadPermission(true).setListPermission(true), null, "c", "rl"),
- Arguments.of("container", "blob", null, null, false, null, null, new BlobSasPermission().setReadPermission(true),
- "b", "r"),
+ Arguments.of("container", "blob", null, null, false, null, null,
+ new BlobSasPermission().setReadPermission(true), "b", "r"),
Arguments.of("container", "blob", "snapshot", null, false, null, null,
new BlobSasPermission().setReadPermission(true), "bs", "r"),
Arguments.of("container", "blob", null, "version", false, null, null,
@@ -221,7 +221,7 @@ private static Stream ensureStateResourceAndPermissionSupplier() {
new BlobSasPermission().setReadPermission(true), "d", "r"),
Arguments.of("container", "foo/", null, null, true, 1, null,
new BlobSasPermission().setReadPermission(true), "d", "r"),
- Arguments.of("container", "/", null, null, true, 0, null,
- new BlobSasPermission().setReadPermission(true), "d", "r"));
+ Arguments.of("container", "/", null, null, true, 0, null, new BlobSasPermission().setReadPermission(true),
+ "d", "r"));
}
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
index 1406e84c3625..9d56d215549d 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
@@ -1490,7 +1490,125 @@ private static Stream accountSasImplUtilStringToSignSupplier() {
@ParameterizedTest
@ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
public void directorySasAllPermissions(String blobName) {
- //wip
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Generate a SAS token for the directory itself.
+ BlobClient blobClient
+ = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(), cc.getBlobContainerUrl(), blobName);
+ String sasToken = blobClient.generateSas(sasValues);
+
+ // Test using same name as SAS
+ AppendBlobClient appendBlobClient1
+ = getBlobClient(sasToken, cc.getBlobContainerUrl(), blobName).getAppendBlobClient();
+ appendBlobClient1.create();
+
+ // Test using SAS name + suffix
+ AppendBlobClient appendBlobClient2
+ = getBlobClient(sasToken, cc.getBlobContainerUrl(), blobName + "/test").getAppendBlobClient();
+ appendBlobClient2.create();
+ }
+
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void directorySasAllPermissionsFail() {
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Create SAS for a deeper directory.
+ String sasDirectoryName = "foo/bar/hello";
+ BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(), cc.getBlobContainerUrl(),
+ sasDirectoryName);
+ String sasToken = blobClient.generateSas(sasValues);
+
+ // Act: use a blob name that is not a prefix of the name in the SAS.
+ AppendBlobClient appendBlobFailClient
+ = getBlobClient(sasToken, cc.getBlobContainerUrl(), "foo/bar").getAppendBlobClient();
+
+ BlobStorageException ex = assertThrows(BlobStorageException.class, appendBlobFailClient::create);
+ assertExceptionStatusCodeAndMessage(ex, 403, BlobErrorCode.AUTHENTICATION_FAILED);
+ }
+
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @ParameterizedTest
+ @ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
+ public void directoryIdentitySasAllPermissions(String blobName) {
+ BlobServiceClient oauthService = getOAuthServiceClient();
+
+ String identityContainerName = generateContainerName();
+ BlobContainerClient identityContainerClient = oauthService.getBlobContainerClient(identityContainerName);
+ identityContainerClient.createIfNotExists();
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Generate a user delegation SAS token for the directory.
+ BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), blobName);
+ String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // Test using same name as SAS
+ AppendBlobClient appendBlobClient1
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName).getAppendBlobClient();
+ appendBlobClient1.create();
+
+ // Test using SAS name + suffix
+ AppendBlobClient appendBlobClient2
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName + "/test")
+ .getAppendBlobClient();
+ appendBlobClient2.create();
+ }
+
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void directoryIdentitySasAllPermissionsFail() {
+ BlobServiceClient oauthService = getOAuthServiceClient();
+
+ String identityContainerName = generateContainerName();
+ BlobContainerClient identityContainerClient = oauthService.getBlobContainerClient(identityContainerName);
+ identityContainerClient.createIfNotExists();
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Create SAS for a deeper directory.
+ String sasDirectoryName = "foo/bar/hello";
+ BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), sasDirectoryName);
+ String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // Act: use a blob name that is not a prefix of the name in the SAS.
+ AppendBlobClient appendBlobFailClient
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), "foo/bar").getAppendBlobClient();
+
+ BlobStorageException ex = assertThrows(BlobStorageException.class, appendBlobFailClient::create);
+ assertExceptionStatusCodeAndMessage(ex, 403, BlobErrorCode.AUTHENTICATION_FAILED);
+ }
+
+ private BlobSasPermission getAllBlobSasPermissions() {
+ BlobSasPermission allPermissions = new BlobSasPermission().setReadPermission(true)
+ .setWritePermission(true)
+ .setCreatePermission(true)
+ .setDeletePermission(true)
+ .setAddPermission(true)
+ .setListPermission(true);
+
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2019-12-12") >= 0) {
+ allPermissions.setMovePermission(true)
+ .setExecutePermission(true)
+ .setDeleteVersionPermission(true)
+ .setTagsPermission(true);
+ }
+
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2020-02-10") >= 0) {
+ allPermissions.setPermanentDeletePermission(true);
+ }
+
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2020-06-12") >= 0) {
+ allPermissions.setImmutabilityPolicyPermission(true);
+ }
+
+ return allPermissions;
}
}
From 02a3609e52c9ab0377498508b1a4120e2e10afb3 Mon Sep 17 00:00:00 2001
From: Isabelle
Date: Thu, 19 Mar 2026 16:14:14 -0700
Subject: [PATCH 3/4] addressing copilot comments and adding async tests
---
.../implementation/util/BlobSasImplUtil.java | 28 +--
.../sas/BlobServiceSasSignatureValues.java | 8 +-
.../blob/BlobServiceSasModelsTests.java | 171 +++++++++++++++++-
.../com/azure/storage/blob/BlobTestBase.java | 31 ++++
.../storage/blob/SasAsyncClientTests.java | 124 +++++++++++--
.../azure/storage/blob/SasClientTests.java | 147 +++++----------
6 files changed, 375 insertions(+), 134 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
index 93bcbc27efd6..bf67aa92c415 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
@@ -265,8 +265,9 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
this.delegatedUserObjectId);
}
- tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH,
- this.isDirectory != null && this.isDirectory ? this.directoryDepth : 0);
+ if (this.isDirectory != null && this.isDirectory) {
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH, this.directoryDepth);
+ }
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions);
@@ -294,7 +295,8 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
* a. If "BlobName" is _not_ set, it is a container resource.
* b. Otherwise, if "SnapshotId" is set, it is a blob snapshot resource.
* c. Otherwise, if "VersionId" is set, it is a blob version resource.
- * d. Otherwise, it is a blob resource.
+ * d. Otherwise, if "IsDirectory" is set to true, it is a blob directory resource.
+ * e. Otherwise, it is a blob resource.
* 4. Reparse permissions depending on what the resource is. If it is an unrecognized resource, do nothing.
*
* Taken from:
@@ -333,21 +335,23 @@ public void ensureState() {
permissions = BlobContainerSasPermission.parse(permissions).toString();
break;
- case SAS_BLOB_DIRECTORY_CONSTANT:
- if (!blobName.equalsIgnoreCase("/")) {
- directoryDepth = blobName.trim().replaceAll("^/+|/+$", "").split("/").length;
- } else {
- directoryDepth = 0;
- }
- permissions = BlobSasPermission.parse(permissions).toString();
- break;
-
default:
// We won't reparse the permissions if we don't know the type.
LOGGER.info("Not re-parsing permissions. Resource type '{}' is unknown.", resource);
break;
}
}
+
+ if (resource.equals(SAS_BLOB_DIRECTORY_CONSTANT)) {
+ // Normalize backslashes to forward slashes to align directory depth with canonical name computation.
+ String normalizedBlobName = blobName.replace('\\', '/');
+ if (!normalizedBlobName.equalsIgnoreCase("/")) {
+ directoryDepth = normalizedBlobName.trim().replaceAll("^/+|/+$", "").split("/").length;
+ } else {
+ directoryDepth = 0;
+ }
+ permissions = BlobSasPermission.parse(permissions).toString();
+ }
}
/**
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
index 7ba4906d50c7..3878e9b58b1f 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/sas/BlobServiceSasSignatureValues.java
@@ -657,10 +657,10 @@ public BlobServiceSasSignatureValues setRequestQueryParameters(Map ensureStateResourceAndPermissionSupplier() {
return Stream.of(
- // container , blob , snapshot , versionId , isDirectory , directoryDepth , containerSasPermission , blobSasPermission , resource , permissionString
+ // container, blob, snapshot, versionId, isDirectory, directoryDepth, containerSasPermission,
+ // blobSasPermission, resource, permissionString
Arguments.of("container", null, null, null, false, null,
new BlobContainerSasPermission().setReadPermission(true).setListPermission(true), null, "c", "rl"),
Arguments.of("container", "blob", null, null, false, null, null,
@@ -224,4 +238,159 @@ private static Stream ensureStateResourceAndPermissionSupplier() {
Arguments.of("container", "/", null, null, true, 0, null, new BlobSasPermission().setReadPermission(true),
"d", "r"));
}
+
+ /**
+ * Validates encoded query parameters for a directory scoped blob SAS signed with the account key.
+ */
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void toSasQueryParametersDirectoryTest() {
+ String containerName = generateContainerName();
+ String blobName = "foo/bar/hello";
+
+ OffsetDateTime start = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC);
+ OffsetDateTime expiry = OffsetDateTime.of(2020, 1, 3, 3, 4, 5, 0, ZoneOffset.UTC);
+ SasIpRange ipRange = new SasIpRange().setIpMin("1.1.1.1").setIpMax("2.2.2.2");
+
+ BlobSasPermission permissions = getAllBlobSasPermissions();
+
+ BlobServiceSasSignatureValues sasValues
+ = new BlobServiceSasSignatureValues(expiry, permissions).setDirectory(true)
+ .setIdentifier("myidentifier")
+ .setStartTime(start)
+ .setProtocol(SasProtocol.HTTPS_HTTP)
+ .setSasIpRange(ipRange)
+ .setCacheControl("cache")
+ .setContentDisposition("disposition")
+ .setContentEncoding("encoding")
+ .setContentLanguage("language")
+ .setContentType("type");
+
+ BlobSasImplUtil implUtil = new BlobSasImplUtil(sasValues, containerName, blobName, null, null, null);
+
+ List stringToSignHolder = new ArrayList<>();
+ String sasToken = implUtil.generateSas(ENVIRONMENT.getPrimaryAccount().getCredential(), stringToSignHolder::add,
+ Context.NONE);
+ assertEquals(1, stringToSignHolder.size());
+ assertNotNull(stringToSignHolder.get(0));
+
+ CommonSasQueryParameters qp
+ = BlobUrlParts.parse("https://account.blob.core.windows.net/c?" + sasToken).getCommonSasQueryParameters();
+
+ String expectedSig = ENVIRONMENT.getPrimaryAccount().getCredential().computeHmac256(stringToSignHolder.get(0));
+
+ assertEquals(Constants.SAS_SERVICE_VERSION, qp.getVersion());
+ assertNull(qp.getServices());
+ assertNull(qp.getResourceTypes());
+ assertEquals(SasProtocol.HTTPS_HTTP, qp.getProtocol());
+ assertEquals(start, qp.getStartTime());
+ assertEquals(expiry, qp.getExpiryTime());
+ assertEquals(ipRange.getIpMin(), qp.getSasIpRange().getIpMin());
+ assertEquals(ipRange.getIpMax(), qp.getSasIpRange().getIpMax());
+ assertEquals("myidentifier", qp.getIdentifier());
+ assertEquals("d", qp.getResource());
+ assertEquals(3, qp.getDirectoryDepth());
+ assertEquals(permissions.toString(), qp.getPermissions());
+ assertEquals(expectedSig, qp.getSignature());
+ assertEquals("cache", qp.getCacheControl());
+ assertEquals("disposition", qp.getContentDisposition());
+ assertEquals("encoding", qp.getContentEncoding());
+ assertEquals("language", qp.getContentLanguage());
+ assertEquals("type", qp.getContentType());
+ }
+
+ /**
+ * Validates encoded query parameters for a directory scoped user delegation SAS, including delegated OID and request header/query key lists.
+ */
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void toSasQueryParametersDirectoryIdentityTest() {
+ String containerName = generateContainerName();
+ String blobName = "foo/bar/hello";
+ String accountName = ENVIRONMENT.getPrimaryAccount().getName();
+
+ OffsetDateTime start = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC);
+ OffsetDateTime expiry = OffsetDateTime.of(2020, 1, 3, 3, 4, 5, 0, ZoneOffset.UTC);
+ OffsetDateTime keyStart = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ OffsetDateTime keyExpiry = OffsetDateTime.of(2020, 1, 10, 0, 0, 0, 0, ZoneOffset.UTC);
+
+ SasIpRange ipRange = new SasIpRange().setIpMin("1.1.1.1").setIpMax("2.2.2.2");
+
+ BlobSasPermission permissions = getAllBlobSasPermissions();
+
+ Map requestHeaders = new TreeMap<>();
+ requestHeaders.put("a-header", "a-value");
+ requestHeaders.put("b-header", "b-value");
+
+ Map requestQueryParams = new TreeMap<>();
+ requestQueryParams.put("q-one", "1");
+ requestQueryParams.put("q-two", "2");
+
+ String delegatedOid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
+ String delegatedKeyTid = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
+
+ BlobServiceSasSignatureValues sasValues
+ = new BlobServiceSasSignatureValues(expiry, permissions).setDirectory(true)
+ .setStartTime(start)
+ .setProtocol(SasProtocol.HTTPS_HTTP)
+ .setSasIpRange(ipRange)
+ .setDelegatedUserObjectId(delegatedOid)
+ .setRequestHeaders(requestHeaders)
+ .setRequestQueryParameters(requestQueryParams)
+ .setCacheControl("cache")
+ .setContentDisposition("disposition")
+ .setContentEncoding("encoding")
+ .setContentLanguage("language")
+ .setContentType("type");
+
+ UserDelegationKey key = new UserDelegationKey().setSignedObjectId("keyOid")
+ .setSignedTenantId("keyTid")
+ .setSignedStart(keyStart)
+ .setSignedExpiry(keyExpiry)
+ .setSignedService("b")
+ .setSignedVersion("2019-02-02")
+ .setSignedDelegatedUserTenantId(delegatedKeyTid)
+ .setValue(ENVIRONMENT.getPrimaryAccount().getKey());
+
+ BlobSasImplUtil implUtil = new BlobSasImplUtil(sasValues, containerName, blobName, null, null, null);
+
+ List stringToSignHolder = new ArrayList<>();
+ String sasToken = implUtil.generateUserDelegationSas(key, accountName, stringToSignHolder::add, Context.NONE);
+ assertEquals(1, stringToSignHolder.size());
+ assertNotNull(stringToSignHolder.get(0));
+
+ CommonSasQueryParameters qp
+ = BlobUrlParts.parse("https://account.blob.core.windows.net/c?" + sasToken).getCommonSasQueryParameters();
+
+ String expectedSig = StorageImplUtils.computeHMac256(key.getValue(), stringToSignHolder.get(0));
+
+ assertEquals(Constants.SAS_SERVICE_VERSION, qp.getVersion());
+ assertNull(qp.getServices());
+ assertNull(qp.getResourceTypes());
+ assertEquals(SasProtocol.HTTPS_HTTP, qp.getProtocol());
+ assertEquals(start, qp.getStartTime());
+ assertEquals(expiry, qp.getExpiryTime());
+ assertEquals(ipRange.getIpMin(), qp.getSasIpRange().getIpMin());
+ assertEquals(ipRange.getIpMax(), qp.getSasIpRange().getIpMax());
+ assertNull(qp.getIdentifier());
+ assertEquals("keyOid", qp.getKeyObjectId());
+ assertEquals("keyTid", qp.getKeyTenantId());
+ assertEquals(keyStart, qp.getKeyStart());
+ assertEquals(keyExpiry, qp.getKeyExpiry());
+ assertEquals("b", qp.getKeyService());
+ assertEquals("2019-02-02", qp.getKeyVersion());
+ assertEquals(delegatedKeyTid, qp.getKeyDelegatedUserTenantId());
+ assertEquals("d", qp.getResource());
+ assertEquals(3, qp.getDirectoryDepth());
+ assertEquals(permissions.toString(), qp.getPermissions());
+ assertEquals(delegatedOid, qp.getDelegatedUserObjectId());
+ assertEquals(Arrays.asList("a-header", "b-header"), qp.getRequestHeaders());
+ assertEquals(Arrays.asList("q-one", "q-two"), qp.getRequestQueryParameters());
+ assertEquals(expectedSig, qp.getSignature());
+ assertEquals("cache", qp.getCacheControl());
+ assertEquals("disposition", qp.getContentDisposition());
+ assertEquals("encoding", qp.getContentEncoding());
+ assertEquals("language", qp.getContentLanguage());
+ assertEquals("type", qp.getContentType());
+ }
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java
index b1948f4df003..99d925bff60c 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java
@@ -50,6 +50,7 @@
import com.azure.storage.blob.models.ListBlobContainersOptions;
import com.azure.storage.blob.models.PublicAccessType;
import com.azure.storage.blob.options.BlobBreakLeaseOptions;
+import com.azure.storage.blob.sas.BlobSasPermission;
import com.azure.storage.blob.specialized.BlobAsyncClientBase;
import com.azure.storage.blob.specialized.BlobClientBase;
import com.azure.storage.blob.specialized.BlobLeaseAsyncClient;
@@ -285,6 +286,36 @@ protected String getBlockID() {
return Base64.getEncoder().encodeToString(testResourceNamer.randomUuid().getBytes(StandardCharsets.UTF_8));
}
+ /**
+ * Builds a {@link BlobSasPermission} with the broadest set of blob SAS permission flags supported for the current
+ * {@link Constants#SAS_SERVICE_VERSION}. Used by directory SAS and SAS query-parameter tests so permission strings
+ * stay consistent and version-gated in one place.
+ *
+ * @return permissions appropriate for the configured SAS service version
+ */
+ protected static BlobSasPermission getAllBlobSasPermissions() {
+ BlobSasPermission allPermissions = new BlobSasPermission().setReadPermission(true)
+ .setWritePermission(true)
+ .setCreatePermission(true)
+ .setDeletePermission(true)
+ .setAddPermission(true)
+ .setListPermission(true);
+
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2019-12-12") >= 0) {
+ allPermissions.setMovePermission(true)
+ .setExecutePermission(true)
+ .setDeleteVersionPermission(true)
+ .setTagsPermission(true);
+ }
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2020-02-10") >= 0) {
+ allPermissions.setPermanentDeletePermission(true);
+ }
+ if (Constants.SAS_SERVICE_VERSION.compareTo("2020-06-12") >= 0) {
+ allPermissions.setImmutabilityPolicyPermission(true);
+ }
+ return allPermissions;
+ }
+
/**
* This will retrieve the etag to be used in testing match conditions. The result will typically be assigned to
* the ifMatch condition when testing success and the ifNoneMatch condition when testing failure.
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java
index cdc576d7fb8b..ef0a3f9a6f58 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java
@@ -84,26 +84,7 @@ public void setup() {
@Test
public void blobSasAllPermissionsSuccess() {
// FE will reject a permission string it doesn't recognize
- BlobSasPermission allPermissions = new BlobSasPermission().setReadPermission(true)
- .setWritePermission(true)
- .setCreatePermission(true)
- .setDeletePermission(true)
- .setAddPermission(true)
- .setListPermission(true);
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2019-12-12") >= 0) {
- allPermissions.setMovePermission(true)
- .setExecutePermission(true)
- .setDeleteVersionPermission(true)
- .setTagsPermission(true);
- }
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-02-10") >= 0) {
- allPermissions.setPermanentDeletePermission(true);
- }
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-06-12") >= 0) {
- allPermissions.setImmutabilityPolicyPermission(true);
- }
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
BlobServiceSasSignatureValues sasValues = generateValues(allPermissions);
@@ -1612,4 +1593,107 @@ public void blobSasUserDelegationDelegatedTenantIdFail() {
});
}
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @ParameterizedTest
+ @ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
+ public void directorySasAllPermissions(String blobName) {
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ BlobAsyncClient blobAsyncClient = getBlobAsyncClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ ccAsync.getBlobContainerUrl(), blobName);
+ String sasToken = blobAsyncClient.generateSas(sasValues);
+
+ AppendBlobAsyncClient appendBlobClient1
+ = getBlobAsyncClient(sasToken, ccAsync.getBlobContainerUrl(), blobName, null).getAppendBlobAsyncClient();
+ AppendBlobAsyncClient appendBlobClient2
+ = getBlobAsyncClient(sasToken, ccAsync.getBlobContainerUrl(), blobName + "/test", null)
+ .getAppendBlobAsyncClient();
+
+ StepVerifier.create(appendBlobClient1.create().flatMap(ignored -> appendBlobClient2.create()).then())
+ .verifyComplete();
+ }
+
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void directorySasAllPermissionsFail() {
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ String sasDirectoryName = "foo/bar/hello";
+ BlobAsyncClient blobAsyncClient = getBlobAsyncClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ ccAsync.getBlobContainerUrl(), sasDirectoryName);
+ String sasToken = blobAsyncClient.generateSas(sasValues);
+
+ AppendBlobAsyncClient appendBlobFailClient
+ = getBlobAsyncClient(sasToken, ccAsync.getBlobContainerUrl(), "foo/bar", null).getAppendBlobAsyncClient();
+
+ StepVerifier.create(appendBlobFailClient.create())
+ .verifyErrorSatisfies(
+ e -> assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED));
+ }
+
+ // RBAC replication lag
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @ParameterizedTest
+ @ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
+ public void directoryIdentitySasAllPermissions(String blobName) {
+ liveTestScenarioWithRetry(() -> {
+ String identityContainerName = generateContainerName();
+ BlobContainerAsyncClient identityContainerClient
+ = getOAuthServiceAsyncClient().getBlobContainerAsyncClient(identityContainerName);
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ Mono response
+ = identityContainerClient.createIfNotExists().then(getUserDelegationInfo().flatMap(key -> {
+ BlobAsyncClient blobAsyncClient
+ = getBlobAsyncClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), blobName);
+ String sasToken = blobAsyncClient.generateUserDelegationSas(sasValues, key);
+ AppendBlobAsyncClient appendBlobClient1
+ = getBlobAsyncClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName, null)
+ .getAppendBlobAsyncClient();
+ AppendBlobAsyncClient appendBlobClient2
+ = getBlobAsyncClient(sasToken, identityContainerClient.getBlobContainerUrl(),
+ blobName + "/test", null).getAppendBlobAsyncClient();
+ return appendBlobClient1.create().flatMap(ignored -> appendBlobClient2.create()).then();
+ }));
+
+ StepVerifier.create(response).verifyComplete();
+ });
+ }
+
+ // RBAC replication lag
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
+ @Test
+ public void directoryIdentitySasAllPermissionsFail() {
+ liveTestScenarioWithRetry(() -> {
+ String identityContainerName = generateContainerName();
+ BlobContainerAsyncClient identityContainerClient
+ = getOAuthServiceAsyncClient().getBlobContainerAsyncClient(identityContainerName);
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+ String sasDirectoryName = "foo/bar/hello";
+
+ Mono response
+ = identityContainerClient.createIfNotExists().then(getUserDelegationInfo().flatMap(key -> {
+ BlobAsyncClient blobAsyncClient
+ = getBlobAsyncClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), sasDirectoryName);
+ String sasToken = blobAsyncClient.generateUserDelegationSas(sasValues, key);
+ AppendBlobAsyncClient appendBlobFailClient
+ = getBlobAsyncClient(sasToken, identityContainerClient.getBlobContainerUrl(), "foo/bar", null)
+ .getAppendBlobAsyncClient();
+ return appendBlobFailClient.create().then();
+ }));
+
+ StepVerifier.create(response)
+ .verifyErrorSatisfies(
+ e -> assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED));
+ });
+ }
+
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
index 9d56d215549d..281abda1c388 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java
@@ -76,26 +76,7 @@ public void setup() {
@Test
public void blobSasAllPermissionsSuccess() {
// FE will reject a permission string it doesn't recognize
- BlobSasPermission allPermissions = new BlobSasPermission().setReadPermission(true)
- .setWritePermission(true)
- .setCreatePermission(true)
- .setDeletePermission(true)
- .setAddPermission(true)
- .setListPermission(true);
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2019-12-12") >= 0) {
- allPermissions.setMovePermission(true)
- .setExecutePermission(true)
- .setDeleteVersionPermission(true)
- .setTagsPermission(true);
- }
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-02-10") >= 0) {
- allPermissions.setPermanentDeletePermission(true);
- }
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-06-12") >= 0) {
- allPermissions.setImmutabilityPolicyPermission(true);
- }
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
BlobServiceSasSignatureValues sasValues = generateValues(allPermissions);
@@ -1114,12 +1095,6 @@ public void canUseSasToAuthenticate() {
.getProperties());
}
- @Test
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
- public void sasQueryParametersDirectory() {
-
- }
-
// RBAC replication lag
@Test
@LiveOnly
@@ -1533,82 +1508,60 @@ public void directorySasAllPermissionsFail() {
@ParameterizedTest
@ValueSource(strings = { "foo", "foo/bar", "foo/bar/hello" })
public void directoryIdentitySasAllPermissions(String blobName) {
- BlobServiceClient oauthService = getOAuthServiceClient();
-
- String identityContainerName = generateContainerName();
- BlobContainerClient identityContainerClient = oauthService.getBlobContainerClient(identityContainerName);
- identityContainerClient.createIfNotExists();
-
- BlobSasPermission allPermissions = getAllBlobSasPermissions();
- BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
-
- // Generate a user delegation SAS token for the directory.
- BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
- identityContainerClient.getBlobContainerUrl(), blobName);
- String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
-
- // Test using same name as SAS
- AppendBlobClient appendBlobClient1
- = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName).getAppendBlobClient();
- appendBlobClient1.create();
-
- // Test using SAS name + suffix
- AppendBlobClient appendBlobClient2
- = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName + "/test")
- .getAppendBlobClient();
- appendBlobClient2.create();
+ liveTestScenarioWithRetry(() -> {
+ String identityContainerName = generateContainerName();
+ BlobContainerClient identityContainerClient
+ = getOAuthServiceClient().getBlobContainerClient(identityContainerName);
+ identityContainerClient.createIfNotExists();
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Generate a user delegation SAS token for the directory.
+ BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), blobName);
+ String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // Test using same name as SAS
+ AppendBlobClient appendBlobClient1
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName)
+ .getAppendBlobClient();
+ appendBlobClient1.create();
+
+ // Test using SAS name + suffix
+ AppendBlobClient appendBlobClient2
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), blobName + "/test")
+ .getAppendBlobClient();
+ appendBlobClient2.create();
+ });
}
@RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
@Test
public void directoryIdentitySasAllPermissionsFail() {
- BlobServiceClient oauthService = getOAuthServiceClient();
-
- String identityContainerName = generateContainerName();
- BlobContainerClient identityContainerClient = oauthService.getBlobContainerClient(identityContainerName);
- identityContainerClient.createIfNotExists();
-
- BlobSasPermission allPermissions = getAllBlobSasPermissions();
- BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
-
- // Create SAS for a deeper directory.
- String sasDirectoryName = "foo/bar/hello";
- BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
- identityContainerClient.getBlobContainerUrl(), sasDirectoryName);
- String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
-
- // Act: use a blob name that is not a prefix of the name in the SAS.
- AppendBlobClient appendBlobFailClient
- = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), "foo/bar").getAppendBlobClient();
-
- BlobStorageException ex = assertThrows(BlobStorageException.class, appendBlobFailClient::create);
- assertExceptionStatusCodeAndMessage(ex, 403, BlobErrorCode.AUTHENTICATION_FAILED);
- }
-
- private BlobSasPermission getAllBlobSasPermissions() {
- BlobSasPermission allPermissions = new BlobSasPermission().setReadPermission(true)
- .setWritePermission(true)
- .setCreatePermission(true)
- .setDeletePermission(true)
- .setAddPermission(true)
- .setListPermission(true);
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2019-12-12") >= 0) {
- allPermissions.setMovePermission(true)
- .setExecutePermission(true)
- .setDeleteVersionPermission(true)
- .setTagsPermission(true);
- }
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-02-10") >= 0) {
- allPermissions.setPermanentDeletePermission(true);
- }
-
- if (Constants.SAS_SERVICE_VERSION.compareTo("2020-06-12") >= 0) {
- allPermissions.setImmutabilityPolicyPermission(true);
- }
-
- return allPermissions;
+ liveTestScenarioWithRetry(() -> {
+ String identityContainerName = generateContainerName();
+ BlobContainerClient identityContainerClient
+ = getOAuthServiceClient().getBlobContainerClient(identityContainerName);
+ identityContainerClient.createIfNotExists();
+
+ BlobSasPermission allPermissions = getAllBlobSasPermissions();
+ BlobServiceSasSignatureValues sasValues = generateValues(allPermissions).setDirectory(true);
+
+ // Create SAS for a deeper directory.
+ String sasDirectoryName = "foo/bar/hello";
+ BlobClient blobClient = getBlobClient(ENVIRONMENT.getPrimaryAccount().getCredential(),
+ identityContainerClient.getBlobContainerUrl(), sasDirectoryName);
+ String sasToken = blobClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // Act: use a blob name that is not a prefix of the name in the SAS.
+ AppendBlobClient appendBlobFailClient
+ = getBlobClient(sasToken, identityContainerClient.getBlobContainerUrl(), "foo/bar")
+ .getAppendBlobClient();
+
+ BlobStorageException ex = assertThrows(BlobStorageException.class, appendBlobFailClient::create);
+ assertExceptionStatusCodeAndMessage(ex, 403, BlobErrorCode.AUTHENTICATION_FAILED);
+ });
}
}
From 1c734579bbde4ba2a7db127f28e7fa4014b114f7 Mon Sep 17 00:00:00 2001
From: Isabelle
Date: Thu, 19 Mar 2026 16:30:26 -0700
Subject: [PATCH 4/4] addressing more copilot comments
---
.../implementation/util/BlobSasImplUtil.java | 22 +++++++++----------
.../blob/BlobServiceSasModelsTests.java | 3 ++-
2 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
index bf67aa92c415..7573e77228a6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobSasImplUtil.java
@@ -335,23 +335,23 @@ public void ensureState() {
permissions = BlobContainerSasPermission.parse(permissions).toString();
break;
+ case SAS_BLOB_DIRECTORY_CONSTANT:
+ // Normalize backslashes to forward slashes to align directory depth with canonical name computation.
+ String normalizedBlobName = blobName.replace('\\', '/');
+ if (!normalizedBlobName.equalsIgnoreCase("/")) {
+ directoryDepth = normalizedBlobName.trim().replaceAll("^/+|/+$", "").split("/").length;
+ } else {
+ directoryDepth = 0;
+ }
+ permissions = BlobSasPermission.parse(permissions).toString();
+ break;
+
default:
// We won't reparse the permissions if we don't know the type.
LOGGER.info("Not re-parsing permissions. Resource type '{}' is unknown.", resource);
break;
}
}
-
- if (resource.equals(SAS_BLOB_DIRECTORY_CONSTANT)) {
- // Normalize backslashes to forward slashes to align directory depth with canonical name computation.
- String normalizedBlobName = blobName.replace('\\', '/');
- if (!normalizedBlobName.equalsIgnoreCase("/")) {
- directoryDepth = normalizedBlobName.trim().replaceAll("^/+|/+$", "").split("/").length;
- } else {
- directoryDepth = 0;
- }
- permissions = BlobSasPermission.parse(permissions).toString();
- }
}
/**
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
index ad402d49db74..c8186eaf40b9 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobServiceSasModelsTests.java
@@ -300,7 +300,8 @@ public void toSasQueryParametersDirectoryTest() {
}
/**
- * Validates encoded query parameters for a directory scoped user delegation SAS, including delegated OID and request header/query key lists.
+ * Validates encoded query parameters for a directory scoped user delegation SAS,
+ * including delegated OID and request header/query key lists.
*/
@RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
@Test