diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index a2bc9bdaa663..f9a5c195881a 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_f26563826e" + "Tag": "java/storage/azure-storage-blob_6631ad464e" } 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 76803a1ed4b6..a24f2ab762bf 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 @@ -20,15 +20,17 @@ import com.azure.storage.common.sas.SasProtocol; import java.time.OffsetDateTime; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import static com.azure.storage.common.implementation.SasImplUtils.formatQueryParameterDate; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestHeadersForSasSigning; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestQueryParametersForSasSigning; import static com.azure.storage.common.implementation.SasImplUtils.tryAppendQueryParameter; /** * This class provides helper methods for common blob service sas patterns. - * * RESERVED FOR INTERNAL USE. */ public class BlobSasImplUtil { @@ -58,44 +60,27 @@ public class BlobSasImplUtil { .get(Constants.PROPERTY_AZURE_STORAGE_SAS_SERVICE_VERSION, BlobServiceVersion.getLatest().getVersion()); private SasProtocol protocol; - private OffsetDateTime startTime; - private OffsetDateTime expiryTime; - private String permissions; - private SasIpRange sasIpRange; - private String containerName; - private String blobName; - private String resource; - private String snapshotId; - private String versionId; - private String identifier; - private String cacheControl; - private String contentDisposition; - private String contentEncoding; - private String contentLanguage; - private String contentType; - private String authorizedAadObjectId; - private String correlationId; - private String encryptionScope; - private String delegatedUserObjectId; + private Map requestHeaders; + private Map requestQueryParameters; /** * Creates a new {@link BlobSasImplUtil} with the specified parameters @@ -143,6 +128,8 @@ public BlobSasImplUtil(BlobServiceSasSignatureValues sasValues, String container this.correlationId = sasValues.getCorrelationId(); this.encryptionScope = encryptionScope; this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId(); + this.requestHeaders = sasValues.getRequestHeaders(); + this.requestQueryParameters = sasValues.getRequestQueryParameters(); } /** @@ -272,6 +259,10 @@ private String encode(UserDelegationKey userDelegationKey, String signature) { tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_ENCRYPTION_SCOPE, this.encryptionScope); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_HEADERS, + formatRequestHeadersForSasSigning(this.requestHeaders)); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_QUERY_PARAMETERS, + formatRequestQueryParametersForSasSigning(this.requestQueryParameters)); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CACHE_CONTROL, this.cacheControl); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_DISPOSITION, this.contentDisposition); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_ENCODING, this.contentEncoding); @@ -279,24 +270,23 @@ private String encode(UserDelegationKey userDelegationKey, String signature) { tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_TYPE, this.contentType); return sb.toString(); - } /** * Ensures that the builder's properties are in a consistent state. * - * 1. If there is no version, use latest. + *

1. If there is no version, use latest. * 2. If there is no identifier set, ensure expiryTime and permissions are set. * 3. Resource name is chosen by: * 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. - * 4. Reparse permissions depending on what the resource is. If it is an unrecognized resource, do nothing. + * 4. Reparse permissions depending on what the resource is. If it is an unrecognized resource, do nothing.

* * Taken from: - * https://github.com/Azure/azure-storage-blob-go/blob/master/azblob/sas_service.go#L33 - * https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs + * sas_service.go + * BlobSasBuilder.cs */ public void ensureState() { if (identifier == null) { @@ -443,6 +433,30 @@ private String stringToSign(final UserDelegationKey key, String canonicalName) { this.contentEncoding == null ? "" : this.contentEncoding, this.contentLanguage == null ? "" : this.contentLanguage, this.contentType == null ? "" : this.contentType); + } else if (VERSION.compareTo(BlobServiceVersion.V2026_02_06.getVersion()) <= 0) { + return String.join("\n", this.permissions == null ? "" : this.permissions, + this.startTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), + this.expiryTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), + canonicalName, key.getSignedObjectId() == null ? "" : key.getSignedObjectId(), + key.getSignedTenantId() == null ? "" : key.getSignedTenantId(), + key.getSignedStart() == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedStart()), + key.getSignedExpiry() == null + ? "" + : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedExpiry()), + key.getSignedService() == null ? "" : key.getSignedService(), + key.getSignedVersion() == null ? "" : key.getSignedVersion(), + this.authorizedAadObjectId == null ? "" : this.authorizedAadObjectId, + "", /* suoid - empty since this applies to HNS only accounts. */ + this.correlationId == null ? "" : this.correlationId, "", /* 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, + this.cacheControl == null ? "" : this.cacheControl, + this.contentDisposition == null ? "" : this.contentDisposition, + this.contentEncoding == null ? "" : this.contentEncoding, + this.contentLanguage == null ? "" : this.contentLanguage, + this.contentType == null ? "" : this.contentType); } else { return String.join("\n", this.permissions == null ? "" : this.permissions, this.startTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), @@ -462,6 +476,10 @@ private String stringToSign(final UserDelegationKey key, String canonicalName) { this.sasIpRange == null ? "" : this.sasIpRange.toString(), this.protocol == null ? "" : this.protocol.toString(), VERSION, resource, versionSegment == null ? "" : versionSegment, this.encryptionScope == null ? "" : this.encryptionScope, + this.requestHeaders == null ? "" : formatRequestHeadersForSasSigning(this.requestHeaders), + this.requestQueryParameters == null + ? "" + : formatRequestQueryParametersForSasSigning(this.requestQueryParameters), this.cacheControl == null ? "" : this.cacheControl, this.contentDisposition == null ? "" : this.contentDisposition, this.contentEncoding == null ? "" : this.contentEncoding, @@ -472,7 +490,8 @@ private String stringToSign(final UserDelegationKey key, String canonicalName) { /** * Gets the resource string for SAS token signing. - * @return + * + * @return The resource string. */ public String getResource() { return this.resource; @@ -480,7 +499,7 @@ public String getResource() { /** * Gets the permissions string for SAS token signing. - * @return + * @return The permissions string. */ public String getPermissions() { return this.permissions; 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 829bac396031..e57ce60a1303 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 @@ -16,6 +16,7 @@ import com.azure.storage.common.sas.SasProtocol; import java.time.OffsetDateTime; +import java.util.Map; /** * Used to initialize parameters for a Shared Access Signature (SAS) for an Azure Blob Storage service. Once all the @@ -83,6 +84,8 @@ public final class BlobServiceSasSignatureValues { private String correlationId; private String encryptionScope; private String delegatedUserObjectId; + private Map requestHeaders; + private Map requestQueryParameters; /** * Creates an object with empty values for all fields. @@ -600,6 +603,50 @@ public BlobServiceSasSignatureValues setDelegatedUserObjectId(String delegatedUs return this; } + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Headers to include in the SAS. + * Any usage of the SAS must include these headers and values in the request. + * + * @return The custom request headers to be set when the SAS is used. + */ + public Map getRequestHeaders() { + return requestHeaders; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Headers to include in the SAS. + * Any usage of the SAS must include these headers and values in the request. + * + * @param requestHeaders The custom request headers to be set when the SAS is used. + * @return the updated BlobServiceSasSignatureValues object + */ + public BlobServiceSasSignatureValues setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + return this; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Query Parameters to include in + * the SAS. Any usage of the SAS must include these query parameters and values in the request. + * + * @return The custom query parameters to be set when the SAS is used. + */ + public Map getRequestQueryParameters() { + return requestQueryParameters; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Query Parameters to include in + * the SAS. Any usage of the SAS must include these query parameters and values in the request. + * + * @param requestQueryParameters The custom query parameters to be set when the SAS is used. + * @return the updated BlobServiceSasSignatureValues object + */ + public BlobServiceSasSignatureValues setRequestQueryParameters(Map requestQueryParameters) { + this.requestQueryParameters = requestQueryParameters; + return this; + } + /** * Uses an account's shared key credential to sign these signature values to produce the proper SAS query * parameters. @@ -713,8 +760,8 @@ public BlobServiceSasQueryParameters generateSasQueryParameters(UserDelegationKe * 3. Reparse permissions depending on what the resource is. If it is an unrecognised resource, do nothing. *

* Taken from: - * https://github.com/Azure/azure-storage-blob-go/blob/master/azblob/sas_service.go#L33 - * https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs + * sas_service.go + * BlobSasBuilder.cs */ private void ensureState() { if (CoreUtils.isNullOrEmpty(blobName)) { 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 6e6a0f25d112..fdc2f0bd3fcb 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 @@ -45,7 +45,9 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Stream; @@ -1064,10 +1066,12 @@ 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 delegatedUserObjectId, String expectedStringToSign) { + String encryptionScope, String delegatedUserObjectId, Map requestHeaders, + Map requestQueryParameters, 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); + ArrayList stringToSign = new ArrayList<>(); String expected = String.format(expectedStringToSign, ENVIRONMENT.getPrimaryAccount().getName()); @@ -1087,7 +1091,9 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim .setContentType(type) .setPreauthorizedAgentObjectId(saoid) .setCorrelationId(cid) - .setDelegatedUserObjectId(delegatedUserObjectId); + .setDelegatedUserObjectId(delegatedUserObjectId) + .setRequestHeaders(requestHeaders) + .setRequestQueryParameters(requestQueryParameters); UserDelegationKey key = new UserDelegationKey().setSignedObjectId(keyOid) .setSignedTenantId(keyTid) @@ -1099,11 +1105,12 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim BlobSasImplUtil implUtil = new BlobSasImplUtil(v, "containerName", "blobName", snapId, versionId, encryptionScope); - String sasToken - = implUtil.generateUserDelegationSas(key, ENVIRONMENT.getPrimaryAccount().getName(), Context.NONE); + String sasToken = implUtil.generateUserDelegationSas(key, ENVIRONMENT.getPrimaryAccount().getName(), + stringToSign::add, Context.NONE); CommonSasQueryParameters token = BlobUrlParts.parse(cc.getBlobContainerUrl() + "?" + sasToken).getCommonSasQueryParameters(); + assertEqualsForEachLine(stringToSign, expected); assertEquals(token.getSignature(), StorageImplUtils.computeHMac256(key.getValue(), expected)); } @@ -1111,10 +1118,25 @@ public void blobSasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTim We test string to sign functionality directly related toUserDelegation sas specific parameters */ private static Stream blobSasImplUtilStringToSignUserDelegationKeySupplier() { + // Use LinkedHashMap to ensure deterministic iteration order + Map singleHeader = new LinkedHashMap<>(); + singleHeader.put("x-ms-encryption-key-sha256", "hashvalue"); + + Map singleQueryParam = new LinkedHashMap<>(); + singleQueryParam.put("comp", "blocklist"); + + Map multipleHeaders = new LinkedHashMap<>(); + multipleHeaders.put("x-ms-encryption-key-sha256", "hashvalue"); + multipleHeaders.put("x-ms-source-if-match", "etag"); + + Map multipleQueryParams = new LinkedHashMap<>(); + multipleQueryParams.put("blockid", "blockidvalue"); + multipleQueryParams.put("comp", "blocklist"); + 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, null, null, null, "r\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) @@ -1122,145 +1144,265 @@ private static Stream blobSasImplUtilStringToSignUserDelegationKeySup + 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\n"), + + "\nb\n\n\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, 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\n11111111-1111-1111-1111-111111111111\n\n\n\n\n\n\n\n\n\n\n\n\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, 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\n22222222-2222-2222-2222-222222222222\n\n\n\n\n\n\n\n\n\n\n\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, 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\n2018-01-01T00:00:00Z\n\n\n\n\n\n\n\n\n\n\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, 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"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, 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\nb\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\n\n\n\n"), + + "\nb\n\n\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, 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"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, + new SasIpRange(), 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\n\n\n\n\n\n\nip\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\n\n\n\n"), + + "\nb\n\n\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, null, + SasProtocol.HTTPS_ONLY, 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\n\n\n\n\n\n\n\n" + SasProtocol.HTTPS_ONLY + "\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, + null, "snapId", 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\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nbs\nsnapId\n\n\n\n\n\n"), + + "\nbs\nsnapId\n\n\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, null, "control", 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\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\ncontrol\n\n\n\n"), + + "\nb\n\n\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, null, "disposition", 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 - + "\nb\n\n\n\ndisposition\n\n\n"), + + "\nb\n\n\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, null, "encoding", 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\n\n\nencoding\n\n"), + + "\nb\n\n\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, null, "language", 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\n\n\nlanguage\n"), + + "\nb\n\n\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, null, "type", 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\n\n\ntype"), + + "\nb\n\n\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, null, "versionId", 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 - + "\nbv\nversionId\n\n\n\n\n\n"), + + "\nbv\nversionId\n\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, "saoid", null, null, null, + null, null, null, null, null, null, null, null, "saoid", 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\nsaoid\n\n\n\n\n\n\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, "cid", 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\ncid\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\n\n\n\n"), + + "\nb\n\n\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, null, "encryptionScope", 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\nencryptionScope\n\n\n\n\n"), + + "\nb\n\nencryptionScope\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, null, "delegatedOid", + null, null, null, null, null, null, null, null, null, null, null, "delegatedOid", 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\ndelegatedOid\n\n\n" - + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n")); + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, "delegatedOid", 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\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, "delegatedOid", 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\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, "delegatedOid", 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\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\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, null, "delegatedOid", 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\ndelegatedOid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n"), + // Test requestHeaders only (single header) + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, null, singleHeader, 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\nx-ms-encryption-key-sha256:hashvalue\n\n\n\n\n\n\n"), + // Test requestQueryParameters only (single param) + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, null, null, singleQueryParam, + "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\ncomp:blocklist\n\n\n\n\n"), + // Test both requestHeaders and requestQueryParameters + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, null, singleHeader, singleQueryParam, + "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\nx-ms-encryption-key-sha256:hashvalue\n\n\ncomp:blocklist\n\n\n\n\n"), + // Test multiple headers and multiple query parameters + Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, + null, null, null, null, null, null, null, null, null, null, null, null, multipleHeaders, + multipleQueryParams, + "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\nx-ms-encryption-key-sha256:hashvalue\n" + + "x-ms-source-if-match:etag\n\n\nblockid:blockidvalue\n" + "comp:blocklist\n\n\n\n\n"), + // Test with all parameters populated + Arguments.of(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), // startTime + "11111111-1111-1111-1111-111111111111", // keyOid + "22222222-2222-2222-2222-222222222222", // keyTid + OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), // keyStart + OffsetDateTime.of(LocalDateTime.of(2018, 6, 1, 0, 0), ZoneOffset.UTC), // keyExpiry + "b", // keyService + "2018-06-17", // keyVersion + "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", // keyValue + new SasIpRange(), // ipRange + SasProtocol.HTTPS_ONLY, // protocol + "snapId", // snapId + "control", // cacheControl + "disposition", // contentDisposition + "encoding", // contentEncoding + "language", // contentLanguage + "type", // contentType + null, // versionId, versionId and snapId are mutually exclusive + "saoid", // saoid (preauthorizedAgentObjectId) + "cid", // cid (correlationId) + "encryptionScope", // encryptionScope + "delegatedOid", // delegatedUserObjectId + multipleHeaders, // requestHeaders + multipleQueryParams, // requestQueryParameters + "r\n" // permissions + + Constants.ISO_8601_UTC_DATE_FORMATTER + .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) // startTime + + "\n" + + Constants.ISO_8601_UTC_DATE_FORMATTER + .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) // expiryTime + + "\n/blob/%s/containerName/blobName\n" // canonicalName + + "11111111-1111-1111-1111-111111111111\n" // keyOid + + "22222222-2222-2222-2222-222222222222\n" // keyTid + + "2018-01-01T00:00:00Z\n" // keyStart + + "2018-06-01T00:00:00Z\n" // keyExpiry + + "b\n" // keyService + + "2018-06-17\n" // keyVersion + + "saoid\n" // saoid (preauthorizedAgentObjectId) + + "\n" // suoid (always empty) + + "cid\n" // cid (correlationId) + + "\n" // delegatedUserTenantId (removed - empty) + + "delegatedOid\n" // delegatedUserObjectId + + "ip\n" // sasIpRange + + SasProtocol.HTTPS_ONLY + "\n" // protocol + + Constants.SAS_SERVICE_VERSION + "\n" // VERSION + + "bs\n" // resource (blob snapshot) + + "snapId\n" // snapId (versionSegment with snapId) + + "encryptionScope\n" // encryptionScope + + "x-ms-encryption-key-sha256:hashvalue\n" // requestHeaders (multiple) + + "x-ms-source-if-match:etag\n\n" // requestHeaders continuation + newline separator + + "\nblockid:blockidvalue\n" // requestQueryParameters (multiple, with prepended newline) + + "comp:blocklist\n" // requestQueryParameters continuation + + "control\n" // cacheControl + + "disposition\n" // contentDisposition + + "encoding\n" // contentEncoding + + "language\n" // contentLanguage + + "type" // contentType (no trailing newline) + )); } @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-12-06") @@ -1348,4 +1490,40 @@ 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")); } + + private static void assertEqualsForEachLine(ArrayList stringToSign, String expected) { + String actual = stringToSign.get(0); + if (!expected.equals(actual)) { + StringBuilder output = new StringBuilder(); + String[] expectedLines = expected.split("\n", -1); + String[] actualLines = actual.split("\n", -1); + + output.append("\n=== Line-by-Line String-to-Sign Comparison ===\n"); + output.append("Expected lines: ").append(expectedLines.length).append("\n"); + output.append("Actual lines: ").append(actualLines.length).append("\n\n"); + + int maxLines = Math.max(expectedLines.length, actualLines.length); + for (int i = 0; i < maxLines; i++) { + String expLine = i < expectedLines.length ? expectedLines[i] : ""; + String actLine = i < actualLines.length ? actualLines[i] : ""; + + if (!expLine.equals(actLine)) { + output.append("Line ").append(i).append(" differs:\n"); + output.append(" Expected: [").append(expLine).append("]\n"); + output.append(" Actual: [").append(actLine).append("]\n\n"); + } else { + output.append("Line ").append(i).append(" matches: [").append(expLine).append("]\n"); + } + } + + output.append("=== Full Expected String ===\n"); + output.append(expected.replace("\n", "\\n\n")); + output.append("\n\n=== Full Actual String ===\n"); + output.append(actual.replace("\n", "\\n\n")); + + // Print everything at once + System.out.println(output.toString()); + } + assertEquals(expected, actual, "String-to-sign mismatch"); + } } 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 37105f3dff16..69fbcd58eae1 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 @@ -364,6 +364,16 @@ public static final class UrlConstants { */ public static final String SAS_ENCRYPTION_SCOPE = "ses"; + /** + * The SAS request headers parameter. + */ + public static final String SAS_REQUEST_HEADERS = "srh"; + + /** + * The SAS request query parameters parameter. + */ + public static final String SAS_REQUEST_QUERY_PARAMETERS = "srq"; + /** * The SAS cache control parameter. */ diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/SasImplUtils.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/SasImplUtils.java index 3f7713b09a63..17aa7ffc0d17 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/SasImplUtils.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/SasImplUtils.java @@ -10,6 +10,7 @@ import com.azure.storage.common.policy.StorageSharedKeyCredentialPolicy; import java.util.Comparator; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -117,4 +118,58 @@ public static Map parseQueryString(String queryParams) { return retVals; } + + /** + * Formats request headers for SAS signing. + * + * @param requestHeaders The map of request headers to format. + * @return A formatted string with headers in the format "key:value" separated by newlines, or empty string if + * null/empty. Terminates each pair with a newline (\n). + * @see + * + * Version 2026-04-06 and later (Blob Storage and Data Lake Storage) + */ + + public static String formatRequestHeadersForSasSigning(Map requestHeaders) { + if (requestHeaders == null || requestHeaders.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + requestHeaders.forEach((key, value) -> sb.append(key).append(":").append(value).append("\n")); + return sb.toString(); + } + + /** + * Formats request headers for SAS signing. + * + * @param requestQueryParameters The map of request headers to format. + * @return A formatted string with query params in the format "key:value" separated by newlines, or empty string if + * null/empty. Prepends a newline character. Prefixes each pair with a newline (\n). + * @see + * + * Version 2026-04-06 and later (Blob Storage and Data Lake Storage) + */ + public static String formatRequestQueryParametersForSasSigning(Map requestQueryParameters) { + if (requestQueryParameters == null || requestQueryParameters.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + requestQueryParameters.forEach((key, value) -> sb.append("\n").append(key).append(":").append(value)); + return sb.toString(); + } + + public static Map parseRequestHeadersAndQueryParameterString(String rawString) { + if (CoreUtils.isNullOrEmpty(rawString)) { + return null; + } + Map valueMap = new HashMap<>(); + String[] pairs = rawString.split("\n"); + for (String pair : pairs) { + if (!CoreUtils.isNullOrEmpty(pair)) { + String[] keyValue = pair.split(":", 2); + valueMap.put(keyValue[0].trim(), keyValue[1].trim()); + } + } + return valueMap; + } } 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 f4a526e50212..b1e5f103e4e5 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 @@ -12,6 +12,9 @@ import java.util.Map; import java.util.function.Function; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestHeadersForSasSigning; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestQueryParametersForSasSigning; + /** * Represents the components that make up an Azure Storage SAS' query parameters. This type is not constructed directly * by the user; it is only generated by the URLParts type. NOTE: Instances of this class are immutable to ensure thread @@ -46,6 +49,8 @@ public class CommonSasQueryParameters { private final String correlationId; private final String encryptionScope; private final String delegatedUserObjectId; + private Map requestHeaders; + private Map requestQueryParameters; /** * Creates a new {@link CommonSasQueryParameters} object. @@ -111,6 +116,11 @@ public CommonSasQueryParameters(Map queryParamsMap, boolean re removeSasParametersFromMap); this.delegatedUserObjectId = getQueryParameter(queryParamsMap, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID, removeSasParametersFromMap); + this.requestHeaders = getQueryParameter(queryParamsMap, Constants.UrlConstants.SAS_REQUEST_HEADERS, + removeSasParametersFromMap, SasImplUtils::parseRequestHeadersAndQueryParameterString); + this.requestQueryParameters + = getQueryParameter(queryParamsMap, Constants.UrlConstants.SAS_REQUEST_QUERY_PARAMETERS, + removeSasParametersFromMap, SasImplUtils::parseRequestHeadersAndQueryParameterString); } /** @@ -189,6 +199,10 @@ public String encode() { SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_SERVICE, this.keyService); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_VERSION, this.keyVersion); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource); + SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_HEADERS, + formatRequestHeadersForSasSigning(this.requestHeaders)); + SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_QUERY_PARAMETERS, + formatRequestQueryParametersForSasSigning(this.requestQueryParameters)); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CACHE_CONTROL, this.cacheControl); SasImplUtils.tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_DISPOSITION, this.contentDisposition); @@ -482,4 +496,24 @@ public String getEncryptionScope() { public String getDelegatedUserObjectId() { return delegatedUserObjectId; } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Headers to include in the SAS. + * Any usage of the SAS must include these headers and values in the request. + * + * @return A map of request headers. + */ + public Map getRequestHeaders() { + return requestHeaders; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Query Parameters to include in + * the SAS. Any usage of the SAS must include these query parameters and values in the request. + * + * @return A map of request query parameters. + */ + public Map getRequestQueryParameters() { + return requestQueryParameters; + } } diff --git a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/SasTestData.java b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/SasTestData.java new file mode 100644 index 000000000000..5fa488b87848 --- /dev/null +++ b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/SasTestData.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.storage.common.test.shared; +import com.azure.storage.common.sas.SasIpRange; +import com.azure.storage.common.sas.SasProtocol; +import org.junit.jupiter.params.provider.Arguments; +import java.time.OffsetDateTime; +/** + * Helper class to build test arguments for regular SAS string-to-sign tests. + * This is the base class that contains common fields shared by both regular SAS and user delegation SAS. + * All fields default to null, so you only need to set the ones you're testing. + *

+ * For user delegation SAS tests, use {@link UserDelegationSasTestData} which extends this class. + */ +public class SasTestData { + // Common fields for all SAS types + protected OffsetDateTime startTime; + protected SasIpRange ipRange; + protected SasProtocol protocol; + protected String cacheControl; + protected String disposition; + protected String encoding; + protected String language; + protected String type; + protected String expectedStringToSign; + // Regular SAS specific field + protected String identifier; // Signed identifier for regular SAS + public SasTestData setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + return this; + } + public SasTestData setIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + public SasTestData setIpRange(SasIpRange ipRange) { + this.ipRange = ipRange; + return this; + } + public SasTestData setProtocol(SasProtocol protocol) { + this.protocol = protocol; + return this; + } + public SasTestData setCacheControl(String cacheControl) { + this.cacheControl = cacheControl; + return this; + } + public SasTestData setDisposition(String disposition) { + this.disposition = disposition; + return this; + } + public SasTestData setEncoding(String encoding) { + this.encoding = encoding; + return this; + } + public SasTestData setLanguage(String language) { + this.language = language; + return this; + } + public SasTestData setType(String type) { + this.type = type; + return this; + } + public SasTestData setExpectedStringToSign(String expectedStringToSign) { + this.expectedStringToSign = expectedStringToSign; + return this; + } + /** + * Converts to Arguments for regular SAS tests. + * Returns arguments in this order: + * startTime, identifier, ipRange, protocol, cacheControl, disposition, encoding, language, type, expectedStringToSign + * + * @return Arguments for parameterized tests matching the signature of regular SAS test methods + */ + public Arguments toArguments() { + return Arguments.of(startTime, identifier, ipRange, protocol, cacheControl, disposition, encoding, language, + type, expectedStringToSign); + } +} diff --git a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/UserDelegationSasTestData.java b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/UserDelegationSasTestData.java new file mode 100644 index 000000000000..c90554e1627b --- /dev/null +++ b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/UserDelegationSasTestData.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.common.test.shared; + +import com.azure.storage.common.sas.SasIpRange; +import com.azure.storage.common.sas.SasProtocol; +import org.junit.jupiter.params.provider.Arguments; + +import java.time.OffsetDateTime; +import java.util.Map; + +/** + * Helper class to build test arguments for User Delegation SAS string-to-sign tests. + * Extends {@link SasTestData} to inherit common SAS fields. + * All fields default to null, so you only need to set the ones you're testing. + *

+ * Note: User delegation SAS does NOT use the 'identifier' field (that's for regular SAS). + * Request headers and query parameters are only used in user delegation SAS. + *

+ * For regular SAS tests, use {@link SasTestData} directly. + */ +public class UserDelegationSasTestData extends SasTestData { + // User delegation SAS specific fields + private String keyOid; + private String keyTid; + private OffsetDateTime keyStart; + private OffsetDateTime keyExpiry; + private String keyService; + private String keyVersion; + private String keyValue; + private Map requestHeaders; + private Map requestQueryParameters; + private String saoid; + private String suoid; + private String cid; + + // Override parent setters to return UserDelegationSasTestData for fluent API + @Override + public UserDelegationSasTestData setStartTime(OffsetDateTime startTime) { + super.setStartTime(startTime); + return this; + } + + @Override + public UserDelegationSasTestData setIpRange(SasIpRange ipRange) { + super.setIpRange(ipRange); + return this; + } + + @Override + public UserDelegationSasTestData setProtocol(SasProtocol protocol) { + super.setProtocol(protocol); + return this; + } + + @Override + public UserDelegationSasTestData setCacheControl(String cacheControl) { + super.setCacheControl(cacheControl); + return this; + } + + @Override + public UserDelegationSasTestData setDisposition(String disposition) { + super.setDisposition(disposition); + return this; + } + + @Override + public UserDelegationSasTestData setEncoding(String encoding) { + super.setEncoding(encoding); + return this; + } + + @Override + public UserDelegationSasTestData setLanguage(String language) { + super.setLanguage(language); + return this; + } + + @Override + public UserDelegationSasTestData setType(String type) { + super.setType(type); + return this; + } + + @Override + public UserDelegationSasTestData setExpectedStringToSign(String expectedStringToSign) { + super.setExpectedStringToSign(expectedStringToSign); + return this; + } + + // User delegation SAS specific setters + + public UserDelegationSasTestData setKeyOid(String keyOid) { + this.keyOid = keyOid; + return this; + } + + public UserDelegationSasTestData setKeyTid(String keyTid) { + this.keyTid = keyTid; + return this; + } + + public UserDelegationSasTestData setKeyStart(OffsetDateTime keyStart) { + this.keyStart = keyStart; + return this; + } + + public UserDelegationSasTestData setKeyExpiry(OffsetDateTime keyExpiry) { + this.keyExpiry = keyExpiry; + return this; + } + + public UserDelegationSasTestData setKeyService(String keyService) { + this.keyService = keyService; + return this; + } + + public UserDelegationSasTestData setKeyVersion(String keyVersion) { + this.keyVersion = keyVersion; + return this; + } + + public UserDelegationSasTestData setKeyValue(String keyValue) { + this.keyValue = keyValue; + return this; + } + + public UserDelegationSasTestData setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + return this; + } + + public UserDelegationSasTestData setRequestQueryParameters(Map requestQueryParameters) { + this.requestQueryParameters = requestQueryParameters; + return this; + } + + public UserDelegationSasTestData setSaoid(String saoid) { + this.saoid = saoid; + return this; + } + + public UserDelegationSasTestData setSuoid(String suoid) { + this.suoid = suoid; + return this; + } + + public UserDelegationSasTestData setCid(String cid) { + this.cid = cid; + return this; + } + + /** + * Converts to Arguments for user delegation SAS tests. + * Returns arguments with or without request headers/query parameters based on the parameter. + * + * @param withHeadersAndParams Whether to include request headers and query parameters in the test data. + * @return Arguments for parameterized tests matching the signature of user delegation SAS test methods + */ + public Arguments toArguments(boolean withHeadersAndParams) { + if (withHeadersAndParams) { + return Arguments.of(startTime, keyOid, keyTid, keyStart, keyExpiry, keyService, keyVersion, keyValue, + ipRange, protocol, cacheControl, disposition, encoding, language, type, requestHeaders, + requestQueryParameters, saoid, suoid, cid, expectedStringToSign); + } else { + return Arguments.of(startTime, keyOid, keyTid, keyStart, keyExpiry, keyService, keyVersion, keyValue, + ipRange, protocol, cacheControl, disposition, encoding, language, type, saoid, suoid, cid, expectedStringToSign); + } + } + + /** + * Converts to Arguments for user delegation SAS tests with request headers and query parameters. + * This is a convenience method that calls {@link #toArguments(boolean)} with true. + * + * @return Arguments for parameterized tests with headers and query parameters included + */ + public Arguments toArguments() { + return toArguments(true); + } +} + + diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/SasImplUtilsTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/SasImplUtilsTests.java new file mode 100644 index 000000000000..d5b39c284562 --- /dev/null +++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/SasImplUtilsTests.java @@ -0,0 +1,104 @@ +package com.azure.storage.common.implementation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals;; + +public class SasImplUtilsTests { + + private Map requestHeaders; + private Map requestQueryParams; + + @BeforeEach + public void setup() { + requestHeaders = new HashMap<>(); + requestQueryParams = new HashMap<>(); + } + + @Test + public void formatRequestHeadersForSasSigningNullReturnsEmptyString() { + assertEquals("", SasImplUtils.formatRequestHeadersForSasSigning(null)); + } + + @Test + public void formatRequestHeadersForSasSigningEmptyReturnsEmptyString() { + assertEquals("", SasImplUtils.formatRequestHeadersForSasSigning(requestHeaders)); + } + + @Test + public void formatRequestHeadersForSasSigningReturnsWithLastCharAsNewline() { + requestHeaders.put("Some-Header", "someValue"); + String headerString = SasImplUtils.formatRequestHeadersForSasSigning(requestHeaders); + + assertNotEquals("", headerString); + assertEquals("\n", headerString.substring(headerString.length() - 1)); + } + + @Test + public void formatRequestHeadersForSasSigningPopulatedHeaders() { + requestHeaders.put(Constants.HeaderConstants.ENCRYPTION_KEY, "encryptionKeyValue"); + requestHeaders.put(Constants.HeaderConstants.CONTENT_ENCODING, "contentEncodingValue"); + requestHeaders.put(Constants.HeaderConstants.CONTENT_TYPE, "contentTypeValue"); + requestHeaders.put(Constants.HeaderConstants.CLIENT_REQUEST_ID, "clientRequestId"); + + String expected = "x-ms-encryption-key:encryptionKeyValue\n" + "Content-Encoding:contentEncodingValue\n" + + "Content-Type:contentTypeValue\n" + "x-ms-client-request-id:clientRequestId\n"; + + String headers = SasImplUtils.formatRequestHeadersForSasSigning(requestHeaders); + Integer newLineCount + = Arrays.stream(headers.split("")).filter(s -> s.equals("\n")).collect(Collectors.toList()).size(); + + String sortedExpected = Arrays.stream(expected.split("\n")).sorted().collect(Collectors.joining("\n")) + "\n"; + + String sortedHeaders = Arrays.stream(headers.split("\n")).sorted().collect(Collectors.joining("\n")) + "\n"; + + assertEquals(4, newLineCount); + assertEquals(sortedExpected, sortedHeaders); + } + + @Test + public void formatRequestQueryParamsForSasSigningNullReturnsEmptyString() { + assertEquals("", SasImplUtils.formatRequestQueryParametersForSasSigning(null)); + } + + @Test + public void formatRequestQueryParamsForSasSigningEmptyReturnsEmptyString() { + assertEquals("", SasImplUtils.formatRequestQueryParametersForSasSigning(requestQueryParams)); + } + + @Test + public void formatRequestQueryParamsForSasSigningReturnsWithFirstCharAsNewline() { + requestQueryParams.put("someParam", "someValue"); + + String queryParamString = SasImplUtils.formatRequestQueryParametersForSasSigning(requestQueryParams); + + assertNotEquals("", queryParamString); + assertEquals("\n", queryParamString.substring(0, 1)); + } + + @Test + public void formatRequestQueryParamsForSasSigningPopulatedParams() { + requestQueryParams.put("paramA", "valueA"); + requestQueryParams.put("paramB", "valueB"); + requestQueryParams.put("paramC", "valueC"); + String expected = "\nparamA:valueA\nparamB:valueB\nparamC:valueC"; + + String queryParams = SasImplUtils.formatRequestQueryParametersForSasSigning(requestQueryParams); + Integer newLineCount + = Arrays.stream(queryParams.split("")).filter(s -> s.equals("\n")).collect(Collectors.toList()).size(); + String sortedExpected + = "\n" + Arrays.stream(expected.substring(1).split("\n")).sorted().collect(Collectors.joining("\n")); + String sortedQueryParams + = "\n" + Arrays.stream(queryParams.substring(1).split("\n")).sorted().collect(Collectors.joining("\n")); + + assertEquals(3, newLineCount); + assertEquals(sortedExpected, sortedQueryParams); + } +} diff --git a/sdk/storage/azure-storage-file-datalake/assets.json b/sdk/storage/azure-storage-file-datalake/assets.json index 90e1f30a59e7..f5bb8d4f9891 100644 --- a/sdk/storage/azure-storage-file-datalake/assets.json +++ b/sdk/storage/azure-storage-file-datalake/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-file-datalake", - "Tag": "java/storage/azure-storage-file-datalake_e7c65c4771" + "Tag": "java/storage/azure-storage-file-datalake_cc5d8d21d2" } diff --git a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/implementation/util/DataLakeSasImplUtil.java b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/implementation/util/DataLakeSasImplUtil.java index e4b75d4e50c1..985080402296 100644 --- a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/implementation/util/DataLakeSasImplUtil.java +++ b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/implementation/util/DataLakeSasImplUtil.java @@ -20,10 +20,13 @@ import com.azure.storage.file.datalake.sas.PathSasPermission; import java.time.OffsetDateTime; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import static com.azure.storage.common.implementation.SasImplUtils.formatQueryParameterDate; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestHeadersForSasSigning; +import static com.azure.storage.common.implementation.SasImplUtils.formatRequestQueryParametersForSasSigning; import static com.azure.storage.common.implementation.SasImplUtils.tryAppendQueryParameter; /** @@ -53,46 +56,28 @@ public class DataLakeSasImplUtil { .get(Constants.PROPERTY_AZURE_STORAGE_SAS_SERVICE_VERSION, DataLakeServiceVersion.getLatest().getVersion()); private SasProtocol protocol; - private OffsetDateTime startTime; - private OffsetDateTime expiryTime; - private String permissions; - private SasIpRange sasIpRange; - private String fileSystemName; - private String pathName; - private String resource; - private String identifier; - private String cacheControl; - private String contentDisposition; - private String contentEncoding; - private String contentLanguage; - private String contentType; - private Boolean isDirectory; - private Integer directoryDepth; - private String authorizedAadObjectId; - private String unauthorizedAadObjectId; - private String correlationId; - private String encryptionScope; - private String delegatedUserObjectId; + private Map requestHeaders; + private Map requestQueryParameters; /** * Creates a new {@link DataLakeSasImplUtil} with the specified parameters @@ -134,6 +119,8 @@ public DataLakeSasImplUtil(DataLakeServiceSasSignatureValues sasValues, String f this.isDirectory = isDirectory; this.encryptionScope = sasValues.getEncryptionScope(); this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId(); + this.requestHeaders = sasValues.getRequestHeaders(); + this.requestQueryParameters = sasValues.getRequestQueryParameters(); } /** @@ -270,20 +257,23 @@ private String encode(UserDelegationKey userDelegationKey, String signature) { } tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_ENCRYPTION_SCOPE, this.encryptionScope); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_HEADERS, + formatRequestHeadersForSasSigning(this.requestHeaders)); + tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_REQUEST_QUERY_PARAMETERS, + formatRequestQueryParametersForSasSigning(this.requestQueryParameters)); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CACHE_CONTROL, this.cacheControl); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_DISPOSITION, this.contentDisposition); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_ENCODING, this.contentEncoding); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_LANGUAGE, this.contentLanguage); tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_CONTENT_TYPE, this.contentType); - tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_ENCRYPTION_SCOPE, this.encryptionScope); return sb.toString(); } /** - * Ensures that the builder's properties are in a consistent state. - + *

Ensures that the builder's properties are in a consistent state. * 1. If there is no identifier set, ensure expiryTime and permissions are set. * 2. Resource name is chosen by: * a. If "BlobName" is _not_ set, it is a container resource. @@ -291,11 +281,11 @@ private String encode(UserDelegationKey userDelegationKey, String signature) { * c. Otherwise, if "VersionId" is set, it is a blob version resource. * d. Otherwise, it is a blob resource. * 3. Reparse permissions depending on what the resource is. If it is an unrecognized resource, do nothing. - * 4. Ensure saoid is not set when suoid is set and vice versa. + * 4. Ensure saoid is not set when suoid is set and vice versa.

* * Taken from: - * https://github.com/Azure/azure-storage-blob-go/blob/master/azblob/sas_service.go#L33 - * https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs + * sas_service.go + * BlobSasBuilder.cs */ public void ensureState() { if (identifier == null) { @@ -445,6 +435,30 @@ public String stringToSign(final UserDelegationKey key, String canonicalName) { this.contentEncoding == null ? "" : this.contentEncoding, this.contentLanguage == null ? "" : this.contentLanguage, this.contentType == null ? "" : this.contentType); + } else if (VERSION.compareTo(DataLakeServiceVersion.V2026_02_06.getVersion()) <= 0) { + return String.join("\n", this.permissions == null ? "" : this.permissions, + this.startTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), + this.expiryTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), + canonicalName, key.getSignedObjectId() == null ? "" : key.getSignedObjectId(), + key.getSignedTenantId() == null ? "" : key.getSignedTenantId(), + key.getSignedStart() == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedStart()), + key.getSignedExpiry() == null + ? "" + : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedExpiry()), + key.getSignedService() == null ? "" : key.getSignedService(), + key.getSignedVersion() == null ? "" : key.getSignedVersion(), + this.authorizedAadObjectId == null ? "" : this.authorizedAadObjectId, + this.unauthorizedAadObjectId == null ? "" : this.unauthorizedAadObjectId, + this.correlationId == null ? "" : this.correlationId, "", /* new schema 2025-07-05 */ + this.delegatedUserObjectId == null ? "" : this.delegatedUserObjectId, + this.sasIpRange == null ? "" : this.sasIpRange.toString(), + this.protocol == null ? "" : this.protocol.toString(), VERSION, resource, "", /* Version segment. */ + this.encryptionScope == null ? "" : this.encryptionScope, + this.cacheControl == null ? "" : this.cacheControl, + this.contentDisposition == null ? "" : this.contentDisposition, + this.contentEncoding == null ? "" : this.contentEncoding, + this.contentLanguage == null ? "" : this.contentLanguage, + this.contentType == null ? "" : this.contentType); } else { return String.join("\n", this.permissions == null ? "" : this.permissions, this.startTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), @@ -464,6 +478,10 @@ public String stringToSign(final UserDelegationKey key, String canonicalName) { this.sasIpRange == null ? "" : this.sasIpRange.toString(), this.protocol == null ? "" : this.protocol.toString(), VERSION, resource, "", /* Version segment. */ this.encryptionScope == null ? "" : this.encryptionScope, + this.requestHeaders == null ? "" : formatRequestHeadersForSasSigning(this.requestHeaders), + this.requestQueryParameters == null + ? "" + : formatRequestQueryParametersForSasSigning(this.requestQueryParameters), this.cacheControl == null ? "" : this.cacheControl, this.contentDisposition == null ? "" : this.contentDisposition, this.contentEncoding == null ? "" : this.contentEncoding, diff --git a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/sas/DataLakeServiceSasSignatureValues.java b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/sas/DataLakeServiceSasSignatureValues.java index abca896a2f00..54c90d3c04ba 100644 --- a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/sas/DataLakeServiceSasSignatureValues.java +++ b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/sas/DataLakeServiceSasSignatureValues.java @@ -4,6 +4,7 @@ package com.azure.storage.file.datalake.sas; import com.azure.core.util.Configuration; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.implementation.Constants; import com.azure.storage.common.implementation.StorageImplUtils; import com.azure.storage.common.sas.SasIpRange; @@ -12,6 +13,7 @@ import com.azure.storage.file.datalake.models.UserDelegationKey; import java.time.OffsetDateTime; +import java.util.Map; /** * Used to initialize parameters for a Shared Access Signature (SAS) for an Azure Data Lake Storage service. Once all @@ -45,6 +47,8 @@ public final class DataLakeServiceSasSignatureValues { private String correlationId; private String encryptionScope; private String delegatedUserObjectId; + private Map requestHeaders; + private Map requestQueryParameters; /** * Creates an object with the specified expiry time and permissions @@ -475,4 +479,48 @@ public DataLakeServiceSasSignatureValues setDelegatedUserObjectId(String delegat this.delegatedUserObjectId = delegatedUserObjectId; return this; } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Headers to include in the SAS. + * Any usage of the SAS must include these headers and values in the request. + * + * @return The custom request headers to be set when the SAS is used. + */ + public Map getRequestHeaders() { + return requestHeaders; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Headers to include in the SAS. + * Any usage of the SAS must include these headers and values in the request. + * + * @param requestHeaders The custom request headers to be set when the SAS is used. + * @return the updated DataLakeServiceSasSignatureValues object + */ + public DataLakeServiceSasSignatureValues setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + return this; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Query Parameters to include in + * the SAS. Any usage of the SAS must include these query parameters and values in the request. + * + * @return The custom query parameters to be set when the SAS is used. + */ + public Map getRequestQueryParameters() { + return requestQueryParameters; + } + + /** + * Optional. Beginning in version 2026-04-06, this value specifies Custom Request Query Parameters to include in + * the SAS. Any usage of the SAS must include these query parameters and values in the request. + * + * @param requestQueryParameters The custom query parameters to be set when the SAS is used. + * @return the updated DataLakeServiceSasSignatureValues object + */ + public DataLakeServiceSasSignatureValues setRequestQueryParameters(Map requestQueryParameters) { + this.requestQueryParameters = requestQueryParameters; + return this; + } } diff --git a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/SasTests.java b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/SasTests.java index 9e6979070c7c..8984681b6388 100644 --- a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/SasTests.java +++ b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/SasTests.java @@ -14,6 +14,8 @@ 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.SasTestData; +import com.azure.storage.common.test.shared.UserDelegationSasTestData; import com.azure.storage.common.test.shared.extensions.LiveOnly; import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion; import com.azure.storage.file.datalake.implementation.util.DataLakeSasImplUtil; @@ -41,8 +43,11 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.stream.Stream; import static com.azure.storage.common.test.shared.StorageCommonTestUtils.getOidFromToken; @@ -775,62 +780,45 @@ private static Stream sasImplUtilStringToSignSupplier() { // /blob/accountName. We test canonicalization of resources later. Again, this is not to test a fully functional // sas but the construction of the string to sign. // Signed resource is tested elsewhere, as we work some minor magic in choosing which value to use. - return Stream.of( - // startTime | identifier | ipRange | protocol | cacheControl | disposition | encoding | language | type | expectedStringToSign - Arguments.of(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), 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)) - + "\n" - + Constants.ISO_8601_UTC_DATE_FORMATTER - .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\n\n\n\n"), - Arguments.of(null, "id", 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/fileSystemName/pathName\nid\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), - Arguments.of(null, null, new SasIpRange(), 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/fileSystemName/pathName\n\nip\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n"), - Arguments.of(null, null, null, SasProtocol.HTTPS_ONLY, 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/fileSystemName/pathName\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, "control", 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/fileSystemName/pathName\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\ncontrol\n\n\n\n"), - Arguments.of(null, null, null, null, null, "disposition", 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/fileSystemName/pathName\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\ndisposition\n\n\n"), - Arguments.of(null, null, null, null, null, null, "encoding", 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/fileSystemName/pathName\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, "language", 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/fileSystemName/pathName\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, null, "type", - "r\n\n" - + Constants.ISO_8601_UTC_DATE_FORMATTER - .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + Constants.SAS_SERVICE_VERSION - + "\nb\n\n\n\n\n\n\ntype")); + OffsetDateTime expiryTime = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + String expiryTimeStr = Constants.ISO_8601_UTC_DATE_FORMATTER.format(expiryTime); + + return Stream.of(new SasTestData().setStartTime(expiryTime) + .setExpectedStringToSign("r\n" + expiryTimeStr + "\n" + expiryTimeStr + + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n") + .toArguments(), + new SasTestData().setIdentifier("id") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\nid\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n") + .toArguments(), + new SasTestData().setIpRange(new SasIpRange()) + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\nip\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n") + .toArguments(), + new SasTestData().setProtocol(SasProtocol.HTTPS_ONLY) + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n" + + SasProtocol.HTTPS_ONLY + "\n" + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n") + .toArguments(), + new SasTestData().setCacheControl("control") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\ncontrol\n\n\n\n") + .toArguments(), + new SasTestData().setDisposition("disposition") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\ndisposition\n\n\n") + .toArguments(), + new SasTestData().setEncoding("encoding") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\nencoding\n\n") + .toArguments(), + new SasTestData().setLanguage("language") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\nlanguage\n") + .toArguments(), + new SasTestData().setType("type") + .setExpectedStringToSign("r\n\n" + expiryTimeStr + "\n/blob/%s/fileSystemName/pathName\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\ntype") + .toArguments()); } @RequiredServiceVersion(clazz = DataLakeServiceVersion.class, min = "2020-12-06") @@ -839,10 +827,12 @@ private static Stream sasImplUtilStringToSignSupplier() { public void sasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTime, String keyOid, String keyTid, OffsetDateTime keyStart, OffsetDateTime keyExpiry, String keyService, String keyVersion, String keyValue, SasIpRange ipRange, SasProtocol protocol, String cacheControl, String disposition, String encoding, - String language, String type, String saoid, String suoid, String cid, String expectedStringToSign) { + String language, String type, Map requestHeaders, Map requestQueryParameters, + String saoid, String suoid, String cid, String expectedStringToSign) { OffsetDateTime e = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); PathSasPermission p = new PathSasPermission().setReadPermission(true); + ArrayList stringToSign = new ArrayList<>(); String expected = String.format(expectedStringToSign, ENVIRONMENT.getDataLakeAccount().getName()); DataLakeServiceSasSignatureValues v = new DataLakeServiceSasSignatureValues(e, p).setPermissions(p) @@ -856,7 +846,9 @@ public void sasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTime, S .setContentType(type) .setCorrelationId(cid) .setPreauthorizedAgentObjectId(saoid) - .setAgentObjectId(suoid); + .setAgentObjectId(suoid) + .setRequestHeaders(requestHeaders) + .setRequestQueryParameters(requestQueryParameters); if (ipRange != null) { v.setSasIpRange(new SasIpRange().setIpMin("ip")); @@ -872,150 +864,183 @@ public void sasImplUtilStringToSignUserDelegationKey(OffsetDateTime startTime, S DataLakeSasImplUtil util = new DataLakeSasImplUtil(v, "fileSystemName", "pathName", false); util.ensureState(); + String sasToken = util.generateUserDelegationSas(key, ENVIRONMENT.getDataLakeAccount().getName(), + stringToSign::add, Context.NONE); + assertEqualsForEachLine(stringToSign, expected); assertEquals(expected, util.stringToSign(key, util.getCanonicalName(ENVIRONMENT.getDataLakeAccount().getName()))); } private static Stream sasImplUtilStringToSignUserDelegationKeySupplier() { + // Use LinkedHashMap to ensure deterministic iteration order + Map singleHeader = new LinkedHashMap<>(); + singleHeader.put("x-ms-encryption-key-sha256", "hashvalue"); + + Map singleQueryParam = new LinkedHashMap<>(); + singleQueryParam.put("comp", "blocklist"); + + Map multipleHeaders = new LinkedHashMap<>(); + multipleHeaders.put("x-ms-encryption-key-sha256", "hashvalue"); + multipleHeaders.put("x-ms-source-if-match", "etag"); + + Map multipleQueryParams = new LinkedHashMap<>(); + multipleQueryParams.put("blockid", "blockidvalue"); + multipleQueryParams.put("comp", "blocklist"); + // We test string to sign functionality directly related to user delegation sas specific parameters return Stream.of( - // startTime | keyOid | keyTid | keyStart | keyExpiry | keyService | keyVersion | keyValue | ipRange | protocol | cacheControl | disposition | encoding | language | type | saoid | suoid | cid | expectedStringToSign - Arguments.of(null, null, null, null, null, null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, - null, null, null, null, null, null, null, null, null, - "r\n\n" + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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\n"), - 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, - "r\n" + + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setStartTime(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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\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, - "r\n\n" + + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyOid("11111111-1111-1111-1111-111111111111") + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\n11111111-1111-1111-1111-111111111111\n\n\n\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, "22222222-2222-2222-2222-222222222222", null, null, null, null, - "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, - "r\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyTid("22222222-2222-2222-2222-222222222222") + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\n\n22222222-2222-2222-2222-222222222222\n\n\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, 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, - "r\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData() + .setKeyStart(OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC)) + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\n\n\n2018-01-01T00:00:00Z\n\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, OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC), - null, null, "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, - null, null, null, - "r\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData() + .setKeyExpiry(OffsetDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneOffset.UTC)) + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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, - "r\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyService("b") + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\nb\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, null, "2018-06-17", - "3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=", null, null, null, null, null, null, null, null, null, - null, - "r\n\n" + + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyVersion("2018-06-17") + .setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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, - "r\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setIpRange(new SasIpRange()) + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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, - "r\n\n" + + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setEncoding("encoding") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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, "control", null, null, null, null, null, null, null, - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\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\nencoding\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setType("type") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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, "disposition", null, null, null, null, null, null, - "r\n\n" + + "\nb\n\n\n\n\n\n\n\n\ntype") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setSaoid("saoid") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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, "encoding", null, null, null, null, null, - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\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\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setSuoid("suoid") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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, "language", null, null, null, null, - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\n\n\n\nsuoid\n\n\n\n\n\n" + + Constants.SAS_SERVICE_VERSION + "\nb\n\n\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setCid("cid") + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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, "type", null, null, null, - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\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\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setRequestHeaders(singleHeader) + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + "\n/blob/%s/fileSystemName/pathName\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, "saoid", null, null, - "r\n\n" + + "\nb\n\n\nx-ms-encryption-key-sha256:hashvalue\n\n\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setRequestQueryParameters(singleQueryParam) + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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, "suoid", null, - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + + "\nb\n\n\n\n\ncomp:blocklist\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setRequestHeaders(singleHeader) + .setRequestQueryParameters(singleQueryParam) + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\n\n\n\nsuoid\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, "cid", - "r\n\n" + + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + + "\nb\n\n\nx-ms-encryption-key-sha256:hashvalue\n\n\ncomp:blocklist\n\n\n\n\n") + .toArguments(), + new UserDelegationSasTestData().setKeyValue("3hd4LRwrARVGbeMRQRfTLIsGMkCPuZJnvxZDU7Gak8c=") + .setRequestHeaders(multipleHeaders) + .setRequestQueryParameters(multipleQueryParams) + .setExpectedStringToSign("r\n\n" + Constants.ISO_8601_UTC_DATE_FORMATTER .format(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) - + "\n/blob/%s/fileSystemName/pathName\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")); + + "\n/blob/%s/fileSystemName/pathName\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + Constants.SAS_SERVICE_VERSION + + "\nb\n\n\nx-ms-encryption-key-sha256:hashvalue\n" + + "x-ms-source-if-match:etag\n\n\nblockid:blockidvalue\n" + "comp:blocklist\n\n\n\n\n") + .toArguments()); } @Test @@ -1085,4 +1110,35 @@ public void canUseSasToAuthenticate() { .getProperties()); } + private static void assertEqualsForEachLine(ArrayList stringToSign, String expected) { + String actual = stringToSign.get(0); + if (!expected.equals(actual)) { + StringBuilder output = new StringBuilder(); + String[] expectedLines = expected.split("\n", -1); + String[] actualLines = actual.split("\n", -1); + + output.append("\n=== Line-by-Line String-to-Sign Comparison ===\n"); + output.append("Expected lines: ").append(expectedLines.length).append("\n"); + output.append("Actual lines: ").append(actualLines.length).append("\n\n"); + + int maxLines = Math.max(expectedLines.length, actualLines.length); + for (int i = 0; i < maxLines; i++) { + String expLine = i < expectedLines.length ? expectedLines[i] : ""; + String actLine = i < actualLines.length ? actualLines[i] : ""; + + if (!expLine.equals(actLine)) { + output.append("Line ").append(i).append(" differs:\n"); + output.append(" Expected: [").append(expLine).append("]\n"); + output.append(" Actual: [").append(actLine).append("]\n\n"); + } else { + output.append("Line ").append(i).append(" matches: [").append(expLine).append("]\n"); + } + } + + // Print everything at once + System.out.println(output.toString()); + } + assertEquals(expected, actual, "String-to-sign mismatch"); + } + }