diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 656c2fe951da..01d8f91508ee 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_098e26235c" + "Tag": "java/storage/azure-storage-blob_26d22d7fb8" } 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 12caac54befa..76803a1ed4b6 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 @@ -95,6 +95,8 @@ public class BlobSasImplUtil { private String encryptionScope; + private String delegatedUserObjectId; + /** * Creates a new {@link BlobSasImplUtil} with the specified parameters * @@ -140,6 +142,7 @@ public BlobSasImplUtil(BlobServiceSasSignatureValues sasValues, String container this.authorizedAadObjectId = sasValues.getPreauthorizedAgentObjectId(); this.correlationId = sasValues.getCorrelationId(); this.encryptionScope = encryptionScope; + this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId(); } /** @@ -262,6 +265,8 @@ private String encode(UserDelegationKey userDelegationKey, String signature) { tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_PREAUTHORIZED_AGENT_OBJECT_ID, this.authorizedAadObjectId); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CORRELATION_ID, this.correlationId); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID, + this.delegatedUserObjectId); } tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions); @@ -453,7 +458,7 @@ private String stringToSign(final UserDelegationKey key, String canonicalName) { this.authorizedAadObjectId == null ? "" : this.authorizedAadObjectId, "", /* suoid - empty since this applies to HNS only accounts. */ this.correlationId == null ? "" : this.correlationId, "", /* new schema 2025-07-05 */ - "", /* new schema 2025-07-05 */ + this.delegatedUserObjectId == null ? "" : this.delegatedUserObjectId, this.sasIpRange == null ? "" : this.sasIpRange.toString(), this.protocol == null ? "" : this.protocol.toString(), VERSION, resource, versionSegment == null ? "" : versionSegment, this.encryptionScope == null ? "" : this.encryptionScope, 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 113672d9522c..829bac396031 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 @@ -82,6 +82,7 @@ public final class BlobServiceSasSignatureValues { private String preauthorizedAgentObjectId; /* saoid */ private String correlationId; private String encryptionScope; + private String delegatedUserObjectId; /** * Creates an object with empty values for all fields. @@ -575,6 +576,30 @@ public BlobServiceSasSignatureValues setCorrelationId(String correlationId) { return this; } + /** + * Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user that is authorized to + * use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been + * issued to the user specified in this value. + * + * @return The Entra ID of the user that is authorized to use the resulting SAS URL. + */ + public String getDelegatedUserObjectId() { + return delegatedUserObjectId; + } + + /** + * Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user that is authorized to + * use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been + * issued to the user specified in this value. + * + * @param delegatedUserObjectId The Entra ID of the user that is authorized to use the resulting SAS URL. + * @return the updated BlobServiceSasSignatureValues object + */ + public BlobServiceSasSignatureValues setDelegatedUserObjectId(String delegatedUserObjectId) { + this.delegatedUserObjectId = delegatedUserObjectId; + return this; + } + /** * Uses an account's shared key credential to sign these signature values to produce the proper SAS query * parameters. @@ -754,6 +779,7 @@ private String stringToSign(final UserDelegationKey key, String canonicalName) { key.getSignedExpiry() == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedExpiry()), key.getSignedService() == null ? "" : key.getSignedService(), key.getSignedVersion() == null ? "" : key.getSignedVersion(), + this.delegatedUserObjectId == null ? "" : this.delegatedUserObjectId, this.sasIpRange == null ? "" : this.sasIpRange.toString(), this.protocol == null ? "" : this.protocol.toString(), VERSION_DEPRECATED_USER_DELEGATION_SAS_STRING_TO_SIGN, /* Pin down to version so old string to sign works. */ 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 ee19b6586f1c..27ad9d4836d8 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 @@ -772,15 +772,19 @@ protected void liveTestScenarioWithRetry(Runnable runnable) { } int retry = 0; - while (retry < 5) { + + // Try up to 5 times (4 retries + 1 final attempt) + while (retry < 4) { try { runnable.run(); - break; + return; // success } catch (Exception ex) { retry++; sleepIfRunningAgainstService(5000); } } + // Final attempt (5th try) + runnable.run(); } protected HttpPipelinePolicy getPerCallVersionPolicy() { 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 5cfba2918022..86b9c91304fc 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 @@ -4,10 +4,13 @@ package com.azure.storage.blob; import com.azure.core.credential.AzureSasCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.rest.Response; import com.azure.core.util.Context; import com.azure.core.util.FluxUtil; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; import com.azure.storage.blob.models.BlobContainerProperties; +import com.azure.storage.blob.models.BlobErrorCode; import com.azure.storage.blob.models.BlobItem; import com.azure.storage.blob.models.BlobProperties; import com.azure.storage.blob.models.BlobStorageException; @@ -30,6 +33,8 @@ import com.azure.storage.common.sas.CommonSasQueryParameters; import com.azure.storage.common.sas.SasIpRange; import com.azure.storage.common.sas.SasProtocol; +import com.azure.storage.common.test.shared.StorageCommonTestUtils; +import com.azure.storage.common.test.shared.extensions.LiveOnly; import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,6 +58,7 @@ import java.util.Map; import java.util.stream.Stream; +import static com.azure.storage.common.test.shared.StorageCommonTestUtils.getOidFromToken; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -196,6 +202,74 @@ public void blobSasUserDelegation() { }); } + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void blobSasUserDelegationDelegatedObjectId() { + liveTestScenarioWithRetry(() -> { + BlobSasPermission permissions = new BlobSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + + Mono> response = getUserDelegationInfo().flatMap(key -> { + String sas = sasClient.generateUserDelegationSas(sasValues, key); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. + BlockBlobAsyncClient client = instrument( + new BlobClientBuilder().endpoint(sasClient.getBlobUrl()).sasToken(sas).credential(tokenCredential)) + .buildAsyncClient() + .getBlockBlobAsyncClient(); + + return client.getPropertiesWithResponse(null); + }); + + StepVerifier.create(response).assertNext(r -> assertResponseStatusCode(r, 200)).verifyComplete(); + }); + } + + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void blobSasUserDelegationDelegatedObjectIdFail() { + liveTestScenarioWithRetry(() -> { + BlobSasPermission permissions = new BlobSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + + Mono> response = getUserDelegationInfo().flatMap(key -> { + String sas = sasClient.generateUserDelegationSas(sasValues, key); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. Token credential is not provided here, so the request should fail. + BlockBlobAsyncClient client + = instrument(new BlobClientBuilder().endpoint(sasClient.getBlobUrl()).sasToken(sas)) + .buildAsyncClient() + .getBlockBlobAsyncClient(); + + return client.getPropertiesWithResponse(null); + }); + + StepVerifier.create(response).verifyErrorSatisfies(e -> { + assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED); + }); + }); + } + @SuppressWarnings("deprecation") @Test public void blobSasSnapshot() { @@ -308,6 +382,73 @@ public void containerSasUserDelegation() { }); } + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void containerSasUserDelegationDelegatedObjectId() { + liveTestScenarioWithRetry(() -> { + BlobContainerSasPermission permissions = new BlobContainerSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + + Flux response = getUserDelegationInfo().flatMapMany(key -> { + String sas = ccAsync.generateUserDelegationSas(sasValues, key); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. + BlobContainerAsyncClient client + = instrument(new BlobContainerClientBuilder().endpoint(ccAsync.getBlobContainerUrl()) + .sasToken(sas) + .credential(tokenCredential)).buildAsyncClient(); + + return client.listBlobs(); + }); + + StepVerifier.create(response).expectNextCount(1).verifyComplete(); + }); + } + + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void containerSasUserDelegationDelegatedObjectIdFail() { + liveTestScenarioWithRetry(() -> { + BlobContainerSasPermission permissions = new BlobContainerSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + + Flux response = getUserDelegationInfo().flatMapMany(key -> { + String sas = ccAsync.generateUserDelegationSas(sasValues, key); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. Token credential is not provided here, so the request should fail. + BlobContainerAsyncClient client + = instrument(new BlobContainerClientBuilder().endpoint(ccAsync.getBlobContainerUrl()).sasToken(sas)) + .buildAsyncClient(); + + return client.listBlobs(); + }); + + StepVerifier.create(response).verifyErrorSatisfies(e -> { + assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED); + }); + }); + } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2019-12-12") @Test public void blobSasTags() { @@ -1013,7 +1154,7 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim OffsetDateTime keyStart, OffsetDateTime keyExpiry, String keyService, String keyVersion, String keyValue, SasIpRange ipRange, SasProtocol protocol, String snapId, String cacheControl, String disposition, String encoding, String language, String type, String versionId, String saoid, String cid, - String encryptionScope, String expectedStringToSign) { + String encryptionScope, String delegatedUserObjectId, String expectedStringToSign) { OffsetDateTime e = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); BlobSasPermission p = new BlobSasPermission().setReadPermission(true); BlobServiceSasSignatureValues v = new BlobServiceSasSignatureValues(e, p); @@ -1035,7 +1176,8 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim .setContentLanguage(language) .setContentType(type) .setPreauthorizedAgentObjectId(saoid) - .setCorrelationId(cid); + .setCorrelationId(cid) + .setDelegatedUserObjectId(delegatedUserObjectId); UserDelegationKey key = new UserDelegationKey().setSignedObjectId(keyOid) .setSignedTenantId(keyTid) @@ -1062,7 +1204,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup return Stream.of( Arguments.of(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1073,7 +1215,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, "11111111-1111-1111-1111-111111111111", null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1081,7 +1223,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, "22222222-2222-2222-2222-222222222222", null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1089,7 +1231,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1097,14 +1239,14 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n2018-01-01T00:00:00Z\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, "b", null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1112,96 +1254,103 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, "2018-06-17", "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n2018-06-17\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", - new SasIpRange(), null, null, null, null, null, null, null, null, null, null, null, + new SasIpRange(), null, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\nip\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - SasProtocol.HTTPS_ONLY, null, null, null, null, null, null, null, null, null, null, + SasProtocol.HTTPS_ONLY, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n" + SasProtocol.HTTPS_ONLY + "\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, "snapId", null, null, null, null, null, null, null, null, null, + null, "snapId", null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nbs\nsnapId\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, "control", null, null, null, null, null, null, null, null, + null, null, "control", null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\ncontrol\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, "disposition", null, null, null, null, null, null, null, + null, null, null, "disposition", null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\ndisposition\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, "encoding", null, null, null, null, null, null, + null, null, null, null, "encoding", null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\nencoding\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, "language", null, null, null, null, null, + null, null, null, null, null, "language", null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\nlanguage\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, "type", null, null, null, null, + null, null, null, null, null, null, "type", null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\ntype"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, "versionId", null, null, null, + null, null, null, null, null, null, null, "versionId", null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nbv\nversionId\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, "saoid", null, null, + null, null, null, null, null, null, null, null, "saoid", null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\nsaoid\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, "cid", null, + null, null, null, null, null, null, null, null, null, "cid", null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\ncid\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, null, "encryptionScope", + null, null, null, null, null, null, null, null, null, null, "encryptionScope", null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\nencryptionScope\n\n\n\n\n")); + + "\nb\n\nencryptionScope\n\n\n\n\n"), + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, "delegatedOid", + "r\n\n" + + Constants.ISO_8601_UTC_DATE_FORMATTER + .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n")); } @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-12-06") 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 ea2223591889..6e6a0f25d112 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 @@ -4,8 +4,11 @@ package com.azure.storage.blob; import com.azure.core.credential.AzureSasCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.rest.Response; import com.azure.core.util.Context; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; +import com.azure.storage.blob.models.BlobErrorCode; import com.azure.storage.blob.models.BlobProperties; import com.azure.storage.blob.models.BlobStorageException; import com.azure.storage.blob.models.UserDelegationKey; @@ -26,6 +29,8 @@ import com.azure.storage.common.sas.CommonSasQueryParameters; import com.azure.storage.common.sas.SasIpRange; import com.azure.storage.common.sas.SasProtocol; +import com.azure.storage.common.test.shared.StorageCommonTestUtils; +import com.azure.storage.common.test.shared.extensions.LiveOnly; import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,6 +49,7 @@ import java.util.Map; import java.util.stream.Stream; +import static com.azure.storage.common.test.shared.StorageCommonTestUtils.getOidFromToken; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -177,6 +183,64 @@ public void blobSasUserDelegation() { }); } + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void blobSasUserDelegationDelegatedObjectId() { + liveTestScenarioWithRetry(() -> { + BlobSasPermission permissions = new BlobSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + String sas = sasClient.generateUserDelegationSas(sasValues, getUserDelegationInfo()); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. + BlockBlobClient client = instrument( + new BlobClientBuilder().endpoint(sasClient.getBlobUrl()).sasToken(sas).credential(tokenCredential)) + .buildClient() + .getBlockBlobClient(); + + Response response = client.getPropertiesWithResponse(null, null, Context.NONE); + assertResponseStatusCode(response, 200); + }); + } + + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void blobSasUserDelegationDelegatedObjectIdFail() { + liveTestScenarioWithRetry(() -> { + BlobSasPermission permissions = new BlobSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + String sas = sasClient.generateUserDelegationSas(sasValues, getUserDelegationInfo()); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. Token credential is not provided here, so the request should fail. + BlockBlobClient client + = instrument(new BlobClientBuilder().endpoint(sasClient.getBlobUrl()).sasToken(sas)).buildClient() + .getBlockBlobClient(); + + BlobStorageException e = assertThrows(BlobStorageException.class, + () -> client.getPropertiesWithResponse(null, null, Context.NONE)); + assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED); + }); + } + @SuppressWarnings("deprecation") @Test public void blobSasSnapshot() { @@ -263,6 +327,63 @@ public void containerSasUserDelegation() { }); } + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void containerSasUserDelegationDelegatedObjectId() { + liveTestScenarioWithRetry(() -> { + BlobContainerSasPermission permissions = new BlobContainerSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + String sas = cc.generateUserDelegationSas(sasValues, getUserDelegationInfo()); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. + BlobContainerClient client = instrument(new BlobContainerClientBuilder().endpoint(cc.getBlobContainerUrl()) + .sasToken(sas) + .credential(tokenCredential)).buildClient(); + + assertDoesNotThrow(() -> client.listBlobs().iterator().hasNext()); + }); + } + + // RBAC replication lag + @Test + @LiveOnly + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-02-06") + public void containerSasUserDelegationDelegatedObjectIdFail() { + liveTestScenarioWithRetry(() -> { + + BlobContainerSasPermission permissions = new BlobContainerSasPermission().setReadPermission(true); + OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1); + + TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager); + + // We need to get the object ID from the token credential used to authenticate the request + String oid = getOidFromToken(tokenCredential); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid); + String sas = cc.generateUserDelegationSas(sasValues, getUserDelegationInfo()); + + // When a delegated user object ID is set, the client must be authenticated with both the SAS and the + // token credential. Token credential is not provided here, so the request should fail. + BlobContainerClient client + = instrument(new BlobContainerClientBuilder().endpoint(cc.getBlobContainerUrl()).sasToken(sas)) + .buildClient(); + + BlobStorageException e + = assertThrows(BlobStorageException.class, () -> client.listBlobs().iterator().hasNext()); + assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED); + }); + } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2019-12-12") @Test public void blobSasTags() { @@ -334,7 +455,7 @@ public void containerSasTagsFail() { .setCreatePermission(true) .setDeletePermission(true) .setAddPermission(true); - /* No tags permission. */ + /* No tagsPermission. */ OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); BlobServiceSasSignatureValues sasValues = new BlobServiceSasSignatureValues(expiryTime, permissions); @@ -943,7 +1064,7 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim OffsetDateTime keyStart, OffsetDateTime keyExpiry, String keyService, String keyVersion, String keyValue, SasIpRange ipRange, SasProtocol protocol, String snapId, String cacheControl, String disposition, String encoding, String language, String type, String versionId, String saoid, String cid, - String encryptionScope, String expectedStringToSign) { + String encryptionScope, String delegatedUserObjectId, String expectedStringToSign) { OffsetDateTime e = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); BlobSasPermission p = new BlobSasPermission().setReadPermission(true); BlobServiceSasSignatureValues v = new BlobServiceSasSignatureValues(e, p); @@ -965,7 +1086,8 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim .setContentLanguage(language) .setContentType(type) .setPreauthorizedAgentObjectId(saoid) - .setCorrelationId(cid); + .setCorrelationId(cid) + .setDelegatedUserObjectId(delegatedUserObjectId); UserDelegationKey key = new UserDelegationKey().setSignedObjectId(keyOid) .setSignedTenantId(keyTid) @@ -992,7 +1114,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup return Stream.of( Arguments.of(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1003,7 +1125,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, "11111111-1111-1111-1111-111111111111", null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1011,7 +1133,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, "22222222-2222-2222-2222-222222222222", null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1019,7 +1141,7 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1027,14 +1149,14 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n2018-01-01T00:00:00Z\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, "b", null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1042,96 +1164,103 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, "2018-06-17", "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n2018-06-17\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", - new SasIpRange(), null, null, null, null, null, null, null, null, null, null, null, + new SasIpRange(), null, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\nip\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - SasProtocol.HTTPS_ONLY, null, null, null, null, null, null, null, null, null, null, + SasProtocol.HTTPS_ONLY, null, null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n" + SasProtocol.HTTPS_ONLY + "\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, "snapId", null, null, null, null, null, null, null, null, null, + null, "snapId", null, null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nbs\nsnapId\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, "control", null, null, null, null, null, null, null, null, + null, null, "control", null, null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\ncontrol\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, "disposition", null, null, null, null, null, null, null, + null, null, null, "disposition", null, null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\ndisposition\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, "encoding", null, null, null, null, null, null, + null, null, null, null, "encoding", null, null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\nencoding\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, "language", null, null, null, null, null, + null, null, null, null, null, "language", null, null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\nlanguage\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, "type", null, null, null, null, + null, null, null, null, null, null, "type", null, null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\ntype"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, "versionId", null, null, null, + null, null, null, null, null, null, null, "versionId", null, null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nbv\nversionId\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, "saoid", null, null, + null, null, null, null, null, null, null, null, "saoid", null, null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\nsaoid\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, "cid", null, + null, null, null, null, null, null, null, null, null, "cid", null, null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\ncid\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, null, "encryptionScope", + null, null, null, null, null, null, null, null, null, null, "encryptionScope", null, "r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\nencryptionScope\n\n\n\n\n")); + + "\nb\n\nencryptionScope\n\n\n\n\n"), + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, "delegatedOid", + "r\n\n" + + Constants.ISO_8601_UTC_DATE_FORMATTER + .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + + "\n/blob/%s/containerName/blobName\n\n\n\n\n\n\n\n\n\n\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n")); } @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-12-06") diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java index d2d096f9f390..65f05cd60904 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java @@ -344,6 +344,11 @@ public static final class UrlConstants { */ public static final String SAS_SIGNATURE = "sig"; + /** + * The SAS delegated user object id parameter. + */ + public static final String SAS_DELEGATED_USER_OBJECT_ID = "sduoid"; + /** * The SAS encryption scope parameter. */ diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/sas/CommonSasQueryParameters.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/sas/CommonSasQueryParameters.java index aa54d3623836..f4a526e50212 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/sas/CommonSasQueryParameters.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/sas/CommonSasQueryParameters.java @@ -45,6 +45,7 @@ public class CommonSasQueryParameters { private final String unauthorizedObjectId; private final String correlationId; private final String encryptionScope; + private final String delegatedUserObjectId; /** * Creates a new {@link CommonSasQueryParameters} object. @@ -108,6 +109,8 @@ public CommonSasQueryParameters(Map queryParamsMap, boolean re removeSasParametersFromMap, Integer::parseInt); this.encryptionScope = getQueryParameter(queryParamsMap, Constants.UrlConstants.SAS_ENCRYPTION_SCOPE, removeSasParametersFromMap); + this.delegatedUserObjectId = getQueryParameter(queryParamsMap, + Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID, removeSasParametersFromMap); } /** @@ -168,6 +171,8 @@ public String encode() { SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_IP_RANGE, this.sasIpRange); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, this.signature); + SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID, + this.delegatedUserObjectId); // Account SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SERVICES, this.services); @@ -466,4 +471,15 @@ public String getCorrelationId() { public String getEncryptionScope() { return encryptionScope; } + + /** + * Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user that is authorized to + * use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been + * issued to the user specified in this value. + * + * @return The Entra ID of the user that is authorized to use the resulting SAS URL. + */ + public String getDelegatedUserObjectId() { + return delegatedUserObjectId; + } } diff --git a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/StorageCommonTestUtils.java b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/StorageCommonTestUtils.java index a57842ce582b..f6790e20d37d 100644 --- a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/StorageCommonTestUtils.java +++ b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/StorageCommonTestUtils.java @@ -3,7 +3,9 @@ package com.azure.storage.common.test.shared; import com.azure.core.client.traits.HttpTrait; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpClient; import com.azure.core.http.netty.NettyAsyncHttpClientProvider; import com.azure.core.http.okhttp.OkHttpAsyncClientProvider; @@ -43,8 +45,11 @@ import java.util.Random; import java.util.UUID; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.CRC32; +import static java.util.Base64.getUrlDecoder; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -389,4 +394,32 @@ public static TokenCredential getTokenCredential(InterceptorManager interceptorM return builder.build(); } } + + /** + * Extracts the OID (Object ID) from a token. + * + * @param credential The TokenCredential to extract the OID from. + * @return The OID extracted from the token. + */ + public static String getOidFromToken(TokenCredential credential) { + AccessToken accessToken + = credential.getTokenSync(new TokenRequestContext().addScopes("https://storage.azure.com/.default")); + String[] chunks = accessToken.getToken().split("\\."); + if (chunks.length < 2) { + throw new RuntimeException("Malformed JWT: expected at least 2 parts, got " + chunks.length); + } + String payload; + try { + payload = new String(getUrlDecoder().decode(chunks[1]), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Malformed JWT: payload is not valid base64url", e); + } + + Pattern pattern = Pattern.compile("\"oid\":\"(.*?)\""); + Matcher matcher = pattern.matcher(payload); + if (matcher.find()) { + return matcher.group(1); + } + throw new RuntimeException("Could not find oid in token"); + } }