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