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..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 @@ -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 != 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); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature); @@ -282,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: @@ -303,6 +317,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 +335,17 @@ 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); @@ -507,4 +534,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..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 @@ -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")); + } + + /** + * 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 47b3259e9539..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); @@ -1283,17 +1264,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 +1395,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 +1460,108 @@ 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) { + 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) { + 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() { + 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); + }); + } + }