diff --git a/sdk/storage/Azure.Storage.Blobs.Batch/CHANGELOG.md b/sdk/storage/Azure.Storage.Blobs.Batch/CHANGELOG.md index 60fd4529805d..58e50f4a7ee3 100644 --- a/sdk/storage/Azure.Storage.Blobs.Batch/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Blobs.Batch/CHANGELOG.md @@ -1,14 +1,9 @@ # Release History -## 12.24.0-beta.2 (Unreleased) +## 12.25.0-beta.1 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed - -### Other Changes +- Added support for service version 2026-04-06. ## 12.24.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Blobs.Batch/samples/Azure.Storage.Blobs.Batch.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Blobs.Batch/samples/Azure.Storage.Blobs.Batch.Samples.Tests.csproj index 3dea34a02b7e..6009a5336b8b 100644 --- a/sdk/storage/Azure.Storage.Blobs.Batch/samples/Azure.Storage.Blobs.Batch.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs.Batch/samples/Azure.Storage.Blobs.Batch.Samples.Tests.csproj @@ -17,6 +17,7 @@ + PreserveNewest diff --git a/sdk/storage/Azure.Storage.Blobs.Batch/src/Azure.Storage.Blobs.Batch.csproj b/sdk/storage/Azure.Storage.Blobs.Batch/src/Azure.Storage.Blobs.Batch.csproj index 72236ca326fc..3b85d2e21cb0 100644 --- a/sdk/storage/Azure.Storage.Blobs.Batch/src/Azure.Storage.Blobs.Batch.csproj +++ b/sdk/storage/Azure.Storage.Blobs.Batch/src/Azure.Storage.Blobs.Batch.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.Blobs.Batch client library - 12.24.0-beta.2 + 12.25.0-beta.1 12.23.0 BlobSDK;$(DefineConstants) diff --git a/sdk/storage/Azure.Storage.Blobs.Batch/tests/Azure.Storage.Blobs.Batch.Tests.csproj b/sdk/storage/Azure.Storage.Blobs.Batch/tests/Azure.Storage.Blobs.Batch.Tests.csproj index dd1fb1a64a8c..919c07c0065c 100644 --- a/sdk/storage/Azure.Storage.Blobs.Batch/tests/Azure.Storage.Blobs.Batch.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs.Batch/tests/Azure.Storage.Blobs.Batch.Tests.csproj @@ -24,6 +24,7 @@ + PreserveNewest @@ -43,4 +44,4 @@ - \ No newline at end of file + diff --git a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/CHANGELOG.md b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/CHANGELOG.md index af235844cd0b..4dd61a138074 100644 --- a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/CHANGELOG.md @@ -3,12 +3,7 @@ ## 12.0.0-preview.59 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed - -### Other Changes +- Added support for service version 2026-04-06. ## 12.0.0-preview.58 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/samples/Azure.Storage.Blobs.ChangeFeed.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/samples/Azure.Storage.Blobs.ChangeFeed.Samples.Tests.csproj index 7711cae537db..6f8fcaf6528b 100644 --- a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/samples/Azure.Storage.Blobs.ChangeFeed.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/samples/Azure.Storage.Blobs.ChangeFeed.Samples.Tests.csproj @@ -1,4 +1,4 @@ - + $(RequiredTargetFrameworks) Microsoft Azure.Storage.Blobs.ChangeFeed client library samples @@ -14,6 +14,7 @@ + diff --git a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/Azure.Storage.Blobs.ChangeFeed.Tests.csproj b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/Azure.Storage.Blobs.ChangeFeed.Tests.csproj index 9682ab15ecd6..8cf13cd60744 100644 --- a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/Azure.Storage.Blobs.ChangeFeed.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/Azure.Storage.Blobs.ChangeFeed.Tests.csproj @@ -17,6 +17,7 @@ + @@ -28,4 +29,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/ChangeFeedTestBase.cs b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/ChangeFeedTestBase.cs index 8b7f691ad88f..87ce4b8704d1 100644 --- a/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/ChangeFeedTestBase.cs +++ b/sdk/storage/Azure.Storage.Blobs.ChangeFeed/tests/ChangeFeedTestBase.cs @@ -37,6 +37,7 @@ namespace Azure.Storage.Blobs.ChangeFeed.Tests BlobClientOptions.ServiceVersion.V2025_07_05, BlobClientOptions.ServiceVersion.V2025_11_05, BlobClientOptions.ServiceVersion.V2026_02_06, + BlobClientOptions.ServiceVersion.V2026_04_06, StorageVersionExtensions.LatestVersion, StorageVersionExtensions.MaxVersion, RecordingServiceVersion = StorageVersionExtensions.MaxVersion, diff --git a/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md b/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md index 8e0ad022bf1e..1c6715b76103 100644 --- a/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md @@ -1,14 +1,21 @@ # Release History -## 12.27.0-beta.2 (Unreleased) +## 12.28.0-beta.1 (Unreleased) ### Features Added +- Added support for service version 2026-04-06. +- Added support for Content Validation via Structured Message. +- Added support for Delete Blob Conditional Tier. +- Added support for Server-side Encryption Rekeying. +- Added cross-tenant support for Principal-Bound User Delegation SAS. +- Added support for Dynamic User Delegation SAS. -### Breaking Changes +### Other Changes +- Changed the default concurrency transfer count from 5 to Math.Clamp(Environment.ProcessorCount * 2, 8, 32). This controls the maximum number of concurrent tasks that will be used during large downloads or uploads, and this change should result in higher throughput for these operations by default in most environments. This can be reverted by enabling "Azure.Storage.UseLegacyDefaultConcurrency" in the AppContext switch or "AZURE_STORAGE_USE_LEGACY_DEFAULT_CONCURRENCY" in the environment variable. ### Bugs Fixed - -### Other Changes +- Added BlobErrorCode.IncrementalCopyOfEarlierSnapshotNotAllowed, deprecated BlobErrorCode.IncrementalCopyOfEarlierVersionSnapshotNotAllowed. +- Added support for missing SkuName values. ## 12.27.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net8.0.cs b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net8.0.cs index e979c7a85c26..6f7e5297d541 100644 --- a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net8.0.cs +++ b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net8.0.cs @@ -92,6 +92,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class BlobContainerClient @@ -224,8 +225,12 @@ public BlobServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyCr public virtual System.Threading.Tasks.Task> GetPropertiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -310,6 +315,7 @@ public AppendBlobAppendBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.AppendBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -536,6 +542,7 @@ public BlobDownloadDetails() { } public long BlobSequenceNumber { get { throw null; } } public Azure.Storage.Blobs.Models.BlobType BlobType { get { throw null; } } public string CacheControl { get { throw null; } } + public byte[] ContentCrc { get { throw null; } } public string ContentDisposition { get { throw null; } } public string ContentEncoding { get { throw null; } } public byte[] ContentHash { get { throw null; } } @@ -581,6 +588,7 @@ internal BlobDownloadInfo() { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string ContentType { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadOptions @@ -602,6 +610,7 @@ public partial class BlobDownloadStreamingResult : System.IDisposable internal BlobDownloadStreamingResult() { } public System.IO.Stream Content { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadToOptions @@ -655,6 +664,8 @@ public BlobDownloadToOptions() { } public static Azure.Storage.Blobs.Models.BlobErrorCode EmptyMetadataKey { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode FeatureVersionMismatch { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyBlobMismatch { get { throw null; } } + public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierSnapshotNotAllowed { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierVersionSnapshotNotAllowed { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEralierVersionSnapshotNotAllowed { get { throw null; } } @@ -763,6 +774,13 @@ public enum BlobGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class BlobGetUserDelegationKeyOptions + { + public BlobGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class BlobHierarchyItem { internal BlobHierarchyItem() { } @@ -1023,6 +1041,8 @@ protected BlobQueryTextOptions() { } public partial class BlobRequestConditions : Azure.Storage.Blobs.Models.BlobLeaseRequestConditions { public BlobRequestConditions() { } + public System.DateTimeOffset? AccessTierIfModifiedSince { get { throw null; } set { } } + public System.DateTimeOffset? AccessTierIfUnmodifiedSince { get { throw null; } set { } } public string LeaseId { get { throw null; } set { } } public override string ToString() { throw null; } } @@ -1149,8 +1169,12 @@ public static partial class BlobsModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null) { throw null; } public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null, System.Collections.Generic.IDictionary tags = null) { throw null; } - public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string delegatedUserObjectId = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, string signedService, string signedVersion, string value, System.DateTimeOffset signedExpiresOn, System.DateTimeOffset signedStartsOn) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, string signedService = null, string signedVersion = null, string value = null, System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), string DelegatedUserObjectId = null) { throw null; } } public partial class BlobSnapshotInfo { @@ -1192,6 +1216,7 @@ public BlobSyncUploadFromUriOptions() { } public System.Collections.Generic.IDictionary Metadata { get { throw null; } set { } } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.BlobRequestConditions SourceConditions { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } public System.Collections.Generic.IDictionary Tags { get { throw null; } set { } } } @@ -1460,6 +1485,7 @@ public PageBlobUploadPagesFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.PageBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } public partial class PageBlobUploadPagesOptions @@ -1563,6 +1589,9 @@ public enum SkuName StandardRagrs = 2, StandardZrs = 3, PremiumLrs = 4, + StandardGzrs = 5, + PremiumZrs = 6, + StandardRagzrs = 7, } public partial class StageBlockFromUriOptions { @@ -1571,6 +1600,7 @@ public StageBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.RequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -1584,6 +1614,7 @@ internal TaggedBlobItem() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -2000,6 +2031,8 @@ public BlobSasBuilder(Azure.Storage.Sas.BlobSasPermissions permissions, System.D public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } set { } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestHeaders { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestQueryParameters { get { throw null; } set { } } public string Resource { get { throw null; } set { } } public string Snapshot { get { throw null; } set { } } public System.DateTimeOffset StartsOn { get { throw null; } set { } } @@ -2044,6 +2077,7 @@ public sealed partial class BlobSasQueryParameters : Azure.Storage.Sas.SasQueryP { internal BlobSasQueryParameters() { } public static new Azure.Storage.Sas.BlobSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.0.cs b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.0.cs index b80d50793d78..6b38ef0a593e 100644 --- a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.0.cs +++ b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.0.cs @@ -92,6 +92,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class BlobContainerClient @@ -224,8 +225,12 @@ public BlobServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyCr public virtual System.Threading.Tasks.Task> GetPropertiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -310,6 +315,7 @@ public AppendBlobAppendBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.AppendBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -536,6 +542,7 @@ public BlobDownloadDetails() { } public long BlobSequenceNumber { get { throw null; } } public Azure.Storage.Blobs.Models.BlobType BlobType { get { throw null; } } public string CacheControl { get { throw null; } } + public byte[] ContentCrc { get { throw null; } } public string ContentDisposition { get { throw null; } } public string ContentEncoding { get { throw null; } } public byte[] ContentHash { get { throw null; } } @@ -581,6 +588,7 @@ internal BlobDownloadInfo() { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string ContentType { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadOptions @@ -602,6 +610,7 @@ public partial class BlobDownloadStreamingResult : System.IDisposable internal BlobDownloadStreamingResult() { } public System.IO.Stream Content { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadToOptions @@ -655,6 +664,8 @@ public BlobDownloadToOptions() { } public static Azure.Storage.Blobs.Models.BlobErrorCode EmptyMetadataKey { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode FeatureVersionMismatch { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyBlobMismatch { get { throw null; } } + public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierSnapshotNotAllowed { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierVersionSnapshotNotAllowed { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEralierVersionSnapshotNotAllowed { get { throw null; } } @@ -763,6 +774,13 @@ public enum BlobGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class BlobGetUserDelegationKeyOptions + { + public BlobGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class BlobHierarchyItem { internal BlobHierarchyItem() { } @@ -1023,6 +1041,8 @@ protected BlobQueryTextOptions() { } public partial class BlobRequestConditions : Azure.Storage.Blobs.Models.BlobLeaseRequestConditions { public BlobRequestConditions() { } + public System.DateTimeOffset? AccessTierIfModifiedSince { get { throw null; } set { } } + public System.DateTimeOffset? AccessTierIfUnmodifiedSince { get { throw null; } set { } } public string LeaseId { get { throw null; } set { } } public override string ToString() { throw null; } } @@ -1149,8 +1169,12 @@ public static partial class BlobsModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null) { throw null; } public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null, System.Collections.Generic.IDictionary tags = null) { throw null; } - public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string delegatedUserObjectId = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, string signedService, string signedVersion, string value, System.DateTimeOffset signedExpiresOn, System.DateTimeOffset signedStartsOn) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, string signedService = null, string signedVersion = null, string value = null, System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), string DelegatedUserObjectId = null) { throw null; } } public partial class BlobSnapshotInfo { @@ -1192,6 +1216,7 @@ public BlobSyncUploadFromUriOptions() { } public System.Collections.Generic.IDictionary Metadata { get { throw null; } set { } } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.BlobRequestConditions SourceConditions { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } public System.Collections.Generic.IDictionary Tags { get { throw null; } set { } } } @@ -1460,6 +1485,7 @@ public PageBlobUploadPagesFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.PageBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } public partial class PageBlobUploadPagesOptions @@ -1563,6 +1589,9 @@ public enum SkuName StandardRagrs = 2, StandardZrs = 3, PremiumLrs = 4, + StandardGzrs = 5, + PremiumZrs = 6, + StandardRagzrs = 7, } public partial class StageBlockFromUriOptions { @@ -1571,6 +1600,7 @@ public StageBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.RequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -1584,6 +1614,7 @@ internal TaggedBlobItem() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -2000,6 +2031,8 @@ public BlobSasBuilder(Azure.Storage.Sas.BlobSasPermissions permissions, System.D public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } set { } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestHeaders { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestQueryParameters { get { throw null; } set { } } public string Resource { get { throw null; } set { } } public string Snapshot { get { throw null; } set { } } public System.DateTimeOffset StartsOn { get { throw null; } set { } } @@ -2044,6 +2077,7 @@ public sealed partial class BlobSasQueryParameters : Azure.Storage.Sas.SasQueryP { internal BlobSasQueryParameters() { } public static new Azure.Storage.Sas.BlobSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.1.cs b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.1.cs index b80d50793d78..6b38ef0a593e 100644 --- a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.1.cs +++ b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.netstandard2.1.cs @@ -92,6 +92,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class BlobContainerClient @@ -224,8 +225,12 @@ public BlobServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyCr public virtual System.Threading.Tasks.Task> GetPropertiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Blobs.Models.BlobGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Blobs.Models.BlobServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -310,6 +315,7 @@ public AppendBlobAppendBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.AppendBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -536,6 +542,7 @@ public BlobDownloadDetails() { } public long BlobSequenceNumber { get { throw null; } } public Azure.Storage.Blobs.Models.BlobType BlobType { get { throw null; } } public string CacheControl { get { throw null; } } + public byte[] ContentCrc { get { throw null; } } public string ContentDisposition { get { throw null; } } public string ContentEncoding { get { throw null; } } public byte[] ContentHash { get { throw null; } } @@ -581,6 +588,7 @@ internal BlobDownloadInfo() { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string ContentType { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadOptions @@ -602,6 +610,7 @@ public partial class BlobDownloadStreamingResult : System.IDisposable internal BlobDownloadStreamingResult() { } public System.IO.Stream Content { get { throw null; } } public Azure.Storage.Blobs.Models.BlobDownloadDetails Details { get { throw null; } } + public bool ExpectTrailingDetails { get { throw null; } } public void Dispose() { } } public partial class BlobDownloadToOptions @@ -655,6 +664,8 @@ public BlobDownloadToOptions() { } public static Azure.Storage.Blobs.Models.BlobErrorCode EmptyMetadataKey { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode FeatureVersionMismatch { get { throw null; } } public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyBlobMismatch { get { throw null; } } + public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierSnapshotNotAllowed { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEarlierVersionSnapshotNotAllowed { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.BlobErrorCode IncrementalCopyOfEralierVersionSnapshotNotAllowed { get { throw null; } } @@ -763,6 +774,13 @@ public enum BlobGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class BlobGetUserDelegationKeyOptions + { + public BlobGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class BlobHierarchyItem { internal BlobHierarchyItem() { } @@ -1023,6 +1041,8 @@ protected BlobQueryTextOptions() { } public partial class BlobRequestConditions : Azure.Storage.Blobs.Models.BlobLeaseRequestConditions { public BlobRequestConditions() { } + public System.DateTimeOffset? AccessTierIfModifiedSince { get { throw null; } set { } } + public System.DateTimeOffset? AccessTierIfUnmodifiedSince { get { throw null; } set { } } public string LeaseId { get { throw null; } set { } } public override string ToString() { throw null; } } @@ -1149,8 +1169,12 @@ public static partial class BlobsModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null) { throw null; } public static Azure.Storage.Blobs.Models.TaggedBlobItem TaggedBlobItem(string blobName = null, string blobContainerName = null, System.Collections.Generic.IDictionary tags = null) { throw null; } - public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string delegatedUserObjectId = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, string signedService, string signedVersion, string value, System.DateTimeOffset signedExpiresOn, System.DateTimeOffset signedStartsOn) { throw null; } + public static Azure.Storage.Blobs.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, string signedService = null, string signedVersion = null, string value = null, System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), string DelegatedUserObjectId = null) { throw null; } } public partial class BlobSnapshotInfo { @@ -1192,6 +1216,7 @@ public BlobSyncUploadFromUriOptions() { } public System.Collections.Generic.IDictionary Metadata { get { throw null; } set { } } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.BlobRequestConditions SourceConditions { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } public System.Collections.Generic.IDictionary Tags { get { throw null; } set { } } } @@ -1460,6 +1485,7 @@ public PageBlobUploadPagesFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.Storage.Blobs.Models.PageBlobRequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } public partial class PageBlobUploadPagesOptions @@ -1563,6 +1589,9 @@ public enum SkuName StandardRagrs = 2, StandardZrs = 3, PremiumLrs = 4, + StandardGzrs = 5, + PremiumZrs = 6, + StandardRagzrs = 7, } public partial class StageBlockFromUriOptions { @@ -1571,6 +1600,7 @@ public StageBlockFromUriOptions() { } public Azure.HttpAuthorization SourceAuthentication { get { throw null; } set { } } public Azure.RequestConditions SourceConditions { get { throw null; } set { } } public byte[] SourceContentHash { get { throw null; } set { } } + public Azure.Storage.Blobs.Models.CustomerProvidedKey? SourceCustomerProvidedKey { get { throw null; } set { } } public Azure.HttpRange SourceRange { get { throw null; } set { } } public Azure.Storage.Blobs.Models.FileShareTokenIntent? SourceShareTokenIntent { get { throw null; } set { } } } @@ -1584,6 +1614,7 @@ internal TaggedBlobItem() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -2000,6 +2031,8 @@ public BlobSasBuilder(Azure.Storage.Sas.BlobSasPermissions permissions, System.D public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } set { } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestHeaders { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestQueryParameters { get { throw null; } set { } } public string Resource { get { throw null; } set { } } public string Snapshot { get { throw null; } set { } } public System.DateTimeOffset StartsOn { get { throw null; } set { } } @@ -2044,6 +2077,7 @@ public sealed partial class BlobSasQueryParameters : Azure.Storage.Sas.SasQueryP { internal BlobSasQueryParameters() { } public static new Azure.Storage.Sas.BlobSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Blobs/assets.json b/sdk/storage/Azure.Storage.Blobs/assets.json index 48511d06cc0d..20af2a61135c 100644 --- a/sdk/storage/Azure.Storage.Blobs/assets.json +++ b/sdk/storage/Azure.Storage.Blobs/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Blobs", - "Tag": "net/storage/Azure.Storage.Blobs_6b5ddc8447" + "Tag": "net/storage/Azure.Storage.Blobs_6ff82d4055" } diff --git a/sdk/storage/Azure.Storage.Blobs/samples/Azure.Storage.Blobs.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Blobs/samples/Azure.Storage.Blobs.Samples.Tests.csproj index 77fd767c3486..568dd6cba951 100644 --- a/sdk/storage/Azure.Storage.Blobs/samples/Azure.Storage.Blobs.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs/samples/Azure.Storage.Blobs.Samples.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/sdk/storage/Azure.Storage.Blobs/src/AppendBlobClient.cs b/sdk/storage/Azure.Storage.Blobs/src/AppendBlobClient.cs index 696cdd784390..844694739d9c 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/AppendBlobClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/AppendBlobClient.cs @@ -892,7 +892,9 @@ private async Task> CreateInternal( conditions.ValidateConditionsNotPresent( invalidConditions: BlobRequestConditionProperty.IfAppendPositionEqual - | BlobRequestConditionProperty.IfMaxSizeLessThanOrEqual, + | BlobRequestConditionProperty.IfMaxSizeLessThanOrEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(AppendBlobClient.Create), parameterName: nameof(conditions)); @@ -1260,9 +1262,10 @@ internal async Task> AppendBlockInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(AppendBlobClient)}.{nameof(AppendBlock)}"); - // All AppendBlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(AppendBlobClient.AppendBlock), parameterName: nameof(conditions)); @@ -1272,14 +1275,39 @@ internal async Task> AppendBlockInternal( BlobErrors.VerifyHttpsCustomerProvidedKey(Uri, ClientConfiguration.CustomerProvidedKey); Errors.VerifyStreamPosition(content, nameof(content)); - // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); - - content = content.WithNoDispose().WithProgress(progressHandler); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + if (validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && + ClientSideEncryption == null) // don't allow feature combination + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + contentLength = (content?.Length - content?.Position) ?? 0; + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content.WithNoDispose().WithProgress(progressHandler); + content = validationOptions.PrecalculatedChecksum.IsEmpty + ? new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64) + : new StructuredMessagePrecalculatedCrcWrapperStream( + content, + validationOptions.PrecalculatedChecksum.Span); + contentLength = (content?.Length - content?.Position) ?? 0; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content.WithNoDispose().WithProgress(progressHandler); + } ResponseWithHeaders response; @@ -1297,6 +1325,8 @@ internal async Task> AppendBlockInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, ifModifiedSince: conditions?.IfModifiedSince, ifUnmodifiedSince: conditions?.IfUnmodifiedSince, ifMatch: conditions?.IfMatch?.ToString(), @@ -1319,6 +1349,8 @@ internal async Task> AppendBlockInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, ifModifiedSince: conditions?.IfModifiedSince, ifUnmodifiedSince: conditions?.IfUnmodifiedSince, ifMatch: conditions?.IfMatch?.ToString(), @@ -1394,6 +1426,7 @@ public virtual Response AppendBlockFromUri( options?.SourceConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: false, cancellationToken) .EnsureCompleted(); @@ -1445,6 +1478,7 @@ await AppendBlockFromUriInternal( options?.SourceConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: true, cancellationToken) .ConfigureAwait(false); @@ -1523,6 +1557,7 @@ public virtual Response AppendBlockFromUri( sourceConditions, sourceAuthentication: default, sourceShareTokenIntent: default, + sourceCustomerProvidedKey: default, async: false, cancellationToken) .EnsureCompleted(); @@ -1601,6 +1636,7 @@ await AppendBlockFromUriInternal( sourceConditions, sourceAuthentication: default, sourceShareTokenIntent: default, + sourceCustomerProvidedKey: default, async: true, cancellationToken) .ConfigureAwait(false); @@ -1654,6 +1690,9 @@ await AppendBlockFromUriInternal( /// Optional, only applicable (but required) when the source is Azure Storage Files and using token authentication. /// Used to indicate the intent of the request. /// + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// /// /// Whether to invoke the operation asynchronously. /// @@ -1679,6 +1718,7 @@ private async Task> AppendBlockFromUriInternal( AppendBlobRequestConditions sourceConditions, HttpAuthorization sourceAuthentication, FileShareTokenIntent? sourceShareTokenIntent, + CustomerProvidedKey? sourceCustomerProvidedKey, bool async, CancellationToken cancellationToken = default) { @@ -1693,9 +1733,10 @@ private async Task> AppendBlockFromUriInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(AppendBlobClient)}.{nameof(AppendBlockFromUri)}"); - // All destination AppendBlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(AppendBlobClient.AppendBlockFromUri), parameterName: nameof(conditions)); @@ -1704,7 +1745,9 @@ private async Task> AppendBlockFromUriInternal( BlobRequestConditionProperty.LeaseId | BlobRequestConditionProperty.TagConditions | BlobRequestConditionProperty.IfAppendPositionEqual - | BlobRequestConditionProperty.IfMaxSizeLessThanOrEqual, + | BlobRequestConditionProperty.IfMaxSizeLessThanOrEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(AppendBlobClient.AppendBlockFromUri), parameterName: nameof(sourceConditions)); @@ -1738,6 +1781,9 @@ private async Task> AppendBlockFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceShareTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -1766,6 +1812,9 @@ private async Task> AppendBlockFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceShareTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken); } @@ -1888,7 +1937,9 @@ private async Task> SealInternal( conditions.ValidateConditionsNotPresent( invalidConditions: BlobRequestConditionProperty.IfMaxSizeLessThanOrEqual - | BlobRequestConditionProperty.TagConditions, + | BlobRequestConditionProperty.TagConditions + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(AppendBlobClient.Seal), parameterName: nameof(conditions)); diff --git a/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj b/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj index e20171e33a80..6c70453fc594 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj +++ b/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.Blobs client library - 12.27.0-beta.2 + 12.28.0-beta.1 12.26.0 BlobSDK;$(DefineConstants) @@ -61,6 +61,8 @@ + + @@ -100,6 +102,11 @@ + + + + + diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs index c096f0c57e02..bdf046441cf9 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs @@ -1023,6 +1023,13 @@ private async Task> DownloadInternal( bool async, CancellationToken cancellationToken) { + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.Download), + parameterName: nameof(conditions)); + Response response = await DownloadStreamingDirect( range, conditions, @@ -1049,6 +1056,7 @@ private async Task> DownloadInternal( ContentHash = blobDownloadDetails.ContentHash, ContentLength = blobDownloadDetails.ContentLength, ContentType = blobDownloadDetails.ContentType, + ExpectTrailingDetails = blobDownloadStreamingResult.ExpectTrailingDetails, }, response.GetRawResponse()); } #endregion @@ -1556,6 +1564,13 @@ internal virtual async ValueTask> Download disposableBucket.Add(Shared.StorageExtensions.CreateClientSideEncryptionScope(ClientSideEncryption.EncryptionVersion)); } + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.DownloadStreaming), + parameterName: nameof(conditions)); + // Start downloading the blob Response response = await StartDownloadAsync( range, @@ -1577,30 +1592,52 @@ internal virtual async ValueTask> Download // Wrap the response Content in a RetriableStream so we // can return it before it's finished downloading, but still // allow retrying if it fails. - Stream stream = RetriableStream.Create( - response.Value.Content, - startOffset => - StartDownloadAsync( - range, - conditionsWithEtag, - validationOptions, - startOffset, - async, - cancellationToken) - .EnsureCompleted() - .Value.Content, - async startOffset => - (await StartDownloadAsync( - range, - conditionsWithEtag, - validationOptions, - startOffset, - async, - cancellationToken) - .ConfigureAwait(false)) - .Value.Content, - ClientConfiguration.Pipeline.ResponseClassifier, - Constants.MaxReliabilityRetries); + ValueTask> Factory(long offset, bool async, CancellationToken cancellationToken) + => StartDownloadAsync( + range, + conditionsWithEtag, + validationOptions, + offset, + async, + cancellationToken); + async ValueTask<(Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData)> StructuredMessageFactory( + long offset, bool async, CancellationToken cancellationToken) + { + Response result = await Factory(offset, async, cancellationToken).ConfigureAwait(false); + return StructuredMessageDecodingStream.WrapStream(result.Value.Content, result.Value.Details.ContentLength); + } + Stream stream; + if (response.GetRawResponse().Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) + { + (Stream decodingStream, StructuredMessageDecodingStream.RawDecodedData decodedData) = StructuredMessageDecodingStream.WrapStream( + response.Value.Content, response.Value.Details.ContentLength); + stream = new StructuredMessageDecodingRetriableStream( + decodingStream, + decodedData, + StructuredMessage.Flags.StorageCrc64, + startOffset => StructuredMessageFactory(startOffset, async: false, cancellationToken) + .EnsureCompleted(), + async startOffset => await StructuredMessageFactory(startOffset, async: true, cancellationToken) + .ConfigureAwait(false), + decodedData => + { + response.Value.Details.ContentCrc = new byte[StructuredMessage.Crc64Length]; + decodedData.Crc.WriteCrc64(response.Value.Details.ContentCrc); + }, + ClientConfiguration.Pipeline.ResponseClassifier, + Constants.MaxReliabilityRetries); + } + else + { + stream = RetriableStream.Create( + response.Value.Content, + startOffset => Factory(startOffset, async: false, cancellationToken) + .EnsureCompleted().Value.Content, + async startOffset => (await Factory(startOffset, async: true, cancellationToken) + .ConfigureAwait(false)).Value.Content, + ClientConfiguration.Pipeline.ResponseClassifier, + Constants.MaxReliabilityRetries); + } stream = stream.WithProgress(progressHandler); @@ -1608,7 +1645,11 @@ internal virtual async ValueTask> Download * Buffer response stream and ensure it matches the transactional checksum if any. * Storage will not return a checksum for payload >4MB, so this buffer is capped similarly. * Checksum validation is opt-in, so this buffer is part of that opt-in. */ - if (validationOptions != default && validationOptions.ChecksumAlgorithm != StorageChecksumAlgorithm.None && validationOptions.AutoValidateChecksum) + if (validationOptions != default && + validationOptions.ChecksumAlgorithm != StorageChecksumAlgorithm.None && + validationOptions.AutoValidateChecksum && + // structured message decoding does the validation for us + !response.GetRawResponse().Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) { // safe-buffer; transactional hash download limit well below maxInt var readDestStream = new MemoryStream((int)response.Value.Details.ContentLength); @@ -1679,8 +1720,8 @@ await ContentHasher.AssertResponseHashMatchInternal( /// notifications that the operation should be cancelled. /// /// - /// A describing the - /// downloaded blob. contains + /// A describing the + /// downloaded blob. contains /// the blob's data. /// /// @@ -1715,19 +1756,36 @@ private async ValueTask> StartDownloadAsyn ResponseWithHeaders response; - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.Download), parameterName: nameof(conditions)); + bool? rangeGetContentMD5 = null; + bool? rangeGetContentCRC64 = null; + string structuredBodyType = null; + switch (validationOptions?.ChecksumAlgorithm.ResolveAuto()) + { + case StorageChecksumAlgorithm.MD5: + rangeGetContentMD5 = true; + break; + case StorageChecksumAlgorithm.StorageCrc64: + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + break; + default: + break; + } + if (async) { response = await BlobRestClient.DownloadAsync( range: pageRange?.ToString(), leaseId: conditions?.LeaseId, - rangeGetContentMD5: validationOptions?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.MD5 ? true : null, - rangeGetContentCRC64: validationOptions?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 ? true : null, + rangeGetContentMD5: rangeGetContentMD5, + rangeGetContentCRC64: rangeGetContentCRC64, + structuredBodyType: structuredBodyType, encryptionKey: ClientConfiguration.CustomerProvidedKey?.EncryptionKey, encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, @@ -1744,8 +1802,9 @@ private async ValueTask> StartDownloadAsyn response = BlobRestClient.Download( range: pageRange?.ToString(), leaseId: conditions?.LeaseId, - rangeGetContentMD5: validationOptions?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.MD5 ? true : null, - rangeGetContentCRC64: validationOptions?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 ? true : null, + rangeGetContentMD5: rangeGetContentMD5, + rangeGetContentCRC64: rangeGetContentCRC64, + structuredBodyType: structuredBodyType, encryptionKey: ClientConfiguration.CustomerProvidedKey?.EncryptionKey, encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, @@ -1761,9 +1820,11 @@ private async ValueTask> StartDownloadAsyn long length = response.IsUnavailable() ? 0 : response.Headers.ContentLength ?? 0; ClientConfiguration.Pipeline.LogTrace($"Response: {response.GetRawResponse().Status}, ContentLength: {length}"); - return Response.FromValue( + Response result = Response.FromValue( response.ToBlobDownloadStreamingResult(), response.GetRawResponse()); + result.Value.ExpectTrailingDetails = structuredBodyType != null; + return result; } #endregion @@ -2296,6 +2357,13 @@ private async Task> DownloadContentInternal( bool async, CancellationToken cancellationToken) { + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.DownloadContent), + parameterName: nameof(conditions)); + Response response = await DownloadStreamingDirect( range, conditions, @@ -3271,6 +3339,14 @@ internal async Task OpenReadInternal( string operationName = $"{nameof(BlobBaseClient)}.{nameof(OpenRead)}"; DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope(operationName); + + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.OpenRead), + parameterName: nameof(conditions)); + try { scope.Start(); @@ -3769,15 +3845,18 @@ private async Task> StartCopyFromUriInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(StartCopyFromUri)}"); - // All BlobRequestConditions are valid. destinationConditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.StartCopyFromUri), parameterName: nameof(destinationConditions)); sourceConditions.ValidateConditionsNotPresent( invalidConditions: - BlobRequestConditionProperty.LeaseId, + BlobRequestConditionProperty.LeaseId + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.StartCopyFromUri), parameterName: nameof(sourceConditions)); @@ -3993,7 +4072,9 @@ private async Task AbortCopyFromUriInternal( | BlobRequestConditionProperty.IfUnmodifiedSince | BlobRequestConditionProperty.IfMatch | BlobRequestConditionProperty.IfNoneMatch - | BlobRequestConditionProperty.TagConditions, + | BlobRequestConditionProperty.TagConditions + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.AbortCopyFromUri), parameterName: nameof(conditions)); @@ -4243,14 +4324,18 @@ private async Task> SyncCopyFromUriInternal( { DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(SyncCopyFromUri)}"); - // All BlobRequestConditions are valid for destinationConditions. destinationConditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SyncCopyFromUri), parameterName: nameof(destinationConditions)); sourceConditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.LeaseId, + invalidConditions: + BlobRequestConditionProperty.LeaseId + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SyncCopyFromUri), parameterName: nameof(sourceConditions)); @@ -4676,6 +4761,8 @@ private async Task DeleteInternal( ifMatch: conditions?.IfMatch?.ToString(), ifNoneMatch: conditions?.IfNoneMatch?.ToString(), ifTags: conditions?.TagConditions, + accessTierIfModifiedSince: conditions?.AccessTierIfModifiedSince, + accessTierIfUnmodifiedSince: conditions?.AccessTierIfUnmodifiedSince, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -4689,6 +4776,8 @@ private async Task DeleteInternal( ifMatch: conditions?.IfMatch?.ToString(), ifNoneMatch: conditions?.IfNoneMatch?.ToString(), ifTags: conditions?.TagConditions, + accessTierIfModifiedSince: conditions?.AccessTierIfModifiedSince, + accessTierIfUnmodifiedSince: conditions?.AccessTierIfUnmodifiedSince, cancellationToken: cancellationToken); } @@ -5087,9 +5176,10 @@ internal async Task> GetPropertiesInternal( operationName ??= $"{nameof(BlobBaseClient)}.{nameof(GetProperties)}"; DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope(operationName); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.GetProperties), parameterName: nameof(conditions)); @@ -5275,9 +5365,10 @@ private async Task> SetHttpHeadersInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(SetHttpHeaders)}"); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SetHttpHeaders), parameterName: nameof(conditions)); @@ -5470,9 +5561,10 @@ internal async Task> SetMetadataInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(SetMetadata)}"); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SetMetadata), parameterName: nameof(conditions)); @@ -5663,9 +5755,10 @@ private async Task> CreateSnapshotInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(CreateSnapshot)}"); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.CreateSnapshot), parameterName: nameof(conditions)); @@ -5903,7 +5996,9 @@ private async Task SetAccessTierInternal( BlobRequestConditionProperty.IfModifiedSince | BlobRequestConditionProperty.IfUnmodifiedSince | BlobRequestConditionProperty.IfMatch - | BlobRequestConditionProperty.IfNoneMatch, + | BlobRequestConditionProperty.IfNoneMatch + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SetAccessTier), parameterName: nameof(conditions)); @@ -6060,6 +6155,13 @@ private async Task> GetTagsInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(GetTags)}"); + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.GetTags), + parameterName: nameof(conditions)); + try { scope.Start(); @@ -6249,6 +6351,13 @@ private async Task SetTagsInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlobBaseClient)}.{nameof(SetTags)}"); + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.SetTags), + parameterName: nameof(conditions)); + try { scope.Start(); @@ -6431,7 +6540,9 @@ private async Task> SetImmutabilityPolicyIntern | BlobRequestConditionProperty.IfNoneMatch | BlobRequestConditionProperty.IfModifiedSince | BlobRequestConditionProperty.LeaseId - | BlobRequestConditionProperty.TagConditions, + | BlobRequestConditionProperty.TagConditions + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlobBaseClient.SetImmutabilityPolicy), parameterName: nameof(conditions)); @@ -7046,7 +7157,7 @@ public virtual Uri GenerateSasUri(BlobSasBuilder builder, out string stringToSig /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -7079,7 +7190,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -7118,7 +7229,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -7146,7 +7257,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobSasBuilder builder, UserDele /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs index 0f33c39299d0..5a4f5632c42e 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs @@ -176,7 +176,12 @@ public enum ServiceVersion /// /// The 2026-02-06 service version. /// - V2026_02_06 = 29 + V2026_02_06 = 29, + + /// + /// The 2026-04-06 service version. + /// + V2026_04_06 = 30 #pragma warning restore CA1707 // Identifiers should not contain underscores } @@ -338,6 +343,8 @@ private void AddHeadersAndQueryParameters() Diagnostics.LoggedHeaderNames.Add("x-ms-encryption-key-sha256"); Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-error-code"); Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-status-code"); + Diagnostics.LoggedHeaderNames.Add("x-ms-structured-body"); + Diagnostics.LoggedHeaderNames.Add("x-ms-structured-content-length"); Diagnostics.LoggedQueryParameters.Add("comp"); Diagnostics.LoggedQueryParameters.Add("maxresults"); diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobClientSideDecryptor.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobClientSideDecryptor.cs index 0d03c057fee5..2ab39be2784c 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobClientSideDecryptor.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobClientSideDecryptor.cs @@ -187,7 +187,7 @@ private static bool CanIgnorePadding(ContentRange? contentRange) // did we request the last block? // end is inclusive/0-index, so end = n and size = n+1 means we requested the last block - if (contentRange.Value.Size - contentRange.Value.End == 1) + if (contentRange.Value.TotalResourceLength - contentRange.Value.End == 1) { return false; } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs index 052c0a836a91..4f50ee82b241 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs @@ -4140,7 +4140,7 @@ public virtual Uri GenerateSasUri(BlobSasBuilder builder, out string stringToSig /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4173,7 +4173,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobContainerSasPermissions perm /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -4205,7 +4205,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobContainerSasPermissions perm /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4233,7 +4233,7 @@ public virtual Uri GenerateUserDelegationSasUri(BlobSasBuilder builder, UserDele /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobExtensions.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobExtensions.cs index a82a7be5eb7e..ae025bbb68a5 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobExtensions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobExtensions.cs @@ -1786,6 +1786,18 @@ internal static void ValidateConditionsNotPresent( invalidList ??= new List(); invalidList.Add(nameof(BlobRequestConditions.LeaseId)); } + if ((invalidConditions & BlobRequestConditionProperty.AccessTierIfModifiedSince) == BlobRequestConditionProperty.AccessTierIfModifiedSince + && requestConditions.AccessTierIfModifiedSince != null) + { + invalidList ??= new List(); + invalidList.Add(nameof(BlobRequestConditions.AccessTierIfModifiedSince)); + } + if ((invalidConditions & BlobRequestConditionProperty.AccessTierIfUnmodifiedSince) == BlobRequestConditionProperty.AccessTierIfUnmodifiedSince + && requestConditions.AccessTierIfUnmodifiedSince != null) + { + invalidList ??= new List(); + invalidList.Add(nameof(BlobRequestConditions.AccessTierIfUnmodifiedSince)); + } } #endregion } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs index 775051b352a0..de08d83b90d3 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs @@ -1476,7 +1476,83 @@ private async Task> GetStatisticsInternal( #region GetUserDelegationKey /// - /// The operation retrieves a + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + public virtual Response GetUserDelegationKey( + BlobGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + false, // async + cancellationToken) + .EnsureCompleted(); + } + + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + public virtual async Task> GetUserDelegationKeyAsync( + BlobGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return await GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + true, // async + cancellationToken) + .ConfigureAwait(false); + } + + /// + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -1507,19 +1583,23 @@ private async Task> GetStatisticsInternal( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual Response GetUserDelegationKey( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => GetUserDelegationKeyInternal( startsOn, expiresOn, + default, false, // async cancellationToken) .EnsureCompleted(); /// - /// The operation retrieves a + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -1550,13 +1630,17 @@ public virtual Response GetUserDelegationKey( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual async Task> GetUserDelegationKeyAsync( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => await GetUserDelegationKeyInternal( startsOn, expiresOn, + default, true, // async cancellationToken) .ConfigureAwait(false); @@ -1578,6 +1662,9 @@ await GetUserDelegationKeyInternal( /// Expiration of the key's validity. The time should be specified /// in UTC. /// + /// + /// The delegated user tenant id in Azure AD. + /// /// /// Optional to propagate /// notifications that the operation should be cancelled. @@ -1596,6 +1683,7 @@ await GetUserDelegationKeyInternal( private async Task> GetUserDelegationKeyInternal( DateTimeOffset? startsOn, DateTimeOffset expiresOn, + string delegatedUserTenantId, bool async, CancellationToken cancellationToken) { @@ -1621,7 +1709,8 @@ private async Task> GetUserDelegationKeyInternal( KeyInfo keyInfo = new KeyInfo(expiresOn.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture)) { - Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture) + Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture), + DelegatedUserTid = delegatedUserTenantId }; ResponseWithHeaders response; diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlockBlobClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlockBlobClient.cs index 121643e50800..aa5b699a374e 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlockBlobClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlockBlobClient.cs @@ -866,9 +866,10 @@ internal virtual async Task> UploadInternal( operationName ??= $"{nameof(BlockBlobClient)}.{nameof(Upload)}"; DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope(operationName); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.Upload), parameterName: nameof(conditions)); @@ -885,14 +886,35 @@ internal virtual async Task> UploadInternal( scope.Start(); Errors.VerifyStreamPosition(content, nameof(content)); - // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); - - content = content?.WithNoDispose().WithProgress(progressHandler); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + if (content != null && + validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && + ClientSideEncryption == null) // don't allow feature combination + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content.WithNoDispose().WithProgress(progressHandler); + content = new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64); + contentLength = content.Length - content.Position; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content.WithNoDispose().WithProgress(progressHandler); + } ResponseWithHeaders response; @@ -931,6 +953,8 @@ internal virtual async Task> UploadInternal( legalHold: legalHold, transactionalContentMD5: hashResult?.MD5AsArray, transactionalContentCrc64: hashResult?.StorageCrc64AsArray, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -963,6 +987,8 @@ internal virtual async Task> UploadInternal( legalHold: legalHold, transactionalContentMD5: hashResult?.MD5AsArray, transactionalContentCrc64: hashResult?.StorageCrc64AsArray, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, cancellationToken: cancellationToken); } @@ -1320,7 +1346,9 @@ internal virtual async Task> StageBlockInternal( | BlobRequestConditionProperty.IfUnmodifiedSince | BlobRequestConditionProperty.TagConditions | BlobRequestConditionProperty.IfMatch - | BlobRequestConditionProperty.IfNoneMatch, + | BlobRequestConditionProperty.IfNoneMatch + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.StageBlock), parameterName: nameof(conditions)); @@ -1330,14 +1358,39 @@ internal virtual async Task> StageBlockInternal( Errors.VerifyStreamPosition(content, nameof(content)); - // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); - - content = content.WithNoDispose().WithProgress(progressHandler); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + if (validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && + ClientSideEncryption == null) // don't allow feature combination + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + contentLength = (content?.Length - content?.Position) ?? 0; + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content.WithNoDispose().WithProgress(progressHandler); + content = validationOptions.PrecalculatedChecksum.IsEmpty + ? new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64) + : new StructuredMessagePrecalculatedCrcWrapperStream( + content, + validationOptions.PrecalculatedChecksum.Span); + contentLength = (content?.Length - content?.Position) ?? 0; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content.WithNoDispose().WithProgress(progressHandler); + } ResponseWithHeaders response; @@ -1345,7 +1398,7 @@ internal virtual async Task> StageBlockInternal( { response = await BlockBlobRestClient.StageBlockAsync( blockId: base64BlockId, - contentLength: (content?.Length - content?.Position) ?? 0, + contentLength: contentLength, body: content, transactionalContentCrc64: hashResult?.StorageCrc64AsArray, transactionalContentMD5: hashResult?.MD5AsArray, @@ -1354,6 +1407,8 @@ internal virtual async Task> StageBlockInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -1361,7 +1416,7 @@ internal virtual async Task> StageBlockInternal( { response = BlockBlobRestClient.StageBlock( blockId: base64BlockId, - contentLength: (content?.Length - content?.Position) ?? 0, + contentLength: contentLength, body: content, transactionalContentCrc64: hashResult?.StorageCrc64AsArray, transactionalContentMD5: hashResult?.MD5AsArray, @@ -1370,6 +1425,8 @@ internal virtual async Task> StageBlockInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, cancellationToken: cancellationToken); } @@ -1448,6 +1505,7 @@ public virtual Response StageBlockFromUri( options?.DestinationConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: false, cancellationToken) .EnsureCompleted(); @@ -1507,6 +1565,7 @@ await StageBlockFromUriInternal( options?.DestinationConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: true, cancellationToken) .ConfigureAwait(false); @@ -1593,6 +1652,7 @@ public virtual Response StageBlockFromUri( conditions, sourceAuthentication: default, sourceShareTokenIntent: default, + sourceCustomerProvidedKey: default, async: false, cancellationToken) .EnsureCompleted(); @@ -1679,6 +1739,7 @@ await StageBlockFromUriInternal( conditions, sourceAuthentication: default, sourceShareTokenIntent: default, + sourceCustomerProvidedKey: default, async: true, cancellationToken) .ConfigureAwait(false); @@ -1738,6 +1799,9 @@ await StageBlockFromUriInternal( /// Optional, only applicable (but required) when the source is Azure Storage Files and using token authentication. /// Used to indicate the intent of the request. /// + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// /// /// Whether to invoke the operation asynchronously. /// @@ -1764,6 +1828,7 @@ private async Task> StageBlockFromUriInternal( BlobRequestConditions conditions, HttpAuthorization sourceAuthentication, FileShareTokenIntent? sourceShareTokenIntent, + CustomerProvidedKey? sourceCustomerProvidedKey, bool async, CancellationToken cancellationToken) { @@ -1785,7 +1850,9 @@ private async Task> StageBlockFromUriInternal( | BlobRequestConditionProperty.IfUnmodifiedSince | BlobRequestConditionProperty.TagConditions | BlobRequestConditionProperty.IfMatch - | BlobRequestConditionProperty.IfNoneMatch, + | BlobRequestConditionProperty.IfNoneMatch + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.StageBlockFromUri), parameterName: nameof(conditions)); @@ -1819,6 +1886,9 @@ private async Task> StageBlockFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceShareTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -1841,6 +1911,9 @@ private async Task> StageBlockFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceShareTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken); } @@ -2241,9 +2314,10 @@ internal virtual async Task> CommitBlockListInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlockBlobClient)}.{nameof(CommitBlockList)}"); - // All BlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.CommitBlockList), parameterName: nameof(conditions)); @@ -2511,7 +2585,9 @@ private async Task> GetBlockListInternal( BlobRequestConditionProperty.IfModifiedSince | BlobRequestConditionProperty.IfUnmodifiedSince | BlobRequestConditionProperty.IfMatch - | BlobRequestConditionProperty.IfNoneMatch, + | BlobRequestConditionProperty.IfNoneMatch + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.GetBlockList), parameterName: nameof(conditions)); @@ -2678,9 +2754,10 @@ private async Task> QueryInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlockBlobClient)}.{nameof(Query)}"); - // All BlobRequestConditions are valid. options?.Conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.Query), parameterName: nameof(BlobQueryOptions.Conditions)); @@ -2886,7 +2963,7 @@ internal async Task OpenWriteInternal( immutabilityPolicy: default, legalHold: default, progressHandler: default, - transferValidationOverride: default, + transferValidationOverride: new() { ChecksumAlgorithm = StorageChecksumAlgorithm.None }, operationName: default, async: async, cancellationToken: cancellationToken) @@ -3156,14 +3233,18 @@ internal virtual async Task> SyncUploadFromUriInternal DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(BlockBlobClient)}.{nameof(SyncUploadFromUri)}"); - // All BlobRequestConditions are valid for options.DestinationConditions. options?.DestinationConditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.SyncUploadFromUri), parameterName: nameof(BlobSyncUploadFromUriOptions.DestinationConditions)); options?.SourceConditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.LeaseId, + invalidConditions: + BlobRequestConditionProperty.LeaseId + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(BlockBlobClient.SyncUploadFromUri), parameterName: nameof(BlobSyncUploadFromUriOptions.SourceConditions)); @@ -3206,6 +3287,9 @@ internal virtual async Task> SyncUploadFromUriInternal copySourceAuthorization: options?.SourceAuthentication?.ToString(), copySourceTags: options?.CopySourceTagsMode, fileRequestIntent: options?.SourceShareTokenIntent, + sourceEncryptionKey: options?.SourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: options?.SourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: options?.SourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -3243,6 +3327,9 @@ internal virtual async Task> SyncUploadFromUriInternal copySourceAuthorization: options?.SourceAuthentication?.ToString(), copySourceTags: options?.CopySourceTagsMode, fileRequestIntent: options?.SourceShareTokenIntent, + sourceEncryptionKey: options?.SourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: options?.SourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: options?.SourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken); } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/AppendBlobRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/AppendBlobRestClient.cs index 67a713dab5ae..6a5fc6d47c26 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/AppendBlobRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/AppendBlobRestClient.cs @@ -29,7 +29,7 @@ internal partial class AppendBlobRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public AppendBlobRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { @@ -386,7 +386,7 @@ public ResponseWithHeaders AppendBlock(long conten } } - internal HttpMessage CreateAppendBlockFromUrlRequest(string sourceUrl, long contentLength, string sourceRange, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, byte[] transactionalContentMD5, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, long? maxSize, long? appendPosition, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent) + internal HttpMessage CreateAppendBlockFromUrlRequest(string sourceUrl, long contentLength, string sourceRange, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, byte[] transactionalContentMD5, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, long? maxSize, long? appendPosition, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent, string sourceEncryptionKey, string sourceEncryptionKeySha256, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm) { var message = _pipeline.CreateMessage(); var request = message.Request; @@ -485,6 +485,18 @@ internal HttpMessage CreateAppendBlockFromUrlRequest(string sourceUrl, long cont { request.Headers.Add("x-ms-file-request-intent", fileRequestIntent.Value.ToString()); } + if (sourceEncryptionKey != null) + { + request.Headers.Add("x-ms-source-encryption-key", sourceEncryptionKey); + } + if (sourceEncryptionKeySha256 != null) + { + request.Headers.Add("x-ms-source-encryption-key-sha256", sourceEncryptionKeySha256); + } + if (sourceEncryptionAlgorithm != null) + { + request.Headers.Add("x-ms-source-encryption-algorithm", sourceEncryptionAlgorithm.Value.ToSerialString()); + } request.Headers.Add("Accept", "application/xml"); return message; } @@ -515,16 +527,19 @@ internal HttpMessage CreateAppendBlockFromUrlRequest(string sourceUrl, long cont /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// is null. - public async Task> AppendBlockFromUrlAsync(string sourceUrl, long contentLength, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, byte[] transactionalContentMD5 = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? maxSize = null, long? appendPosition = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public async Task> AppendBlockFromUrlAsync(string sourceUrl, long contentLength, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, byte[] transactionalContentMD5 = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? maxSize = null, long? appendPosition = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (sourceUrl == null) { throw new ArgumentNullException(nameof(sourceUrl)); } - using var message = CreateAppendBlockFromUrlRequest(sourceUrl, contentLength, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, transactionalContentMD5, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, maxSize, appendPosition, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateAppendBlockFromUrlRequest(sourceUrl, contentLength, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, transactionalContentMD5, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, maxSize, appendPosition, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); var headers = new AppendBlobAppendBlockFromUrlHeaders(message.Response); switch (message.Response.Status) @@ -562,16 +577,19 @@ public async Task> Appe /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// is null. - public ResponseWithHeaders AppendBlockFromUrl(string sourceUrl, long contentLength, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, byte[] transactionalContentMD5 = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? maxSize = null, long? appendPosition = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public ResponseWithHeaders AppendBlockFromUrl(string sourceUrl, long contentLength, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, byte[] transactionalContentMD5 = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? maxSize = null, long? appendPosition = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (sourceUrl == null) { throw new ArgumentNullException(nameof(sourceUrl)); } - using var message = CreateAppendBlockFromUrlRequest(sourceUrl, contentLength, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, transactionalContentMD5, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, maxSize, appendPosition, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateAppendBlockFromUrlRequest(sourceUrl, contentLength, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, transactionalContentMD5, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, maxSize, appendPosition, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); _pipeline.Send(message, cancellationToken); var headers = new AppendBlobAppendBlockFromUrlHeaders(message.Response); switch (message.Response.Status) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs index 720caef66b4a..8e70ad644edf 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs @@ -30,7 +30,7 @@ internal partial class BlobRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public BlobRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { @@ -436,7 +436,7 @@ public virtual Response GetProperties(string snapshot = null, string versionId = } } - internal HttpMessage CreateDeleteRequest(string snapshot, string versionId, int? timeout, string leaseId, DeleteSnapshotsOption? deleteSnapshots, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, BlobDeleteType? blobDeleteType) + internal HttpMessage CreateDeleteRequest(string snapshot, string versionId, int? timeout, string leaseId, DeleteSnapshotsOption? deleteSnapshots, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, BlobDeleteType? blobDeleteType, DateTimeOffset? accessTierIfModifiedSince, DateTimeOffset? accessTierIfUnmodifiedSince) { var message = _pipeline.CreateMessage(); var request = message.Request; @@ -489,6 +489,14 @@ internal HttpMessage CreateDeleteRequest(string snapshot, string versionId, int? request.Headers.Add("x-ms-if-tags", ifTags); } request.Headers.Add("x-ms-version", _version); + if (accessTierIfModifiedSince != null) + { + request.Headers.Add("x-ms-access-tier-if-modified-since", accessTierIfModifiedSince.Value, "R"); + } + if (accessTierIfUnmodifiedSince != null) + { + request.Headers.Add("x-ms-access-tier-if-unmodified-since", accessTierIfUnmodifiedSince.Value, "R"); + } request.Headers.Add("Accept", "application/xml"); return message; } @@ -505,10 +513,12 @@ internal HttpMessage CreateDeleteRequest(string snapshot, string versionId, int? /// Specify an ETag value to operate only on blobs without a matching value. /// Specify a SQL where clause on blob tags to operate only on blobs with a matching value. /// Optional. Only possible value is 'permanent', which specifies to permanently delete a blob if blob soft delete is enabled. + /// Specify this header value to operate only on a blob if the access-tier has been modified since the specified date/time. + /// Specify this header value to operate only on a blob if the access-tier has not been modified since the specified date/time. /// The cancellation token to use. - public async Task> DeleteAsync(string snapshot = null, string versionId = null, int? timeout = null, string leaseId = null, DeleteSnapshotsOption? deleteSnapshots = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, BlobDeleteType? blobDeleteType = null, CancellationToken cancellationToken = default) + public async Task> DeleteAsync(string snapshot = null, string versionId = null, int? timeout = null, string leaseId = null, DeleteSnapshotsOption? deleteSnapshots = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, BlobDeleteType? blobDeleteType = null, DateTimeOffset? accessTierIfModifiedSince = null, DateTimeOffset? accessTierIfUnmodifiedSince = null, CancellationToken cancellationToken = default) { - using var message = CreateDeleteRequest(snapshot, versionId, timeout, leaseId, deleteSnapshots, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, blobDeleteType); + using var message = CreateDeleteRequest(snapshot, versionId, timeout, leaseId, deleteSnapshots, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, blobDeleteType, accessTierIfModifiedSince, accessTierIfUnmodifiedSince); await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); var headers = new BlobDeleteHeaders(message.Response); switch (message.Response.Status) @@ -532,10 +542,12 @@ public async Task> DeleteAsync(string sna /// Specify an ETag value to operate only on blobs without a matching value. /// Specify a SQL where clause on blob tags to operate only on blobs with a matching value. /// Optional. Only possible value is 'permanent', which specifies to permanently delete a blob if blob soft delete is enabled. + /// Specify this header value to operate only on a blob if the access-tier has been modified since the specified date/time. + /// Specify this header value to operate only on a blob if the access-tier has not been modified since the specified date/time. /// The cancellation token to use. - public ResponseWithHeaders Delete(string snapshot = null, string versionId = null, int? timeout = null, string leaseId = null, DeleteSnapshotsOption? deleteSnapshots = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, BlobDeleteType? blobDeleteType = null, CancellationToken cancellationToken = default) + public ResponseWithHeaders Delete(string snapshot = null, string versionId = null, int? timeout = null, string leaseId = null, DeleteSnapshotsOption? deleteSnapshots = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, BlobDeleteType? blobDeleteType = null, DateTimeOffset? accessTierIfModifiedSince = null, DateTimeOffset? accessTierIfUnmodifiedSince = null, CancellationToken cancellationToken = default) { - using var message = CreateDeleteRequest(snapshot, versionId, timeout, leaseId, deleteSnapshots, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, blobDeleteType); + using var message = CreateDeleteRequest(snapshot, versionId, timeout, leaseId, deleteSnapshots, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, blobDeleteType, accessTierIfModifiedSince, accessTierIfUnmodifiedSince); _pipeline.Send(message, cancellationToken); var headers = new BlobDeleteHeaders(message.Response); switch (message.Response.Status) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlockBlobRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlockBlobRestClient.cs index af938b490fe5..f476cf71d554 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlockBlobRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlockBlobRestClient.cs @@ -30,7 +30,7 @@ internal partial class BlockBlobRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public BlockBlobRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { @@ -267,7 +267,7 @@ public ResponseWithHeaders Upload(long contentLength, St } } - internal HttpMessage CreatePutBlobFromUrlRequest(long contentLength, string copySource, int? timeout, byte[] transactionalContentMD5, string blobContentType, string blobContentEncoding, string blobContentLanguage, byte[] blobContentMD5, string blobCacheControl, IDictionary metadata, string leaseId, string blobContentDisposition, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, AccessTier? tier, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string sourceIfTags, byte[] sourceContentMD5, string blobTagsString, bool? copySourceBlobProperties, string copySourceAuthorization, BlobCopySourceTagsMode? copySourceTags, FileShareTokenIntent? fileRequestIntent) + internal HttpMessage CreatePutBlobFromUrlRequest(long contentLength, string copySource, int? timeout, byte[] transactionalContentMD5, string blobContentType, string blobContentEncoding, string blobContentLanguage, byte[] blobContentMD5, string blobCacheControl, IDictionary metadata, string leaseId, string blobContentDisposition, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, AccessTier? tier, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string sourceIfTags, byte[] sourceContentMD5, string blobTagsString, bool? copySourceBlobProperties, string copySourceAuthorization, BlobCopySourceTagsMode? copySourceTags, FileShareTokenIntent? fileRequestIntent, string sourceEncryptionKey, string sourceEncryptionKeySha256, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm) { var message = _pipeline.CreateMessage(); var request = message.Request; @@ -398,6 +398,18 @@ internal HttpMessage CreatePutBlobFromUrlRequest(long contentLength, string copy { request.Headers.Add("x-ms-file-request-intent", fileRequestIntent.Value.ToString()); } + if (sourceEncryptionKey != null) + { + request.Headers.Add("x-ms-source-encryption-key", sourceEncryptionKey); + } + if (sourceEncryptionKeySha256 != null) + { + request.Headers.Add("x-ms-source-encryption-key-sha256", sourceEncryptionKeySha256); + } + if (sourceEncryptionAlgorithm != null) + { + request.Headers.Add("x-ms-source-encryption-algorithm", sourceEncryptionAlgorithm.Value.ToSerialString()); + } request.Headers.Add("Accept", "application/xml"); return message; } @@ -436,16 +448,19 @@ internal HttpMessage CreatePutBlobFromUrlRequest(long contentLength, string copy /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Optional, default 'replace'. Indicates if source tags should be copied or replaced with the tags specified by x-ms-tags. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// is null. - public async Task> PutBlobFromUrlAsync(long contentLength, string copySource, int? timeout = null, byte[] transactionalContentMD5 = null, string blobContentType = null, string blobContentEncoding = null, string blobContentLanguage = null, byte[] blobContentMD5 = null, string blobCacheControl = null, IDictionary metadata = null, string leaseId = null, string blobContentDisposition = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, AccessTier? tier = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string sourceIfTags = null, byte[] sourceContentMD5 = null, string blobTagsString = null, bool? copySourceBlobProperties = null, string copySourceAuthorization = null, BlobCopySourceTagsMode? copySourceTags = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public async Task> PutBlobFromUrlAsync(long contentLength, string copySource, int? timeout = null, byte[] transactionalContentMD5 = null, string blobContentType = null, string blobContentEncoding = null, string blobContentLanguage = null, byte[] blobContentMD5 = null, string blobCacheControl = null, IDictionary metadata = null, string leaseId = null, string blobContentDisposition = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, AccessTier? tier = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string sourceIfTags = null, byte[] sourceContentMD5 = null, string blobTagsString = null, bool? copySourceBlobProperties = null, string copySourceAuthorization = null, BlobCopySourceTagsMode? copySourceTags = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (copySource == null) { throw new ArgumentNullException(nameof(copySource)); } - using var message = CreatePutBlobFromUrlRequest(contentLength, copySource, timeout, transactionalContentMD5, blobContentType, blobContentEncoding, blobContentLanguage, blobContentMD5, blobCacheControl, metadata, leaseId, blobContentDisposition, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, tier, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, sourceIfTags, sourceContentMD5, blobTagsString, copySourceBlobProperties, copySourceAuthorization, copySourceTags, fileRequestIntent); + using var message = CreatePutBlobFromUrlRequest(contentLength, copySource, timeout, transactionalContentMD5, blobContentType, blobContentEncoding, blobContentLanguage, blobContentMD5, blobCacheControl, metadata, leaseId, blobContentDisposition, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, tier, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, sourceIfTags, sourceContentMD5, blobTagsString, copySourceBlobProperties, copySourceAuthorization, copySourceTags, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); var headers = new BlockBlobPutBlobFromUrlHeaders(message.Response); switch (message.Response.Status) @@ -491,16 +506,19 @@ public async Task> PutBlobFr /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Optional, default 'replace'. Indicates if source tags should be copied or replaced with the tags specified by x-ms-tags. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// is null. - public ResponseWithHeaders PutBlobFromUrl(long contentLength, string copySource, int? timeout = null, byte[] transactionalContentMD5 = null, string blobContentType = null, string blobContentEncoding = null, string blobContentLanguage = null, byte[] blobContentMD5 = null, string blobCacheControl = null, IDictionary metadata = null, string leaseId = null, string blobContentDisposition = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, AccessTier? tier = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string sourceIfTags = null, byte[] sourceContentMD5 = null, string blobTagsString = null, bool? copySourceBlobProperties = null, string copySourceAuthorization = null, BlobCopySourceTagsMode? copySourceTags = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public ResponseWithHeaders PutBlobFromUrl(long contentLength, string copySource, int? timeout = null, byte[] transactionalContentMD5 = null, string blobContentType = null, string blobContentEncoding = null, string blobContentLanguage = null, byte[] blobContentMD5 = null, string blobCacheControl = null, IDictionary metadata = null, string leaseId = null, string blobContentDisposition = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, AccessTier? tier = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string sourceIfTags = null, byte[] sourceContentMD5 = null, string blobTagsString = null, bool? copySourceBlobProperties = null, string copySourceAuthorization = null, BlobCopySourceTagsMode? copySourceTags = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (copySource == null) { throw new ArgumentNullException(nameof(copySource)); } - using var message = CreatePutBlobFromUrlRequest(contentLength, copySource, timeout, transactionalContentMD5, blobContentType, blobContentEncoding, blobContentLanguage, blobContentMD5, blobCacheControl, metadata, leaseId, blobContentDisposition, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, tier, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, sourceIfTags, sourceContentMD5, blobTagsString, copySourceBlobProperties, copySourceAuthorization, copySourceTags, fileRequestIntent); + using var message = CreatePutBlobFromUrlRequest(contentLength, copySource, timeout, transactionalContentMD5, blobContentType, blobContentEncoding, blobContentLanguage, blobContentMD5, blobCacheControl, metadata, leaseId, blobContentDisposition, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, tier, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, sourceIfTags, sourceContentMD5, blobTagsString, copySourceBlobProperties, copySourceAuthorization, copySourceTags, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); _pipeline.Send(message, cancellationToken); var headers = new BlockBlobPutBlobFromUrlHeaders(message.Response); switch (message.Response.Status) @@ -648,7 +666,7 @@ public ResponseWithHeaders StageBlock(string blockId } } - internal HttpMessage CreateStageBlockFromURLRequest(string blockId, long contentLength, string sourceUrl, string sourceRange, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent) + internal HttpMessage CreateStageBlockFromURLRequest(string blockId, long contentLength, string sourceUrl, string sourceRange, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent, string sourceEncryptionKey, string sourceEncryptionKeySha256, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm) { var message = _pipeline.CreateMessage(); var request = message.Request; @@ -720,6 +738,18 @@ internal HttpMessage CreateStageBlockFromURLRequest(string blockId, long content { request.Headers.Add("x-ms-file-request-intent", fileRequestIntent.Value.ToString()); } + if (sourceEncryptionKey != null) + { + request.Headers.Add("x-ms-source-encryption-key", sourceEncryptionKey); + } + if (sourceEncryptionKeySha256 != null) + { + request.Headers.Add("x-ms-source-encryption-key-sha256", sourceEncryptionKeySha256); + } + if (sourceEncryptionAlgorithm != null) + { + request.Headers.Add("x-ms-source-encryption-algorithm", sourceEncryptionAlgorithm.Value.ToSerialString()); + } request.Headers.Add("Accept", "application/xml"); return message; } @@ -743,9 +773,12 @@ internal HttpMessage CreateStageBlockFromURLRequest(string blockId, long content /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// or is null. - public async Task> StageBlockFromURLAsync(string blockId, long contentLength, string sourceUrl, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public async Task> StageBlockFromURLAsync(string blockId, long contentLength, string sourceUrl, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (blockId == null) { @@ -756,7 +789,7 @@ public async Task> StageB throw new ArgumentNullException(nameof(sourceUrl)); } - using var message = CreateStageBlockFromURLRequest(blockId, contentLength, sourceUrl, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateStageBlockFromURLRequest(blockId, contentLength, sourceUrl, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); var headers = new BlockBlobStageBlockFromURLHeaders(message.Response); switch (message.Response.Status) @@ -787,9 +820,12 @@ public async Task> StageB /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// or is null. - public ResponseWithHeaders StageBlockFromURL(string blockId, long contentLength, string sourceUrl, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public ResponseWithHeaders StageBlockFromURL(string blockId, long contentLength, string sourceUrl, string sourceRange = null, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (blockId == null) { @@ -800,7 +836,7 @@ public ResponseWithHeaders StageBlockFromURL( throw new ArgumentNullException(nameof(sourceUrl)); } - using var message = CreateStageBlockFromURLRequest(blockId, contentLength, sourceUrl, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateStageBlockFromURLRequest(blockId, contentLength, sourceUrl, sourceRange, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); _pipeline.Send(message, cancellationToken); var headers = new BlockBlobStageBlockFromURLHeaders(message.Response); switch (message.Response.Status) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/ContainerRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/ContainerRestClient.cs index c70fa61447fc..f067266a5596 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/ContainerRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/ContainerRestClient.cs @@ -31,7 +31,7 @@ internal partial class ContainerRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public ContainerRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/BlobErrorCode.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/BlobErrorCode.cs index 57e03c5078ef..5624effcf0ea 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/BlobErrorCode.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/BlobErrorCode.cs @@ -84,7 +84,7 @@ public BlobErrorCode(string value) private const string CopyIdMismatchValue = "CopyIdMismatch"; private const string FeatureVersionMismatchValue = "FeatureVersionMismatch"; private const string IncrementalCopyBlobMismatchValue = "IncrementalCopyBlobMismatch"; - private const string IncrementalCopyOfEarlierVersionSnapshotNotAllowedValue = "IncrementalCopyOfEarlierVersionSnapshotNotAllowed"; + private const string IncrementalCopyOfEarlierSnapshotNotAllowedValue = "IncrementalCopyOfEarlierSnapshotNotAllowed"; private const string IncrementalCopySourceMustBeSnapshotValue = "IncrementalCopySourceMustBeSnapshot"; private const string InfiniteLeaseDurationRequiredValue = "InfiniteLeaseDurationRequired"; private const string InvalidBlobOrBlockValue = "InvalidBlobOrBlock"; @@ -261,8 +261,8 @@ public BlobErrorCode(string value) public static BlobErrorCode FeatureVersionMismatch { get; } = new BlobErrorCode(FeatureVersionMismatchValue); /// IncrementalCopyBlobMismatch. public static BlobErrorCode IncrementalCopyBlobMismatch { get; } = new BlobErrorCode(IncrementalCopyBlobMismatchValue); - /// IncrementalCopyOfEarlierVersionSnapshotNotAllowed. - public static BlobErrorCode IncrementalCopyOfEarlierVersionSnapshotNotAllowed { get; } = new BlobErrorCode(IncrementalCopyOfEarlierVersionSnapshotNotAllowedValue); + /// IncrementalCopyOfEarlierSnapshotNotAllowed. + public static BlobErrorCode IncrementalCopyOfEarlierSnapshotNotAllowed { get; } = new BlobErrorCode(IncrementalCopyOfEarlierSnapshotNotAllowedValue); /// IncrementalCopySourceMustBeSnapshot. public static BlobErrorCode IncrementalCopySourceMustBeSnapshot { get; } = new BlobErrorCode(IncrementalCopySourceMustBeSnapshotValue); /// InfiniteLeaseDurationRequired. diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.Serialization.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.Serialization.cs index 7ef1acceb83f..c0b1d1a6a24a 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.Serialization.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.Serialization.cs @@ -25,6 +25,12 @@ void IXmlSerializable.Write(XmlWriter writer, string nameHint) writer.WriteStartElement("Expiry"); writer.WriteValue(Expiry); writer.WriteEndElement(); + if (Common.Optional.IsDefined(DelegatedUserTid)) + { + writer.WriteStartElement("DelegatedUserTid"); + writer.WriteValue(DelegatedUserTid); + writer.WriteEndElement(); + } writer.WriteEndElement(); } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.cs index aff2b00c5c1c..e502c47a205d 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/KeyInfo.cs @@ -26,15 +26,19 @@ public KeyInfo(string expiry) /// Initializes a new instance of . /// The date-time the key is active in ISO 8601 UTC time. /// The date-time the key expires in ISO 8601 UTC time. - internal KeyInfo(string start, string expiry) + /// The delegated user tenant id in Azure AD. + internal KeyInfo(string start, string expiry, string delegatedUserTid) { Start = start; Expiry = expiry; + DelegatedUserTid = delegatedUserTid; } /// The date-time the key is active in ISO 8601 UTC time. public string Start { get; set; } /// The date-time the key expires in ISO 8601 UTC time. public string Expiry { get; } + /// The delegated user tenant id in Azure AD. + public string DelegatedUserTid { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/SkuName.Serialization.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/SkuName.Serialization.cs index 59e73caa20b8..482b4e1659b1 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/SkuName.Serialization.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/SkuName.Serialization.cs @@ -18,6 +18,9 @@ internal static partial class SkuNameExtensions SkuName.StandardRagrs => "Standard_RAGRS", SkuName.StandardZrs => "Standard_ZRS", SkuName.PremiumLrs => "Premium_LRS", + SkuName.StandardGzrs => "Standard_GZRS", + SkuName.PremiumZrs => "Premium_ZRS", + SkuName.StandardRagzrs => "Standard_RAGZRS", _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown SkuName value.") }; @@ -28,6 +31,9 @@ public static SkuName ToSkuName(this string value) if (StringComparer.OrdinalIgnoreCase.Equals(value, "Standard_RAGRS")) return SkuName.StandardRagrs; if (StringComparer.OrdinalIgnoreCase.Equals(value, "Standard_ZRS")) return SkuName.StandardZrs; if (StringComparer.OrdinalIgnoreCase.Equals(value, "Premium_LRS")) return SkuName.PremiumLrs; + if (StringComparer.OrdinalIgnoreCase.Equals(value, "Standard_GZRS")) return SkuName.StandardGzrs; + if (StringComparer.OrdinalIgnoreCase.Equals(value, "Premium_ZRS")) return SkuName.PremiumZrs; + if (StringComparer.OrdinalIgnoreCase.Equals(value, "Standard_RAGZRS")) return SkuName.StandardRagzrs; throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown SkuName value."); } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.Serialization.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.Serialization.cs index d21748961018..e4b780598a8f 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.Serialization.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.Serialization.cs @@ -21,6 +21,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) DateTimeOffset signedExpiresOn = default; string signedService = default; string signedVersion = default; + string signedDelegatedUserTenantId = default; string value = default; if (element.Element("SignedOid") is XElement signedOidElement) { @@ -46,6 +47,10 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) { signedVersion = (string)signedVersionElement; } + if (element.Element("SignedDelegatedUserTid") is XElement signedDelegatedUserTidElement) + { + signedDelegatedUserTenantId = (string)signedDelegatedUserTidElement; + } if (element.Element("Value") is XElement valueElement) { value = (string)valueElement; @@ -57,6 +62,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) signedExpiresOn, signedService, signedVersion, + signedDelegatedUserTenantId, value); } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.cs index e01e87bb2661..beee78f187fc 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/Models/UserDelegationKey.cs @@ -38,5 +38,26 @@ internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTim SignedVersion = signedVersion; Value = value; } + + /// Initializes a new instance of . + /// The Azure Active Directory object ID in GUID format. + /// The Azure Active Directory tenant ID in GUID format. + /// The date-time the key is active. + /// The date-time the key expires. + /// Abbreviation of the Azure Storage service that accepts the key. + /// The service version that created the key. + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// The key as a base64 string. + internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTimeOffset signedStartsOn, DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string signedDelegatedUserTenantId, string value) + { + SignedObjectId = signedObjectId; + SignedTenantId = signedTenantId; + SignedStartsOn = signedStartsOn; + SignedExpiresOn = signedExpiresOn; + SignedService = signedService; + SignedVersion = signedVersion; + SignedDelegatedUserTenantId = signedDelegatedUserTenantId; + Value = value; + } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/PageBlobRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/PageBlobRestClient.cs index 26024e2ad053..e63994f9614b 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/PageBlobRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/PageBlobRestClient.cs @@ -30,7 +30,7 @@ internal partial class PageBlobRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public PageBlobRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { @@ -554,7 +554,7 @@ public ResponseWithHeaders ClearPages(long contentLen } } - internal HttpMessage CreateUploadPagesFromURLRequest(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, long? ifSequenceNumberLessThanOrEqualTo, long? ifSequenceNumberLessThan, long? ifSequenceNumberEqualTo, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent) + internal HttpMessage CreateUploadPagesFromURLRequest(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5, byte[] sourceContentcrc64, int? timeout, string encryptionKey, string encryptionKeySha256, EncryptionAlgorithmTypeInternal? encryptionAlgorithm, string encryptionScope, string leaseId, long? ifSequenceNumberLessThanOrEqualTo, long? ifSequenceNumberLessThan, long? ifSequenceNumberEqualTo, DateTimeOffset? ifModifiedSince, DateTimeOffset? ifUnmodifiedSince, string ifMatch, string ifNoneMatch, string ifTags, DateTimeOffset? sourceIfModifiedSince, DateTimeOffset? sourceIfUnmodifiedSince, string sourceIfMatch, string sourceIfNoneMatch, string copySourceAuthorization, FileShareTokenIntent? fileRequestIntent, string sourceEncryptionKey, string sourceEncryptionKeySha256, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm) { var message = _pipeline.CreateMessage(); var request = message.Request; @@ -656,6 +656,18 @@ internal HttpMessage CreateUploadPagesFromURLRequest(string sourceUrl, string so { request.Headers.Add("x-ms-file-request-intent", fileRequestIntent.Value.ToString()); } + if (sourceEncryptionKey != null) + { + request.Headers.Add("x-ms-source-encryption-key", sourceEncryptionKey); + } + if (sourceEncryptionKeySha256 != null) + { + request.Headers.Add("x-ms-source-encryption-key-sha256", sourceEncryptionKeySha256); + } + if (sourceEncryptionAlgorithm != null) + { + request.Headers.Add("x-ms-source-encryption-algorithm", sourceEncryptionAlgorithm.Value.ToSerialString()); + } request.Headers.Add("Accept", "application/xml"); return message; } @@ -687,9 +699,12 @@ internal HttpMessage CreateUploadPagesFromURLRequest(string sourceUrl, string so /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// , or is null. - public async Task> UploadPagesFromURLAsync(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? ifSequenceNumberLessThanOrEqualTo = null, long? ifSequenceNumberLessThan = null, long? ifSequenceNumberEqualTo = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public async Task> UploadPagesFromURLAsync(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? ifSequenceNumberLessThanOrEqualTo = null, long? ifSequenceNumberLessThan = null, long? ifSequenceNumberEqualTo = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (sourceUrl == null) { @@ -704,7 +719,7 @@ public async Task> Upload throw new ArgumentNullException(nameof(range)); } - using var message = CreateUploadPagesFromURLRequest(sourceUrl, sourceRange, contentLength, range, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, ifSequenceNumberLessThanOrEqualTo, ifSequenceNumberLessThan, ifSequenceNumberEqualTo, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateUploadPagesFromURLRequest(sourceUrl, sourceRange, contentLength, range, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, ifSequenceNumberLessThanOrEqualTo, ifSequenceNumberLessThan, ifSequenceNumberEqualTo, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); var headers = new PageBlobUploadPagesFromURLHeaders(message.Response); switch (message.Response.Status) @@ -743,9 +758,12 @@ public async Task> Upload /// Specify an ETag value to operate only on blobs without a matching value. /// Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. /// Valid value is backup. + /// Optional. Specifies the source encryption key to use to encrypt the source data provided in the request. + /// The SHA-256 hash of the provided source encryption key. Must be provided if the x-ms-source-encryption-key header is provided. + /// The algorithm used to produce the source encryption key hash. Currently, the only accepted value is "AES256". Must be provided if the x-ms-source-encryption-key is provided. /// The cancellation token to use. /// , or is null. - public ResponseWithHeaders UploadPagesFromURL(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? ifSequenceNumberLessThanOrEqualTo = null, long? ifSequenceNumberLessThan = null, long? ifSequenceNumberEqualTo = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, CancellationToken cancellationToken = default) + public ResponseWithHeaders UploadPagesFromURL(string sourceUrl, string sourceRange, long contentLength, string range, byte[] sourceContentMD5 = null, byte[] sourceContentcrc64 = null, int? timeout = null, string encryptionKey = null, string encryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? encryptionAlgorithm = null, string encryptionScope = null, string leaseId = null, long? ifSequenceNumberLessThanOrEqualTo = null, long? ifSequenceNumberLessThan = null, long? ifSequenceNumberEqualTo = null, DateTimeOffset? ifModifiedSince = null, DateTimeOffset? ifUnmodifiedSince = null, string ifMatch = null, string ifNoneMatch = null, string ifTags = null, DateTimeOffset? sourceIfModifiedSince = null, DateTimeOffset? sourceIfUnmodifiedSince = null, string sourceIfMatch = null, string sourceIfNoneMatch = null, string copySourceAuthorization = null, FileShareTokenIntent? fileRequestIntent = null, string sourceEncryptionKey = null, string sourceEncryptionKeySha256 = null, EncryptionAlgorithmTypeInternal? sourceEncryptionAlgorithm = null, CancellationToken cancellationToken = default) { if (sourceUrl == null) { @@ -760,7 +778,7 @@ public ResponseWithHeaders UploadPagesFromURL throw new ArgumentNullException(nameof(range)); } - using var message = CreateUploadPagesFromURLRequest(sourceUrl, sourceRange, contentLength, range, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, ifSequenceNumberLessThanOrEqualTo, ifSequenceNumberLessThan, ifSequenceNumberEqualTo, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent); + using var message = CreateUploadPagesFromURLRequest(sourceUrl, sourceRange, contentLength, range, sourceContentMD5, sourceContentcrc64, timeout, encryptionKey, encryptionKeySha256, encryptionAlgorithm, encryptionScope, leaseId, ifSequenceNumberLessThanOrEqualTo, ifSequenceNumberLessThan, ifSequenceNumberEqualTo, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, ifTags, sourceIfModifiedSince, sourceIfUnmodifiedSince, sourceIfMatch, sourceIfNoneMatch, copySourceAuthorization, fileRequestIntent, sourceEncryptionKey, sourceEncryptionKeySha256, sourceEncryptionAlgorithm); _pipeline.Send(message, cancellationToken); var headers = new PageBlobUploadPagesFromURLHeaders(message.Response); switch (message.Response.Status) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/ServiceRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/ServiceRestClient.cs index 8936d67e02ca..b2569c050c42 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/ServiceRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/ServiceRestClient.cs @@ -31,7 +31,7 @@ internal partial class ServiceRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, container, or blob that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public ServiceRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/AppendBlobAppendBlockFromUriOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/AppendBlobAppendBlockFromUriOptions.cs index ae49b54d2a3e..4af098e2a5cd 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/AppendBlobAppendBlockFromUriOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/AppendBlobAppendBlockFromUriOptions.cs @@ -53,5 +53,11 @@ public class AppendBlobAppendBlockFromUriOptions /// Used to indicate the intent of the request. /// public FileShareTokenIntent? SourceShareTokenIntent { get; set; } + + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// Applicable only for service version 2026-02-06 or later. + /// + public CustomerProvidedKey? SourceCustomerProvidedKey { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadDetails.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadDetails.cs index bc119822cdc1..0490ec239798 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadDetails.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadDetails.cs @@ -34,6 +34,14 @@ public class BlobDownloadDetails public byte[] ContentHash { get; internal set; } #pragma warning restore CA1819 // Properties should not return arrays + /// + /// When requested using , this value contains the CRC for the download blob range. + /// This value may only become populated once the network stream is fully consumed. If this instance is accessed through + /// , the network stream has already been consumed. Otherwise, consume the content stream before + /// checking this value. + /// + public byte[] ContentCrc { get; internal set; } + /// /// Returns the date and time the container was last modified. Any operation that modifies the blob, including an update of the blob's metadata or properties, changes the last-modified time of the blob. /// diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadInfo.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadInfo.cs index e034573b54b3..b42801e36ab5 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadInfo.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadInfo.cs @@ -4,6 +4,8 @@ using System; using System.ComponentModel; using System.IO; +using System.Threading.Tasks; +using Azure.Core; using Azure.Storage.Shared; namespace Azure.Storage.Blobs.Models @@ -49,6 +51,14 @@ public class BlobDownloadInfo : IDisposable, IDownloadedContent /// public BlobDownloadDetails Details { get; internal set; } + /// + /// Indicates some contents of are mixed into the response stream. + /// They will not be set until has been fully consumed. These details + /// will be extracted from the content stream by the library before the calling code can + /// encounter them. + /// + public bool ExpectTrailingDetails { get; internal set; } + /// /// Constructor. /// diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadStreamingResult.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadStreamingResult.cs index 4fbada6e67aa..9b7d4d4e00da 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadStreamingResult.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobDownloadStreamingResult.cs @@ -24,6 +24,14 @@ internal BlobDownloadStreamingResult() { } /// public Stream Content { get; internal set; } + /// + /// Indicates some contents of are mixed into the response stream. + /// They will not be set until has been fully consumed. These details + /// will be extracted from the content stream by the library before the calling code can + /// encounter them. + /// + public bool ExpectTrailingDetails { get; internal set; } + /// /// Disposes the by calling Dispose on the underlying stream. /// diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobErrorCode.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobErrorCode.cs index a07fb0928741..1ca01301558a 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobErrorCode.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobErrorCode.cs @@ -14,6 +14,7 @@ public partial struct BlobErrorCode { private const string SnaphotOperationRateExceededValue = "SnaphotOperationRateExceeded"; private const string IncrementalCopyOfEralierVersionSnapshotNotAllowedValue = "IncrementalCopyOfEralierVersionSnapshotNotAllowed"; + private const string IncrementalCopyOfEarlierVersionSnapshotNotAllowedValue = "IncrementalCopyOfEarlierVersionSnapshotNotAllowed"; /// SnaphotOperationRateExceeded. [EditorBrowsable(EditorBrowsableState.Never)] @@ -23,6 +24,10 @@ public partial struct BlobErrorCode [EditorBrowsable(EditorBrowsableState.Never)] public static BlobErrorCode IncrementalCopyOfEralierVersionSnapshotNotAllowed { get; } = new BlobErrorCode(IncrementalCopyOfEralierVersionSnapshotNotAllowedValue); + /// IncrementalCopyOfEarlierVersionSnapshotNotAllowed. + [EditorBrowsable(EditorBrowsableState.Never)] + public static BlobErrorCode IncrementalCopyOfEarlierVersionSnapshotNotAllowed { get; } = new BlobErrorCode(IncrementalCopyOfEarlierVersionSnapshotNotAllowedValue); + /// Overloading equality for BlobErrorCode==string public static bool operator ==(BlobErrorCode code, string value) => code.Equals(value); diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobGetUserDelegationKeyOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobGetUserDelegationKeyOptions.cs new file mode 100644 index 000000000000..348588eb98ea --- /dev/null +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobGetUserDelegationKeyOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Storage.Blobs.Models +{ + /// + /// Parameters for Get User Delegation Key. + /// + public class BlobGetUserDelegationKeyOptions + { + /// + /// Constructor for BlobGetUserDelegationKeyOptions. + /// + public BlobGetUserDelegationKeyOptions(DateTimeOffset expiresOn) + { + ExpiresOn = expiresOn; + } + + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + public DateTimeOffset ExpiresOn { get; set; } + + /// + /// Optional. Start time for the key's validity, with null indicating an + /// immediate start. The time should be specified in UTC. + /// + /// Note: If you set the start time to the current time, failures + /// might occur intermittently for the first few minutes. This is due to different + /// machines having slightly different current times (known as clock skew). + /// + public DateTimeOffset? StartsOn { get; set; } + + /// + /// Optional. The delegated user tenant id in Azure AD. + /// + public string DelegatedUserTenantId { get; set; } + } +} diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobRequestConditions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobRequestConditions.cs index da2dd476b456..6ae8c048cc96 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobRequestConditions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobRequestConditions.cs @@ -20,6 +20,18 @@ public class BlobRequestConditions : BlobLeaseRequestConditions /// public string LeaseId { get; set; } + /// + /// Specify this header value to operate only on a blob if the access-tier has been modified since the specified date/time. + /// Note: If this is specified, AccessTierIfUnmodifiedSince cannot be specified. + /// + public DateTimeOffset? AccessTierIfModifiedSince { get; set; } + + /// + /// Specify this header value to operate only on a blob if the access-tier has not been modified since the specified date/time. + /// Note: If this is specified, AccessTierIfModifiedSince cannot be specified. + /// + public DateTimeOffset? AccessTierIfUnmodifiedSince { get; set; } + /// /// Default constructor. /// @@ -31,6 +43,8 @@ private BlobRequestConditions(BlobRequestConditions deepCopySource) : base(deepC { Argument.AssertNotNull(deepCopySource, nameof(deepCopySource)); LeaseId = deepCopySource.LeaseId; + AccessTierIfModifiedSince = deepCopySource.AccessTierIfModifiedSince; + AccessTierIfUnmodifiedSince = deepCopySource.AccessTierIfUnmodifiedSince; } /// @@ -112,6 +126,16 @@ internal virtual void AddConditions(StringBuilder conditions) { conditions.Append(nameof(TagConditions)).Append('=').Append(TagConditions).Append(';'); } + + if (AccessTierIfModifiedSince != null) + { + conditions.Append(nameof(AccessTierIfModifiedSince)).Append('=').Append(AccessTierIfModifiedSince).Append(';'); + } + + if (AccessTierIfUnmodifiedSince != null) + { + conditions.Append(nameof(AccessTierIfUnmodifiedSince)).Append('=').Append(AccessTierIfUnmodifiedSince).Append(';'); + } } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobSyncUploadFromUriOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobSyncUploadFromUriOptions.cs index dca2ddd9f3a5..1c61d8639c90 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobSyncUploadFromUriOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobSyncUploadFromUriOptions.cs @@ -81,5 +81,11 @@ public class BlobSyncUploadFromUriOptions /// Used to indicate the intent of the request. /// public FileShareTokenIntent? SourceShareTokenIntent { get; set; } + + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// Applicable only for service version 2026-02-06 or later. + /// + public CustomerProvidedKey? SourceCustomerProvidedKey { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobsModelFactory.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobsModelFactory.cs index b1d2ba307da2..67bfa07df62b 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobsModelFactory.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobsModelFactory.cs @@ -18,49 +18,6 @@ namespace Azure.Storage.Blobs.Models public static partial class BlobsModelFactory { #region BlobContentInfo - /// Initializes a new instance of . - /// The Azure Active Directory object ID in GUID format. - /// The Azure Active Directory tenant ID in GUID format. - /// The date-time the key is active. - /// The date-time the key expires. - /// Abbreviation of the Azure Storage service that accepts the key. - /// The service version that created the key. - /// The key as a base64 string. - /// , , , or is null. - /// A new instance for mocking. - public static UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, DateTimeOffset signedStartsOn = default, DateTimeOffset signedExpiresOn = default, string signedService = null, string signedVersion = null, string value = null) - { - if (signedObjectId == null) - { - throw new ArgumentNullException(nameof(signedObjectId)); - } - if (signedTenantId == null) - { - throw new ArgumentNullException(nameof(signedTenantId)); - } - if (signedService == null) - { - throw new ArgumentNullException(nameof(signedService)); - } - if (signedVersion == null) - { - throw new ArgumentNullException(nameof(signedVersion)); - } - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return new UserDelegationKey( - signedObjectId, - signedTenantId, - signedStartsOn, - signedExpiresOn, - signedService, - signedVersion, - value); - } - /// /// Creates a new BlobContentInfo instance for mocking. /// @@ -2020,6 +1977,31 @@ public static BlockList BlockList( /// /// Creates a new UserDelegationKey instance for mocking. /// + public static UserDelegationKey UserDelegationKey( + string signedObjectId = default, + string signedTenantId = default, + string signedService = default, + string signedVersion = default, + string value = default, + DateTimeOffset signedExpiresOn = default, + DateTimeOffset signedStartsOn = default, + string DelegatedUserObjectId = default) + { + return new UserDelegationKey( + signedObjectId, + signedTenantId, + signedStartsOn, + signedExpiresOn, + signedService, + signedVersion, + DelegatedUserObjectId, + value); + } + + /// + /// Creates a new UserDelegationKey instance for mocking. + /// + [EditorBrowsable(EditorBrowsableState.Never)] public static UserDelegationKey UserDelegationKey( string signedObjectId, string signedTenantId, @@ -2036,6 +2018,76 @@ public static UserDelegationKey UserDelegationKey( signedExpiresOn, signedService, signedVersion, + default, + value); + } + + /// Initializes a new instance of . + /// The Azure Active Directory object ID in GUID format. + /// The Azure Active Directory tenant ID in GUID format. + /// The date-time the key is active. + /// The date-time the key expires. + /// Abbreviation of the Azure Storage service that accepts the key. + /// The service version that created the key. + /// The delegated user tenant id in Azure AD. + /// The key as a base64 string. + /// , , , or is null. + /// A new instance for mocking. + public static UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, DateTimeOffset signedStartsOn = default, DateTimeOffset signedExpiresOn = default, string signedService = null, string signedVersion = null, string delegatedUserObjectId = null, string value = null) + { + return new UserDelegationKey( + signedObjectId, + signedTenantId, + signedStartsOn, + signedExpiresOn, + signedService, + signedVersion, + delegatedUserObjectId, + value); + } + + /// Initializes a new instance of . + /// The Azure Active Directory object ID in GUID format. + /// The Azure Active Directory tenant ID in GUID format. + /// The date-time the key is active. + /// The date-time the key expires. + /// Abbreviation of the Azure Storage service that accepts the key. + /// The service version that created the key. + /// The key as a base64 string. + /// , , , or is null. + /// A new instance for mocking. + [EditorBrowsable(EditorBrowsableState.Never)] + public static UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, DateTimeOffset signedStartsOn, DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) + { + if (signedObjectId == null) + { + throw new ArgumentNullException(nameof(signedObjectId)); + } + if (signedTenantId == null) + { + throw new ArgumentNullException(nameof(signedTenantId)); + } + if (signedService == null) + { + throw new ArgumentNullException(nameof(signedService)); + } + if (signedVersion == null) + { + throw new ArgumentNullException(nameof(signedVersion)); + } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return new UserDelegationKey( + signedObjectId, + signedTenantId, + signedStartsOn, + signedExpiresOn, + signedService, + signedVersion, + default, value); } #endregion diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/Internal/BlobRequestConditionProperty.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/Internal/BlobRequestConditionProperty.cs index 3044e1a15b0d..48f56c84eb6b 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/Internal/BlobRequestConditionProperty.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/Internal/BlobRequestConditionProperty.cs @@ -19,6 +19,8 @@ internal enum BlobRequestConditionProperty IfMaxSizeLessThanOrEqual = 128, IfSequenceNumberLessThan = 256, IfSequenceNumberLessThanOrEqual = 512, - IfSequenceNumberEqual = 1024 + IfSequenceNumberEqual = 1024, + AccessTierIfModifiedSince = 2048, + AccessTierIfUnmodifiedSince = 4096 } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/PageBlobUploadPagesFromUriOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/PageBlobUploadPagesFromUriOptions.cs index 41cb65bcebfd..c969875acd7f 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/PageBlobUploadPagesFromUriOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/PageBlobUploadPagesFromUriOptions.cs @@ -44,5 +44,11 @@ public class PageBlobUploadPagesFromUriOptions /// Used to indicate the intent of the request. /// public FileShareTokenIntent? SourceShareTokenIntent { get; set; } + + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// Applicable only for service version 2026-02-06 or later. + /// + public CustomerProvidedKey? SourceCustomerProvidedKey { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/SkuName.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/SkuName.cs index 037e918893d7..9c0e49a69030 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/SkuName.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/SkuName.cs @@ -37,6 +37,24 @@ public enum SkuName /// Premium_LRS /// [CodeGenMember("PremiumLRS")] - PremiumLrs + PremiumLrs, + + /// + /// Standard_GZRS + /// + [CodeGenMember("StandardGZRS")] + StandardGzrs, + + /// + /// Premium_ZRS + /// + [CodeGenMember("PremiumZRS")] + PremiumZrs, + + /// + /// Standard_RAGZRS + /// + [CodeGenMember("StandardRAGZRS")] + StandardRagzrs, } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/StageBlockFromUriOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/StageBlockFromUriOptions.cs index 69eb439f208c..c54bd9d983e1 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/StageBlockFromUriOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/StageBlockFromUriOptions.cs @@ -52,5 +52,11 @@ public class StageBlockFromUriOptions /// Used to indicate the intent of the request. /// public FileShareTokenIntent? SourceShareTokenIntent { get; set; } + + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// Applicable only for service version 2026-02-06 or later. + /// + public CustomerProvidedKey? SourceCustomerProvidedKey { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/UserDelegationKey.cs index 04baed8e105d..5ed7431850d6 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/UserDelegationKey.cs @@ -45,6 +45,12 @@ public partial class UserDelegationKey /// public string SignedVersion { get; internal set; } + /// + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// + [CodeGenMember("SignedDelegatedUserTid")] + public string SignedDelegatedUserTenantId { get; internal set; } + /// /// The key as a base64 string. /// diff --git a/sdk/storage/Azure.Storage.Blobs/src/PageBlobClient.cs b/sdk/storage/Azure.Storage.Blobs/src/PageBlobClient.cs index 0973cce9c533..bae8246bb558 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/PageBlobClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/PageBlobClient.cs @@ -1004,7 +1004,9 @@ private async Task> CreateInternal( invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.Create), parameterName: nameof(conditions)); @@ -1401,9 +1403,10 @@ internal async Task> UploadPagesInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(PageBlobClient)}.{nameof(UploadPages)}"); - // All PageBlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.UploadPages), parameterName: nameof(conditions)); @@ -1412,15 +1415,42 @@ internal async Task> UploadPagesInternal( scope.Start(); Errors.VerifyStreamPosition(content, nameof(content)); - // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); - - content = content?.WithNoDispose().WithProgress(progressHandler); - HttpRange range = new HttpRange(offset, (content?.Length - content?.Position) ?? null); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + HttpRange range; + if (validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && + ClientSideEncryption == null) // don't allow feature combination + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + contentLength = (content?.Length - content?.Position) ?? 0; + range = new HttpRange(offset, (content?.Length - content?.Position) ?? null); + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content?.WithNoDispose().WithProgress(progressHandler); + content = validationOptions.PrecalculatedChecksum.IsEmpty + ? new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64) + : new StructuredMessagePrecalculatedCrcWrapperStream( + content, + validationOptions.PrecalculatedChecksum.Span); + contentLength = (content?.Length - content?.Position) ?? 0; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content?.WithNoDispose().WithProgress(progressHandler); + range = new HttpRange(offset, (content?.Length - content?.Position) ?? null); + } ResponseWithHeaders response; @@ -1437,6 +1467,8 @@ internal async Task> UploadPagesInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, ifSequenceNumberLessThanOrEqualTo: conditions?.IfSequenceNumberLessThanOrEqual, ifSequenceNumberLessThan: conditions?.IfSequenceNumberLessThan, ifSequenceNumberEqualTo: conditions?.IfSequenceNumberEqual, @@ -1461,6 +1493,8 @@ internal async Task> UploadPagesInternal( encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, encryptionScope: ClientConfiguration.EncryptionScope, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, ifSequenceNumberLessThanOrEqualTo: conditions?.IfSequenceNumberLessThanOrEqual, ifSequenceNumberLessThan: conditions?.IfSequenceNumberLessThan, ifSequenceNumberEqualTo: conditions?.IfSequenceNumberEqual, @@ -1638,9 +1672,10 @@ private async Task> ClearPagesInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(PageBlobClient)}.{nameof(ClearPages)}"); - // All PageBlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.ClearPages), parameterName: nameof(conditions)); @@ -1866,7 +1901,9 @@ internal async Task> invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.GetPageRanges), parameterName: nameof(conditions)); @@ -2090,7 +2127,9 @@ private async Task> GetPageRangesInternal( invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.GetPageRanges), parameterName: nameof(conditions)); @@ -2337,7 +2376,9 @@ internal async Task> GetPageRangesDiffInternal( invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.GetPageRangesDiff), parameterName: nameof(conditions)); @@ -2960,7 +3003,9 @@ private async Task> ResizeInternal( invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.Resize), parameterName: nameof(conditions)); @@ -3217,7 +3262,9 @@ private async Task> UpdateSequenceNumberInternal( invalidConditions: BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.UpdateSequenceNumber), parameterName: nameof(conditions)); @@ -3615,7 +3662,9 @@ private async Task> StartCopyIncrementalInternal( BlobRequestConditionProperty.LeaseId | BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan - | BlobRequestConditionProperty.IfSequenceNumberEqual, + | BlobRequestConditionProperty.IfSequenceNumberEqual + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.StartCopyIncremental), parameterName: nameof(conditions)); @@ -3734,6 +3783,7 @@ public virtual Response UploadPagesFromUri( options?.SourceConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: false, cancellationToken) .EnsureCompleted(); @@ -3798,6 +3848,7 @@ await UploadPagesFromUriInternal( options?.SourceConditions, options?.SourceAuthentication, options?.SourceShareTokenIntent, + options?.SourceCustomerProvidedKey, async: true, cancellationToken) .ConfigureAwait(false); @@ -3882,6 +3933,7 @@ public virtual Response UploadPagesFromUri( sourceConditions, sourceAuthentication: default, sourceTokenIntent: default, + sourceCustomerProvidedKey: default, async: false, cancellationToken) .EnsureCompleted(); @@ -3966,6 +4018,7 @@ await UploadPagesFromUriInternal( sourceConditions, sourceAuthentication: default, sourceTokenIntent: default, + sourceCustomerProvidedKey: default, async: true, cancellationToken) .ConfigureAwait(false); @@ -4023,6 +4076,9 @@ await UploadPagesFromUriInternal( /// Optional, only applicable (but required) when the source is Azure Storage Files and using token authentication. /// Used to indicate the intent of the request. /// + /// + /// Optional. Specifies the source customer provided key to use to encrypt the source blob. + /// /// /// Whether to invoke the operation asynchronously. /// @@ -4049,6 +4105,7 @@ private async Task> UploadPagesFromUriInternal( PageBlobRequestConditions sourceConditions, HttpAuthorization sourceAuthentication, FileShareTokenIntent? sourceTokenIntent, + CustomerProvidedKey? sourceCustomerProvidedKey, bool async, CancellationToken cancellationToken) { @@ -4062,9 +4119,10 @@ private async Task> UploadPagesFromUriInternal( DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(PageBlobClient)}.{nameof(UploadPagesFromUri)}"); - // All destination PageBlobRequestConditions are valid. conditions.ValidateConditionsNotPresent( - invalidConditions: BlobRequestConditionProperty.None, + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.UploadPagesFromUri), parameterName: nameof(conditions)); @@ -4074,7 +4132,9 @@ private async Task> UploadPagesFromUriInternal( | BlobRequestConditionProperty.IfSequenceNumberLessThanOrEqual | BlobRequestConditionProperty.IfSequenceNumberLessThan | BlobRequestConditionProperty.IfSequenceNumberEqual - | BlobRequestConditionProperty.TagConditions, + | BlobRequestConditionProperty.TagConditions + | BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, operationName: nameof(PageBlobClient.UploadPagesFromUri), parameterName: nameof(sourceConditions)); @@ -4110,6 +4170,9 @@ private async Task> UploadPagesFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -4140,6 +4203,9 @@ private async Task> UploadPagesFromUriInternal( sourceIfNoneMatch: sourceConditions?.IfNoneMatch?.ToString(), copySourceAuthorization: sourceAuthentication?.ToString(), fileRequestIntent: sourceTokenIntent, + sourceEncryptionKey: sourceCustomerProvidedKey?.EncryptionKey, + sourceEncryptionKeySha256: sourceCustomerProvidedKey?.EncryptionKeyHash, + sourceEncryptionAlgorithm: sourceCustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, cancellationToken: cancellationToken); } diff --git a/sdk/storage/Azure.Storage.Blobs/src/PartitionedDownloader.cs b/sdk/storage/Azure.Storage.Blobs/src/PartitionedDownloader.cs index d8b3963201c3..b873ee24c1e9 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/PartitionedDownloader.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/PartitionedDownloader.cs @@ -23,6 +23,8 @@ internal class PartitionedDownloader private const string _operationName = nameof(BlobBaseClient) + "." + nameof(BlobBaseClient.DownloadTo); private const string _innerOperationName = nameof(BlobBaseClient) + "." + nameof(BlobBaseClient.DownloadStreaming); + private const int Crc64Len = Constants.StorageCrc64SizeInBytes; + /// /// The client used to download the blob. /// @@ -49,6 +51,7 @@ internal class PartitionedDownloader /// private readonly StorageChecksumAlgorithm _validationAlgorithm; private readonly int _checksumSize; + // TODO disabling master crc temporarily. segment CRCs still handled. private bool UseMasterCrc => _validationAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64; private StorageCrc64HashAlgorithm _masterCrcCalculator = null; @@ -68,6 +71,8 @@ private DownloadTransferValidationOptions ValidationOptions private readonly ArrayPool _arrayPool; + private readonly int DefaultConcurrentTransfersCount = Math.Min(Math.Max(Environment.ProcessorCount * 2, 8), 32); + public PartitionedDownloader( BlobBaseClient client, StorageTransferOptions transferOptions = default, @@ -86,7 +91,9 @@ public PartitionedDownloader( } else { - _maxWorkerCount = Constants.Blob.Block.DefaultConcurrentTransfersCount; + _maxWorkerCount = CompatSwitches.UseLegacyDefaultConcurrency + ? Constants.Blob.Block.LegacyDefaultConcurrentTransfersCount + : DefaultConcurrentTransfersCount; } // Set _rangeSize @@ -146,6 +153,14 @@ public async Task DownloadToInternal( DiagnosticScope scope = _client.ClientConfiguration.ClientDiagnostics.CreateScope(_operationName); using DisposableBucket disposables = new DisposableBucket(); Queue>> runningTasks = null; + + conditions.ValidateConditionsNotPresent( + invalidConditions: + BlobRequestConditionProperty.AccessTierIfModifiedSince + | BlobRequestConditionProperty.AccessTierIfUnmodifiedSince, + operationName: nameof(BlobBaseClient.DownloadTo), + parameterName: nameof(conditions)); + try { scope.Start(); @@ -220,20 +235,31 @@ public async Task DownloadToInternal( } // Destination wrapped in master crc step if needed (must wait until after encryption wrap check) - Memory composedCrc = default; + byte[] composedCrcBuf = default; if (UseMasterCrc) { _masterCrcCalculator = StorageCrc64HashAlgorithm.Create(); destination = ChecksumCalculatingStream.GetWriteStream(destination, _masterCrcCalculator.Append); - disposables.Add(_arrayPool.RentAsMemoryDisposable( - Constants.StorageCrc64SizeInBytes, out composedCrc)); - composedCrc.Span.Clear(); + disposables.Add(_arrayPool.RentDisposable(Crc64Len, out composedCrcBuf)); + composedCrcBuf.Clear(); } // If the first segment was the entire blob, we'll copy that to // the output stream and finish now - long initialLength = initialResponse.Value.Details.ContentLength; - long totalLength = ParseRangeTotalLength(initialResponse.Value.Details.ContentRange); + long initialLength; + long totalLength; + // Get blob content length downloaded from content range when available to handle transit encoding + if (string.IsNullOrWhiteSpace(initialResponse.Value.Details.ContentRange)) + { + initialLength = initialResponse.Value.Details.ContentLength; + totalLength = 0; + } + else + { + ContentRange recievedRange = ContentRange.Parse(initialResponse.Value.Details.ContentRange); + initialLength = recievedRange.GetRangeLength(); + totalLength = recievedRange.TotalResourceLength.Value; + } if (initialLength == totalLength) { await HandleOneShotDownload(initialResponse, destination, async, cancellationToken) @@ -258,15 +284,16 @@ await HandleOneShotDownload(initialResponse, destination, async, cancellationTok } else { - using (_arrayPool.RentAsMemoryDisposable(_checksumSize, out Memory partitionChecksum)) + using (_arrayPool.RentDisposable(_checksumSize, out byte[] partitionChecksum)) { - await CopyToInternal(initialResponse, destination, partitionChecksum, async, cancellationToken).ConfigureAwait(false); + await CopyToInternal(initialResponse, destination, new(partitionChecksum, 0, _checksumSize), async, cancellationToken).ConfigureAwait(false); if (UseMasterCrc) { StorageCrc64Composer.Compose( - (composedCrc.ToArray(), 0L), - (partitionChecksum.ToArray(), initialResponse.Value.Details.ContentLength) - ).CopyTo(composedCrc); + (composedCrcBuf, 0L), + (partitionChecksum, initialResponse.Value.Details.ContentRange.GetContentRangeLengthOrDefault() + ?? initialResponse.Value.Details.ContentLength) + ).AsSpan(0, Crc64Len).CopyTo(composedCrcBuf); } } } @@ -305,15 +332,16 @@ await HandleOneShotDownload(initialResponse, destination, async, cancellationTok else { Response result = await responseValueTask.ConfigureAwait(false); - using (_arrayPool.RentAsMemoryDisposable(_checksumSize, out Memory partitionChecksum)) + using (_arrayPool.RentDisposable(_checksumSize, out byte[] partitionChecksum)) { - await CopyToInternal(result, destination, partitionChecksum, async, cancellationToken).ConfigureAwait(false); + await CopyToInternal(result, destination, new(partitionChecksum, 0, _checksumSize), async, cancellationToken).ConfigureAwait(false); if (UseMasterCrc) { StorageCrc64Composer.Compose( - (composedCrc.ToArray(), 0L), - (partitionChecksum.ToArray(), result.Value.Details.ContentLength) - ).CopyTo(composedCrc); + (composedCrcBuf, 0L), + (partitionChecksum, result.Value.Details.ContentRange.GetContentRangeLengthOrDefault() + ?? result.Value.Details.ContentLength) + ).AsSpan(0, Crc64Len).CopyTo(composedCrcBuf); } } } @@ -329,7 +357,7 @@ await HandleOneShotDownload(initialResponse, destination, async, cancellationTok } #pragma warning restore AZC0110 // DO NOT use await keyword in possibly synchronous scope. - await FinalizeDownloadInternal(destination, composedCrc, async, cancellationToken) + await FinalizeDownloadInternal(destination, composedCrcBuf?.AsMemory(0, Crc64Len) ?? default, async, cancellationToken) .ConfigureAwait(false); return initialResponse.GetRawResponse(); @@ -356,13 +384,14 @@ await CopyToInternal( async, cancellationToken) .ConfigureAwait(false); - if (UseMasterCrc) - { - StorageCrc64Composer.Compose( - (composedCrc.ToArray(), 0L), - (partitionChecksum.ToArray(), response.Value.Details.ContentLength) - ).CopyTo(composedCrc); - } + if (UseMasterCrc) + { + StorageCrc64Composer.Compose( + (composedCrcBuf, 0L), + (partitionChecksum, response.Value.Details.ContentRange.GetContentRangeLengthOrDefault() + ?? response.Value.Details.ContentLength) + ).AsSpan(0, Crc64Len).CopyTo(composedCrcBuf); + } } } } @@ -409,7 +438,7 @@ await FinalizeDownloadInternal(destination, partitionChecksum, async, cancellati private async Task FinalizeDownloadInternal( Stream destination, - Memory composedCrc, + ReadOnlyMemory composedCrc, bool async, CancellationToken cancellationToken) { @@ -425,20 +454,6 @@ private async Task FinalizeDownloadInternal( } } - private static long ParseRangeTotalLength(string range) - { - if (range == null) - { - return 0; - } - int lengthSeparator = range.IndexOf("/", StringComparison.InvariantCultureIgnoreCase); - if (lengthSeparator == -1) - { - throw BlobErrors.ParsingFullHttpRangeFailed(range); - } - return long.Parse(range.Substring(lengthSeparator + 1), CultureInfo.InvariantCulture); - } - private async Task CopyToInternal( Response response, Stream destination, @@ -447,7 +462,9 @@ private async Task CopyToInternal( CancellationToken cancellationToken) { CancellationHelper.ThrowIfCancellationRequested(cancellationToken); - using IHasher hasher = ContentHasher.GetHasherFromAlgorithmId(_validationAlgorithm); + // if structured message, this crc is validated in the decoding process. don't decode it here. + bool structuredMessage = response.GetRawResponse().Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader); + using IHasher hasher = structuredMessage ? null : ContentHasher.GetHasherFromAlgorithmId(_validationAlgorithm); using Stream rawSource = response.Value.Content; using Stream source = hasher != null ? ChecksumCalculatingStream.GetReadStream(rawSource, hasher.AppendHash) @@ -459,7 +476,13 @@ await source.CopyToInternal( cancellationToken) .ConfigureAwait(false); - if (hasher != null) + // with structured message, the message integrity will already be validated, + // but we can still get the checksum out of the response object + if (structuredMessage) + { + response.Value.Details.ContentCrc?.CopyTo(checksumBuffer.Span); + } + else if (hasher != null) { hasher.GetFinalHash(checksumBuffer.Span); (ReadOnlyMemory checksum, StorageChecksumAlgorithm _) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs index 9c36d485a7ed..2ef9f69c39f2 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Security.Cryptography; using System.Text; +using System.Threading; using Azure.Core; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; @@ -189,6 +190,18 @@ public class BlobSasBuilder /// public string DelegatedUserObjectId { get; set; } + /// + /// Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + /// include these headers and values in the request. + /// + public Dictionary RequestHeaders { get; set; } + + /// + /// Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS must + /// include these query parameters and values in the request. + /// + public Dictionary RequestQueryParameters { get; set; } + /// /// Initializes a new instance of the /// class. @@ -428,7 +441,7 @@ private string ToStringToSign(StorageSharedKeyCredential sharedKeyCredential) /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -445,7 +458,7 @@ public BlobSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelegat /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -491,7 +504,10 @@ public BlobSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelegat authorizedAadObjectId: PreauthorizedAgentObjectId, correlationId: CorrelationId, encryptionScope: EncryptionScope, - delegatedUserObjectId: DelegatedUserObjectId); + delegatedUserObjectId: DelegatedUserObjectId, + keyDelegatedUserTenantId: userDelegationKey.SignedDelegatedUserTenantId, + requestHeaders: SasExtensions.ConvertRequestDictToKeyList(RequestHeaders), + requestQueryParameters: SasExtensions.ConvertRequestDictToKeyList(RequestQueryParameters)); return p; } @@ -501,6 +517,8 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun string expiryTime = SasExtensions.FormatTimesForSasSigning(ExpiresOn); string signedStart = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedStartsOn); string signedExpiry = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedExpiresOn); + string canonicalizedSignedRequestHeaders = SasExtensions.FormatRequestHeadersForSasSigning(RequestHeaders); + string canonicalizedSignedRequestQueryParameters = SasExtensions.FormatRequestQueryParametersForSasSigning(RequestQueryParameters); // See http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx return string.Join("\n", @@ -517,7 +535,7 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun PreauthorizedAgentObjectId, null, // AgentObjectId - enabled only in HNS accounts CorrelationId, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + userDelegationKey.SignedDelegatedUserTenantId, DelegatedUserObjectId, IPRange.ToString(), SasExtensions.ToProtocolString(Protocol), @@ -525,6 +543,8 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun Resource, Snapshot ?? BlobVersionId, EncryptionScope, + canonicalizedSignedRequestHeaders, + canonicalizedSignedRequestQueryParameters, CacheControl, ContentDisposition, ContentEncoding, diff --git a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasQueryParameters.cs b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasQueryParameters.cs index 28ae712107b0..0efece092bab 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasQueryParameters.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasQueryParameters.cs @@ -51,6 +51,11 @@ public sealed class BlobSasQueryParameters : SasQueryParameters /// public string KeyVersion => KeyProperties?.Version; + /// + /// Gets the delegated user tenant id. + /// + public string KeyDelegatedUserTenantId => KeyProperties?.DelegatedUserTenantId; + /// /// Gets empty shared access signature query parameters. /// @@ -64,7 +69,7 @@ internal BlobSasQueryParameters() /// /// Creates a new BlobSasQueryParameters instance. /// - internal BlobSasQueryParameters ( + internal BlobSasQueryParameters( string version, AccountSasServices? services, AccountSasResourceTypes? resourceTypes, @@ -91,7 +96,10 @@ internal BlobSasQueryParameters ( string unauthorizedAadObjectId = default, string correlationId = default, string encryptionScope = default, - string delegatedUserObjectId = default) + string delegatedUserObjectId = default, + string keyDelegatedUserTenantId = default, + List requestHeaders = default, + List requestQueryParameters = default) : base( version, services, @@ -114,7 +122,9 @@ internal BlobSasQueryParameters ( correlationId, directoryDepth: null, encryptionScope, - delegatedUserObjectId) + delegatedUserObjectId, + requestHeaders, + requestQueryParameters) { KeyProperties = new UserDelegationKeyProperties { @@ -123,7 +133,8 @@ internal BlobSasQueryParameters ( StartsOn = keyStart, ExpiresOn = keyExpiry, Service = keyService, - Version = keyVersion + Version = keyVersion, + DelegatedUserTenantId = keyDelegatedUserTenantId }; } @@ -134,7 +145,7 @@ internal BlobSasQueryParameters ( /// . /// /// URI query parameters - internal BlobSasQueryParameters ( + internal BlobSasQueryParameters( IDictionary values) : base(values) { diff --git a/sdk/storage/Azure.Storage.Blobs/src/autorest.md b/sdk/storage/Azure.Storage.Blobs/src/autorest.md index 68802c82f330..81f2c6355209 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/autorest.md +++ b/sdk/storage/Azure.Storage.Blobs/src/autorest.md @@ -4,7 +4,7 @@ Run `dotnet build /t:GenerateCode` to generate code. ``` yaml input-file: - - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/b6472ffd34d5d4a155101b41b4eb1f356abff600/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-02-06/blob.json + - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/42b84487c693d3f30939b81ded12b26e931a619b/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-04-06/blob.json generation1-convenience-client: true # https://github.com/Azure/autorest/issues/4075 skip-semantics-validation: true @@ -34,7 +34,7 @@ directive: if (property.includes('/{containerName}/{blob}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/ContainerName") && false == param['$ref'].endsWith("#/parameters/Blob"))}); - } + } else if (property.includes('/{containerName}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/ContainerName"))}); @@ -135,6 +135,17 @@ directive: delete $.EncryptionScope["x-ms-parameter-grouping"]; ``` +### Remove source CPK parameter grouping +``` yaml +directive: +- from: swagger-document + where: $.parameters + transform: > + delete $.SourceEncryptionKey["x-ms-parameter-grouping"]; + delete $.SourceEncryptionKeySha256["x-ms-parameter-grouping"]; + delete $.SourceEncryptionAlgorithm["x-ms-parameter-grouping"]; +``` + ### Fix 304s ``` yaml directive: @@ -162,7 +173,7 @@ directive: var newName = property.replace('/{containerName}/{blob}', ''); $[newName] = $[oldName]; delete $[oldName]; - } + } else if (property.includes('/{containerName}')) { var oldName = property; diff --git a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs index 068fccd6fe0e..27bba516c486 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs @@ -579,9 +579,9 @@ public async Task CreateAsync_EncryptionScopeIdentitySAS() BlobServiceClient oauthService = BlobsClientBuilder.GetServiceClient_OAuth(TestEnvironment.Credential); await using DisposingContainer test = await GetTestContainerAsync(oauthService); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); string blobName = GetNewBlobName(); @@ -1421,6 +1421,81 @@ public async Task AppendBlockFromUriAsync_CPK() } } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task AppendBlockFromUriAsync_SourceCPK() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + AppendBlobClient sourceBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + AppendBlobClient destBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + await destBlob.CreateIfNotExistsAsync(); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + await sourceBlob.CreateIfNotExistsAsync(); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.AppendBlockAsync(stream); + + // Act + AppendBlobAppendBlockFromUriOptions options = new AppendBlobAppendBlockFromUriOptions + { + SourceCustomerProvidedKey = sourceCustomerProvidedKey + }; + Response response = await destBlob.AppendBlockFromUriAsync( + sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddDays(1)), + options); + + // Assert + Assert.AreEqual(destCustomerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); + } + + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task AppendBlockFromUriAsync_SourceCPK_Fail() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + AppendBlobClient sourceBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + AppendBlobClient destBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + await destBlob.CreateIfNotExistsAsync(); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + await sourceBlob.CreateIfNotExistsAsync(); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.AppendBlockAsync(stream); + + // Act + AppendBlobAppendBlockFromUriOptions options = new AppendBlobAppendBlockFromUriOptions + { + // incorrectly use the dest CPK here + SourceCustomerProvidedKey = destCustomerProvidedKey + }; + await TestHelper.AssertExpectedExceptionAsync( + destBlob.AppendBlockFromUriAsync( + sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddDays(1)), + options), + e => + { + Assert.AreEqual(409, e.Status); + Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode); + StringAssert.Contains("The given customer specified encryption does not match the encryption used to encrypt the blob.", e.Message); + }); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_07_07)] public async Task AppendBlockFromUriAsync_EncryptionScope() diff --git a/sdk/storage/Azure.Storage.Blobs/tests/Azure.Storage.Blobs.Tests.csproj b/sdk/storage/Azure.Storage.Blobs/tests/Azure.Storage.Blobs.Tests.csproj index b4dca36c4981..9c7e15af9922 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/Azure.Storage.Blobs.Tests.csproj +++ b/sdk/storage/Azure.Storage.Blobs/tests/Azure.Storage.Blobs.Tests.csproj @@ -6,6 +6,9 @@ Microsoft Azure.Storage.Blobs client library tests false + + BlobSDK + diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs index 399320cd4a72..0fe886b67e89 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs @@ -282,9 +282,9 @@ public async Task Ctor_AzureSasCredential_UserDelegationSAS() var client = test.Container.GetBlobClient(GetNewBlobName()); await client.UploadAsync(new MemoryStream()); Uri blobUri = client.Uri; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); var sasBuilder = new BlobSasBuilder(BlobSasPermissions.All, Recording.UtcNow.AddHours(1)) { BlobContainerName = client.BlobContainerName, @@ -3715,9 +3715,9 @@ public async Task DeleteAsync_VersionIdentitySAS() Response createResponse = await blob.CreateAsync(); IDictionary metadata = BuildMetadata(); Response metadataResponse = await blob.SetMetadataAsync(metadata); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetBlobVersionIdentitySas( test.Container.Name, blob.Name, @@ -3746,9 +3746,9 @@ public async Task DeleteAsync_VersionInvalidSAS() Response createResponse = await blob.CreateAsync(); IDictionary metadata = BuildMetadata(); Response metadataResponse = await blob.SetMetadataAsync(metadata); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetBlobIdentitySas( test.Container.Name, blob.Name, @@ -3804,9 +3804,9 @@ public async Task DeleteAsync_VersionBlobIdentitySAS(BlobSasPermissions blobSasP Response createResponse = await blob.CreateAsync(); IDictionary metadata = BuildMetadata(); Response metadataResponse = await blob.SetMetadataAsync(metadata); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetBlobIdentitySas( test.Container.Name, blob.Name, @@ -3861,9 +3861,9 @@ public async Task DeleteAsync_VersionContainerIdentitySAS(BlobContainerSasPermis Response createResponse = await blob.CreateAsync(); IDictionary metadata = BuildMetadata(); Response metadataResponse = await blob.SetMetadataAsync(metadata); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetContainerIdentitySas( test.Container.Name, blobContainerSasPermissions, @@ -3922,6 +3922,92 @@ public async Task DeleteAsync_InvalidSAS() Assert.IsTrue(await blob.ExistsAsync()); } + [RecordedTest] + [TestCase(true)] + [TestCase(false)] + public async Task DeleteAsync_BlobAccessTierRequestConditions(bool isAccessTierModifiedSince) + { + await using DisposingContainer test = await GetTestContainerAsync(); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container); + + // modify the access tier + await blob.SetAccessTierAsync(AccessTier.Cool); + DateTimeOffset changeTime = Recording.UtcNow; + + BlobRequestConditions accessConditions; + if (isAccessTierModifiedSince) + { + accessConditions = new BlobRequestConditions + { + // requires modification since yesterday (which there should be modification in this time window) + AccessTierIfModifiedSince = changeTime.AddDays(-1) + }; + } + else + { + accessConditions = new BlobRequestConditions + { + // requires no modification after 5 minutes from now (which there should be no modification then) + AccessTierIfUnmodifiedSince = changeTime.AddMinutes(5) + }; + } + + // Act + Response response = await blob.DeleteAsync(conditions: accessConditions); + + // Assert + Assert.IsNotNull(response.Headers.RequestId); + Assert.IsFalse(await blob.ExistsAsync()); + } + + [RecordedTest] + [TestCase(true)] + [TestCase(false)] + public async Task DeleteAsync_BlobAccessTierRequestConditions_Fail(bool isAccessTierModifiedSince) + { + await using DisposingContainer test = await GetTestContainerAsync(); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container); + + // modify the access tier + await blob.SetAccessTierAsync(AccessTier.Cool); + DateTimeOffset changeTime = Recording.UtcNow; + + BlobRequestConditions accessConditions; + if (isAccessTierModifiedSince) + { + accessConditions = new BlobRequestConditions + { + // requires modification after 5 minutes from now (which there should be no modification then) + AccessTierIfModifiedSince = changeTime.AddMinutes(5) + }; + } + else + { + accessConditions = new BlobRequestConditions + { + // requires no modification since yesterday (which there should be modification in this time window) + AccessTierIfUnmodifiedSince = changeTime.AddDays(-1) + }; + } + + // Act + await TestHelper.AssertExpectedExceptionAsync( + blob.DeleteAsync(conditions: accessConditions), + e => + { + Assert.AreEqual(412, e.Status); + Assert.AreEqual("AccessTierChangeTimeConditionNotMet", e.ErrorCode); + StringAssert.Contains("The condition specified using access tier change time conditional header(s) is not met.", e.Message); + }); + + // Assert + Assert.IsTrue(await blob.ExistsAsync()); + } + [RecordedTest] public async Task DeleteIfExistsAsync() { @@ -4384,9 +4470,9 @@ public async Task GetPropertiesAsync_ContainerIdentitySAS() // Arrange BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); BlobSasQueryParameters blobSasQueryParameters = GetContainerIdentitySas( containerName: test.Container.Name, @@ -4548,9 +4634,9 @@ public async Task GetPropertiesAsync_BlobIdentitySAS() // Arrange BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); BlockBlobClient identitySasBlob = InstrumentClient( GetServiceClient_BlobServiceIdentitySas_Blob( @@ -4658,9 +4744,9 @@ public async Task GetPropertiesAsync_SnapshotIdentitySAS() BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); Response snapshotResponse = await blob.CreateSnapshotAsync(); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); BlobSasQueryParameters blobSasQueryParameters = GetSnapshotIdentitySas( containerName: test.Container.Name, @@ -6572,9 +6658,9 @@ public async Task GetSetTagsAsync_BlobIdentityTagSas() BlobBaseClient blob = await GetNewBlobClient(test.Container); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetBlobIdentitySas( test.Container.Name, @@ -6607,9 +6693,9 @@ public async Task GetSetTagsAsync_InvalidBlobIdentitySas() BlobBaseClient blob = await GetNewBlobClient(test.Container); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetBlobIdentitySas( test.Container.Name, @@ -6682,9 +6768,9 @@ public async Task GetSetTagsAsync_ContainerIdentityTagSas() BlobBaseClient blob = await GetNewBlobClient(test.Container); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetContainerIdentitySas( test.Container.Name, @@ -6716,9 +6802,9 @@ public async Task GetSetTagsAsync_InvalidContainerIdentitySas() BlobBaseClient blob = await GetNewBlobClient(test.Container); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); SasQueryParameters sasQueryParameters = GetContainerIdentitySas( test.Container.Name, @@ -7760,9 +7846,9 @@ public async Task GenerateUserDelegationSas_RequiredParameters() GetOptions())); string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7811,9 +7897,9 @@ public async Task GenerateUserDelegationSas_Builder() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7853,9 +7939,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() GetOptions())); string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7925,9 +8011,9 @@ public async Task GenerateUserDelegationSas_BuilderNullContainerName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7975,9 +8061,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongContainerName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8015,9 +8101,9 @@ public async Task GenerateUserDelegationSas_BuilderNullBlobName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8064,9 +8150,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongBlobName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8107,9 +8193,9 @@ public async Task GenerateUserDelegationSas_BuilderNullSnapshot() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8164,9 +8250,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongSnapshot() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8207,9 +8293,9 @@ public async Task GenerateUserDelegationSas_BuilderNullVersion() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8266,9 +8352,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongVersion() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -8306,9 +8392,9 @@ public async Task GenerateUserDelegationSas_TrimBlobSlashes() await createClient.CreateAsync(); string stringToSign = null; + BlobGetUserDelegationKeyOptions getUserDelegationKeyOptions = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await serviceClient.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTransferValidationTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTransferValidationTests.cs index 73d11612f1d8..3ec448e6d1ed 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTransferValidationTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTransferValidationTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Buffers; using System.IO; using System.Threading.Tasks; using Azure.Core.TestFramework; @@ -37,7 +39,10 @@ protected override async Task> GetDispo StorageChecksumAlgorithm uploadAlgorithm = StorageChecksumAlgorithm.None, StorageChecksumAlgorithm downloadAlgorithm = StorageChecksumAlgorithm.None) { - var disposingContainer = await ClientBuilder.GetTestContainerAsync(service: service, containerName: containerName); + var disposingContainer = await ClientBuilder.GetTestContainerAsync( + service: service, + containerName: containerName, + publicAccessType: PublicAccessType.None); disposingContainer.Container.ClientConfiguration.TransferValidation.Upload.ChecksumAlgorithm = uploadAlgorithm; disposingContainer.Container.ClientConfiguration.TransferValidation.Download.ChecksumAlgorithm = downloadAlgorithm; @@ -91,57 +96,96 @@ public override void TestAutoResolve() } #region Added Tests - [TestCaseSource("GetValidationAlgorithms")] - public async Task ExpectedDownloadStreamingStreamTypeReturned(StorageChecksumAlgorithm algorithm) + [Test] + public virtual async Task OlderServiceVersionThrowsOnStructuredMessage() { - await using var test = await GetDisposingContainerAsync(); + // use service version before structured message was introduced + await using DisposingContainer disposingContainer = await ClientBuilder.GetTestContainerAsync( + service: ClientBuilder.GetServiceClient_SharedKey( + InstrumentClientOptions(new BlobClientOptions(BlobClientOptions.ServiceVersion.V2024_11_04))), + publicAccessType: PublicAccessType.None); // Arrange - var data = GetRandomBuffer(Constants.KB); - BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewResourceName())); - using (var stream = new MemoryStream(data)) + const int dataLength = Constants.KB; + var data = GetRandomBuffer(dataLength); + + var resourceName = GetNewResourceName(); + var blob = InstrumentClient(disposingContainer.Container.GetBlobClient(GetNewResourceName())); + await blob.UploadAsync(BinaryData.FromBytes(data)); + + var validationOptions = new DownloadTransferValidationOptions { - await blob.UploadAsync(stream); - } - // don't make options instance at all for no hash request - DownloadTransferValidationOptions transferValidation = algorithm == StorageChecksumAlgorithm.None - ? default - : new DownloadTransferValidationOptions { ChecksumAlgorithm = algorithm }; + ChecksumAlgorithm = StorageChecksumAlgorithm.StorageCrc64 + }; + AsyncTestDelegate operation = async () => await (await blob.DownloadStreamingAsync( + new BlobDownloadOptions + { + Range = new HttpRange(length: Constants.StructuredMessage.MaxDownloadCrcWithHeader + 1), + TransferValidation = validationOptions, + })).Value.Content.CopyToAsync(Stream.Null); + Assert.That(operation, Throws.TypeOf()); + } + + [Test] + public async Task StructuredMessagePopulatesCrcDownloadStreaming() + { + await using DisposingContainer disposingContainer = await ClientBuilder.GetTestContainerAsync( + publicAccessType: PublicAccessType.None); + + const int dataLength = Constants.KB; + byte[] data = GetRandomBuffer(dataLength); + byte[] dataCrc = new byte[8]; + StorageCrc64Calculator.ComputeSlicedSafe(data, 0L).WriteCrc64(dataCrc); + + var blob = disposingContainer.Container.GetBlobClient(GetNewResourceName()); + await blob.UploadAsync(BinaryData.FromBytes(data)); - // Act - Response response = await blob.DownloadStreamingAsync(new BlobDownloadOptions + Response response = await blob.DownloadStreamingAsync(new() { - TransferValidation = transferValidation, - Range = new HttpRange(length: data.Length) + TransferValidation = new DownloadTransferValidationOptions + { + ChecksumAlgorithm = StorageChecksumAlgorithm.StorageCrc64 + } }); - // Assert - // validated stream is buffered - Assert.AreEqual(typeof(MemoryStream), response.Value.Content.GetType()); + // crc is not present until response stream is consumed + Assert.That(response.Value.Details.ContentCrc, Is.Null); + + byte[] downloadedData; + using (MemoryStream ms = new()) + { + await response.Value.Content.CopyToAsync(ms); + downloadedData = ms.ToArray(); + } + + Assert.That(response.Value.Details.ContentCrc, Is.EqualTo(dataCrc)); + Assert.That(downloadedData, Is.EqualTo(data)); } [Test] - public async Task ExpectedDownloadStreamingStreamTypeReturned_None() + public async Task StructuredMessagePopulatesCrcDownloadContent() { - await using var test = await GetDisposingContainerAsync(); + await using DisposingContainer disposingContainer = await ClientBuilder.GetTestContainerAsync( + publicAccessType: PublicAccessType.None); - // Arrange - var data = GetRandomBuffer(Constants.KB); - BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewResourceName())); - using (var stream = new MemoryStream(data)) - { - await blob.UploadAsync(stream); - } + const int dataLength = Constants.KB; + byte[] data = GetRandomBuffer(dataLength); + byte[] dataCrc = new byte[8]; + StorageCrc64Calculator.ComputeSlicedSafe(data, 0L).WriteCrc64(dataCrc); + + var blob = disposingContainer.Container.GetBlobClient(GetNewResourceName()); + await blob.UploadAsync(BinaryData.FromBytes(data)); - // Act - Response response = await blob.DownloadStreamingAsync(new BlobDownloadOptions + Response response = await blob.DownloadContentAsync(new BlobDownloadOptions() { - Range = new HttpRange(length: data.Length) + TransferValidation = new DownloadTransferValidationOptions + { + ChecksumAlgorithm = StorageChecksumAlgorithm.StorageCrc64 + } }); - // Assert - // unvalidated stream type is private; just check we didn't get back a buffered stream - Assert.AreNotEqual(typeof(MemoryStream), response.Value.Content.GetType()); + Assert.That(response.Value.Details.ContentCrc, Is.EqualTo(dataCrc)); + Assert.That(response.Value.Content.ToArray(), Is.EqualTo(data)); } #endregion } diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobSasBuilderTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobSasBuilderTests.cs index f592a2551e3e..840a7be72f43 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobSasBuilderTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobSasBuilderTests.cs @@ -15,6 +15,7 @@ using Azure.Storage.Sas; using Azure.Storage.Test; using Azure.Storage.Test.Shared; +using Microsoft.Rest; using NUnit.Framework; namespace Azure.Storage.Blobs.Test @@ -38,6 +39,7 @@ private static UserDelegationKey GetUserDelegationKey(TestConstants constants) SignedExpiresOn = constants.Sas.KeyExpiry, SignedService = constants.Sas.KeyService, SignedVersion = constants.Sas.KeyVersion, + SignedDelegatedUserTenantId = constants.Sas.KeyDelegatedTenantId, Value = constants.Sas.KeyValue }; @@ -53,6 +55,8 @@ public void ToSasQueryParameters_ContainerTest() includeBlob: false, includeSnapshot: false, includeDelegatedObjectId: false, + includeRequestHeaders: false, + includeRequestQueryParameters: false, containerName, blobName, constants); @@ -77,7 +81,7 @@ public void ToSasQueryParameters_ContainerTest() } [RecordedTest] - [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2020_12_06)] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] public void ToSasQueryParameters_ContainerIdentityTest() { // Arrange @@ -88,6 +92,8 @@ public void ToSasQueryParameters_ContainerIdentityTest() includeBlob: false, includeSnapshot: false, includeDelegatedObjectId: true, + includeRequestHeaders: true, + includeRequestQueryParameters: true, containerName, blobName, constants); @@ -112,9 +118,12 @@ public void ToSasQueryParameters_ContainerIdentityTest() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.Container, sasQueryParameters.Resource); Assert.AreEqual(Permissions, sasQueryParameters.Permissions); Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestHeaders), sasQueryParameters.RequestHeaders); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestQueryParameters), sasQueryParameters.RequestQueryParameters); Assert.AreEqual(signature, sasQueryParameters.Signature); AssertResponseHeaders(constants, sasQueryParameters); Assert.IsNotNull(stringToSign); @@ -132,6 +141,8 @@ public void ToSasQueryParameters_BlobTest() includeBlob: true, includeSnapshot: false, includeDelegatedObjectId: false, + includeRequestHeaders: false, + includeRequestQueryParameters: false, containerName, blobName, constants); @@ -158,7 +169,7 @@ public void ToSasQueryParameters_BlobTest() } [RecordedTest] - [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_02_06)] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] public void ToSasQueryParameters_BlobIdentityTest() { // Arrange @@ -169,6 +180,8 @@ public void ToSasQueryParameters_BlobIdentityTest() includeBlob: true, includeSnapshot: false, includeDelegatedObjectId: true, + includeRequestHeaders: true, + includeRequestQueryParameters: true, containerName, blobName, constants); @@ -192,9 +205,12 @@ public void ToSasQueryParameters_BlobIdentityTest() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.Blob, sasQueryParameters.Resource); Assert.AreEqual(Permissions, sasQueryParameters.Permissions); Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestHeaders), sasQueryParameters.RequestHeaders); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestQueryParameters), sasQueryParameters.RequestQueryParameters); Assert.AreEqual(signature, sasQueryParameters.Signature); AssertResponseHeaders(constants, sasQueryParameters); } @@ -211,6 +227,8 @@ public void ToSasQueryParameters_SnapshotTest() includeBlob: true, includeSnapshot: true, includeDelegatedObjectId: false, + includeRequestHeaders: false, + includeRequestQueryParameters: false, containerName, blobName, constants); @@ -235,7 +253,7 @@ public void ToSasQueryParameters_SnapshotTest() } [RecordedTest] - [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2020_12_06)] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] public void ToSasQueryParameters_SnapshotIdentityTest() { // Arrange @@ -246,6 +264,8 @@ public void ToSasQueryParameters_SnapshotIdentityTest() includeBlob: true, includeSnapshot: true, includeDelegatedObjectId: true, + includeRequestHeaders: true, + includeRequestQueryParameters: true, containerName, blobName, constants); @@ -274,9 +294,12 @@ public void ToSasQueryParameters_SnapshotIdentityTest() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.BlobSnapshot, sasQueryParameters.Resource); Assert.AreEqual(Permissions, sasQueryParameters.Permissions); Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestHeaders), sasQueryParameters.RequestHeaders); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestQueryParameters), sasQueryParameters.RequestQueryParameters); Assert.AreEqual(signature, sasQueryParameters.Signature); AssertResponseHeaders(constants, sasQueryParameters); } @@ -292,6 +315,8 @@ public void ToSasQueryParameters_NullSharedKeyCredentialTest() includeBlob: true, includeSnapshot: true, includeDelegatedObjectId: false, + includeRequestHeaders: false, + includeRequestQueryParameters: false, containerName, blobName, constants); @@ -507,6 +532,8 @@ private BlobSasBuilder BuildBlobSasBuilder( bool includeBlob, bool includeSnapshot, bool includeDelegatedObjectId, + bool includeRequestHeaders, + bool includeRequestQueryParameters, string containerName, string blobName, TestConstants constants) { @@ -533,6 +560,14 @@ private BlobSasBuilder BuildBlobSasBuilder( { builder.DelegatedUserObjectId = constants.Sas.DelegatedObjectId; } + if (includeRequestHeaders) + { + builder.RequestHeaders = constants.Sas.RequestHeaders; + } + if (includeRequestQueryParameters) + { + builder.RequestQueryParameters = constants.Sas.RequestQueryParameters; + } builder.SetPermissions(BlobAccountSasPermissions.Read | BlobAccountSasPermissions.Write | BlobAccountSasPermissions.Delete); return builder; @@ -550,9 +585,9 @@ public async Task BlobSasBuilder_PreauthorizedAgentObjectId() await using DisposingContainer test = await GetTestContainerAsync(service: oauthService, containerName: containerName); // Arrange + BlobGetUserDelegationKeyOptions getUserDelegationKeyOptions = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); BlobSasBuilder BlobSasBuilder = new BlobSasBuilder { @@ -587,9 +622,9 @@ public async Task BlobSasBuilder_CorrelationId() await using DisposingContainer test = await GetTestContainerAsync(service: oauthService, containerName: containerName); // Arrange + BlobGetUserDelegationKeyOptions getUserDelegationKeyOptions = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); BlobSasBuilder blobSasBuilder = new BlobSasBuilder { @@ -624,9 +659,12 @@ public async Task SasCredentialRequiresUriWithoutSasError_RedactedSasUri() await using DisposingContainer test = await GetTestContainerAsync(service: oauthService, containerName: containerName); + BlobGetUserDelegationKeyOptions getUserDelegationKeyOptions = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); BlobSasBuilder blobSasBuilder = new BlobSasBuilder { @@ -729,7 +767,7 @@ private string BuildIdentitySignature( null, null, null, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + constants.Sas.KeyDelegatedTenantId, constants.Sas.DelegatedObjectId, constants.Sas.IPRange.ToString(), SasExtensions.ToProtocolString(constants.Sas.Protocol), @@ -737,6 +775,8 @@ private string BuildIdentitySignature( resource, includeSnapshot ? Snapshot : null, constants.Sas.EncryptionScope, + SasExtensions.FormatRequestHeadersForSasSigning(constants.Sas.RequestHeaders), + SasExtensions.FormatRequestQueryParametersForSasSigning(constants.Sas.RequestQueryParameters), constants.Sas.CacheControl, constants.Sas.ContentDisposition, constants.Sas.ContentEncoding, diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobSasTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobSasTests.cs index 38756513b7f4..e24aaf20f0bd 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobSasTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobSasTests.cs @@ -63,9 +63,9 @@ public async Task BlobIdentitySas_AllPermissions() string blobName = GetNewBlobName(); await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); BlobSasBuilder blobSasBuilder = new BlobSasBuilder( permissions: BlobSasPermissions.All, @@ -128,9 +128,9 @@ public async Task BlobVersionIdentitySas_AllPermissions() string blobName = GetNewBlobName(); await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(blobName)); Response createResponse = await blob.CreateAsync(); @@ -198,9 +198,9 @@ public async Task BlobSnapshotIdentitySas_AllPermissions() string blobName = GetNewBlobName(); await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(blobName)); await blob.CreateAsync(); @@ -262,9 +262,9 @@ public async Task ContainerIdentitySas_AllPermissions() string blobName = GetNewBlobName(); await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); BlobSasBuilder blobSasBuilder = new BlobSasBuilder( permissions: BlobContainerSasPermissions.All, @@ -284,6 +284,171 @@ public async Task ContainerIdentitySas_AllPermissions() await appendBlobClient.CreateAsync(); } + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_DelegatedTenantId() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder blobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + BlockBlobClient identitySasBlob = InstrumentClient(new BlockBlobClient(blobUriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act + Response response = await identitySasBlob.GetPropertiesAsync(); + AssertSasUserDelegationKey(identitySasBlob.Uri, userDelegationKey.Value); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_DelegatedTenantId_Fail() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + // We are deliberately not passing in DelegatedUserObjectId to cause an auth failure + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder blobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + BlockBlobClient identitySasBlob = InstrumentClient(new BlockBlobClient(blobUriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act & Assert + await TestHelper.AssertExpectedExceptionAsync( + identitySasBlob.GetPropertiesAsync(), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_DelegatedTenantId_Roundtrip() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder originalBlobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + BlobUriBuilder roundtripBlobUriBuilder = new BlobUriBuilder(originalBlobUriBuilder.ToUri()); + + Assert.AreEqual(originalBlobUriBuilder.ToUri(), roundtripBlobUriBuilder.ToUri()); + Assert.AreEqual(originalBlobUriBuilder.Sas.ToString(), roundtripBlobUriBuilder.Sas.ToString()); + } + [RecordedTest] [LiveOnly] // Cannot record Entra ID token [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_02_06)] @@ -297,9 +462,9 @@ public async Task ContainerIdentitySAS_DelegatedObjectId() // Arrange BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -346,9 +511,9 @@ public async Task ContainerIdentitySAS_DelegatedObjectId_Fail() // Arrange BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -381,6 +546,187 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_RequestHeadersAndQueryParameters() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync(options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder blobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + CustomRequestHeadersAndQueryParametersPolicy customRequestPolicy = new CustomRequestHeadersAndQueryParametersPolicy(); + // Send the request headers based on 'requestHeaders' Dictionary + foreach (var header in requestHeaders) + { + if (header.Key != null) + { + customRequestPolicy.AddRequestHeader(header.Key, header.Value); + } + } + + // Send the query parameters based on 'requestQueryParameters' Dictionary + foreach (var param in requestQueryParameters) + { + if (param.Key != null) + { + customRequestPolicy.AddQueryParameter(param.Key, param.Value); + } + } + + BlobClientOptions blobClientOptions = GetOptions(); + blobClientOptions.AddPolicy(customRequestPolicy, HttpPipelinePosition.PerCall); + BlockBlobClient identitySasBlob = InstrumentClient(new BlockBlobClient(blobUriBuilder.ToUri(), TestEnvironment.Credential, blobClientOptions)); + + // Act + Response response = await identitySasBlob.GetPropertiesAsync(); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_RequestHeadersAndQueryParameters_Fail() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder blobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + // Deliberately do not send the request header and query parameter to cause an auth failure + + BlobClientOptions blobClientOptions = GetOptions(); + BlockBlobClient identitySasBlob = InstrumentClient(new BlockBlobClient(blobUriBuilder.ToUri(), TestEnvironment.Credential, blobClientOptions)); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + identitySasBlob.GetPropertiesAsync(), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + public async Task ContainerIdentitySAS_RequestHeadersAndQueryParameters_Roundtrip() + { + BlobServiceClient oauthService = GetServiceClient_OAuth(); + var containerName = GetNewContainerName(); + var blobName = GetNewBlobName(); + await using DisposingContainer test = await GetTestContainerAsync(containerName: containerName, service: oauthService); + + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container, blobName); + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + BlobSasBuilder blobSasBuilder = new BlobSasBuilder(BlobContainerSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + BlobContainerName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + BlobSasQueryParameters blobSasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + BlobUriBuilder originalBlobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = blobSasQueryParameters + }; + + BlobUriBuilder roundtripBlobUriBuilder = new BlobUriBuilder(originalBlobUriBuilder.ToUri()); + + Assert.AreEqual(originalBlobUriBuilder.ToUri(), roundtripBlobUriBuilder.ToUri()); + Assert.AreEqual(originalBlobUriBuilder.Sas.ToString(), roundtripBlobUriBuilder.Sas.ToString()); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2020_06_12)] public async Task AccountSas_AllPermissions() diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs index 5a1195871510..8ad5c8b4243b 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs @@ -639,6 +639,7 @@ public void AssertSasUserDelegationKey(Uri uri, UserDelegationKey key) Assert.AreEqual(key.SignedService, sas.KeyService); Assert.AreEqual(key.SignedStartsOn, sas.KeyStartsOn); Assert.AreEqual(key.SignedTenantId, sas.KeyTenantId); + Assert.AreEqual(key.SignedDelegatedUserTenantId, sas.KeyDelegatedUserTenantId); //Assert.AreEqual(key.SignedVersion, sas.Version); } } diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestEnvironment.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestEnvironment.cs index 55e207ee978b..6d6336dac885 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestEnvironment.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestEnvironment.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; using Azure.Storage.Sas; using Azure.Storage.Test; @@ -41,7 +42,9 @@ private async Task DoesOAuthWorkAsync() await blobClient.CreateIfNotExistsAsync(); await blobClient.GetPropertiesAsync(); - var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync(startsOn: null, expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync( + options: options); var sasBuilder = new BlobSasBuilder(BlobSasPermissions.All, DateTimeOffset.UtcNow.AddHours(1)) { BlobContainerName = containerName, diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobsClientTestFixtureAttribute.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobsClientTestFixtureAttribute.cs index bafc5255e280..eaf418db24bd 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobsClientTestFixtureAttribute.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobsClientTestFixtureAttribute.cs @@ -40,6 +40,7 @@ public BlobsClientTestFixtureAttribute(params object[] additionalParameters) BlobClientOptions.ServiceVersion.V2025_07_05, BlobClientOptions.ServiceVersion.V2025_11_05, BlobClientOptions.ServiceVersion.V2026_02_06, + BlobClientOptions.ServiceVersion.V2026_04_06, StorageVersionExtensions.LatestVersion, StorageVersionExtensions.MaxVersion }, diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs index 6d5cd29755e3..dc1044e8627f 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs @@ -716,6 +716,79 @@ await RetryAsync( _retryStageBlockFromUri); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task StageBlockFromUriAsync_SourceCPK() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadAsync(stream); + + // Act + StageBlockFromUriOptions options = new StageBlockFromUriOptions + { + SourceCustomerProvidedKey = sourceCustomerProvidedKey + }; + Response response = await destBlob.StageBlockFromUriAsync( + sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddHours(1)), + ToBase64(GetNewBlockName()), + options); + + // Assert + Assert.AreEqual(destCustomerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); + } + + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task StageBlockFromUriAsync_SourceCPK_Fail() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadAsync(stream); + + // Act + StageBlockFromUriOptions options = new StageBlockFromUriOptions + { + // incorrectly use the dest CPK here + SourceCustomerProvidedKey = destCustomerProvidedKey + }; + await TestHelper.AssertExpectedExceptionAsync( + destBlob.StageBlockFromUriAsync( + sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddHours(1)), + ToBase64(GetNewBlockName()), + options), + e => + { + Assert.AreEqual(409, e.Status); + Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode); + StringAssert.Contains("The given customer specified encryption does not match the encryption used to encrypt the blob.", e.Message); + }); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_07_07)] public async Task StageBlockFromUriAsync_EncryptionScope() @@ -3314,6 +3387,74 @@ public async Task SyncUploadFromUriAsync_CPK() Assert.AreEqual(customerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task SyncUploadFromUriAsync_SourceCPK() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadAsync(stream); + + // Act + BlobSyncUploadFromUriOptions options = new BlobSyncUploadFromUriOptions + { + SourceCustomerProvidedKey = sourceCustomerProvidedKey + }; + Response response = await destBlob.SyncUploadFromUriAsync( + sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddHours(1)), options); + + // Assert + Assert.AreEqual(destCustomerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); + } + + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task SyncUploadFromUriAsync_SourceCPK_Fail() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadAsync(stream); + + // Act + BlobSyncUploadFromUriOptions options = new BlobSyncUploadFromUriOptions + { + // incorrectly use the dest CPK here + SourceCustomerProvidedKey = destCustomerProvidedKey + }; + await TestHelper.AssertExpectedExceptionAsync( + destBlob.SyncUploadFromUriAsync(sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddHours(1)), options), + e => + { + Assert.AreEqual(409, e.Status); + Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode); + StringAssert.Contains("The given customer specified encryption does not match the encryption used to encrypt the blob.", e.Message); + }); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2020_04_08)] public async Task SyncUploadFromUriAsync_EncryptionScope() diff --git a/sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs index 9b7000e7d9d3..a2cfb6baf651 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs @@ -1369,7 +1369,7 @@ public void CanParseLargeContentRange() { long compareValue = (long)Int32.MaxValue + 1; //Increase max int32 by one ContentRange contentRange = ContentRange.Parse($"bytes 0 {compareValue} {compareValue}"); - Assert.AreEqual((long)Int32.MaxValue + 1, contentRange.Size); + Assert.AreEqual((long)Int32.MaxValue + 1, contentRange.TotalResourceLength); Assert.AreEqual(0, contentRange.Start); Assert.AreEqual((long)Int32.MaxValue + 1, contentRange.End); } diff --git a/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs index a64eb4f63cf2..b3f057cd5ba5 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs @@ -3946,9 +3946,10 @@ public async Task GenerateUserDelegationSas_RequiredParameters() GetOptions())); string stringToSign = null; + + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; //Act @@ -3991,9 +3992,9 @@ public async Task GenerateUserDelegationSas_Builder() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -4029,9 +4030,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() GetOptions())); string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -4093,9 +4094,9 @@ public async Task GenerateUserDelegationSas_BuilderNullName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -4140,9 +4141,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -4177,9 +4178,9 @@ public async Task GenerateUserDelegationSas_BuilderIncorrectlySettingBlobName() }; string stringToSign = null; + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs index 94df9d22236f..2e28195995e5 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs @@ -3504,6 +3504,85 @@ public async Task UploadPagesFromUriAsync_CPK() Assert.AreEqual(customerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task UploadPagesFromUriAsync_SourceCPK() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + PageBlobClient sourceBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + PageBlobClient destBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + await destBlob.CreateIfNotExistsAsync(Constants.KB); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + await sourceBlob.CreateIfNotExistsAsync(Constants.KB); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadPagesAsync(stream, 0); + + // Act + PageBlobUploadPagesFromUriOptions options = new PageBlobUploadPagesFromUriOptions + { + SourceCustomerProvidedKey = sourceCustomerProvidedKey + }; + Response response = await destBlob.UploadPagesFromUriAsync( + sourceUri: sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddDays(1)), + sourceRange: new HttpRange(0, Constants.KB), + range: new HttpRange(0, Constants.KB), + options: options); + + // Assert + Assert.AreEqual(destCustomerProvidedKey.EncryptionKeyHash, response.Value.EncryptionKeySha256); + } + + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2026_04_06)] + [LiveOnly(Reason = "Encryption Key cannot be stored in recordings.")] + public async Task UploadPagesFromUriAsync_SourceCPK_Fail() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + PageBlobClient sourceBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + PageBlobClient destBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + + CustomerProvidedKey destCustomerProvidedKey = GetCustomerProvidedKey(); + destBlob = destBlob.WithCustomerProvidedKey(destCustomerProvidedKey); + await destBlob.CreateIfNotExistsAsync(Constants.KB); + + CustomerProvidedKey sourceCustomerProvidedKey = GetCustomerProvidedKey(); + sourceBlob = sourceBlob.WithCustomerProvidedKey(sourceCustomerProvidedKey); + await sourceBlob.CreateIfNotExistsAsync(Constants.KB); + // Upload data to source blob + byte[] data = GetRandomBuffer(Constants.KB); + using Stream stream = new MemoryStream(data); + await sourceBlob.UploadPagesAsync(stream, 0); + + // Act + PageBlobUploadPagesFromUriOptions options = new PageBlobUploadPagesFromUriOptions + { + // incorrectly use the dest CPK here + SourceCustomerProvidedKey = destCustomerProvidedKey + }; + await TestHelper.AssertExpectedExceptionAsync( + destBlob.UploadPagesFromUriAsync( + sourceUri: sourceBlob.GenerateSasUri(BlobSasPermissions.Read, Recording.UtcNow.AddDays(1)), + sourceRange: new HttpRange(0, Constants.KB), + range: new HttpRange(0, Constants.KB), + options: options), + e => + { + Assert.AreEqual(409, e.Status); + Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode); + StringAssert.Contains("The given customer specified encryption does not match the encryption used to encrypt the blob.", e.Message); + }); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_07_07)] public async Task UploadPagesFromUriAsync_EncryptionScope() diff --git a/sdk/storage/Azure.Storage.Blobs/tests/PartitionedDownloaderTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/PartitionedDownloaderTests.cs index 1616cff452cd..acd847efbcbe 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/PartitionedDownloaderTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/PartitionedDownloaderTests.cs @@ -346,7 +346,7 @@ public Response GetStream(HttpRange range, BlobRequ ContentHash = new byte[] { 1, 2, 3 }, LastModified = DateTimeOffset.Now, Metadata = new Dictionary() { { "meta", "data" } }, - ContentRange = $"bytes {range.Offset}-{range.Offset + contentLength}/{_length}", + ContentRange = $"bytes {range.Offset}-{Math.Max(1, range.Offset + contentLength - 1)}/{_length}", ETag = s_etag, ContentEncoding = "test", CacheControl = "test", diff --git a/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs index 5b7e7a03d9c1..cdf6446439f0 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs @@ -14,6 +14,7 @@ using Azure.Storage.Test; using Azure.Storage.Test.Shared; using Azure.Storage.Tests.Shared; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -700,7 +701,9 @@ public async Task GetUserDelegationKey() BlobServiceClient service = GetServiceClient_OAuth(); // Act - Response response = await service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)); + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response response = await service.GetUserDelegationKeyAsync( + options: options); // Assert Assert.IsNotNull(response.Value); @@ -713,8 +716,9 @@ public async Task GetUserDelegationKey_Error() BlobServiceClient service = GetServiceClient_SharedKey(); // Act + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)), + service.GetUserDelegationKeyAsync(options: options), e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } @@ -725,14 +729,15 @@ public async Task GetUserDelegationKey_ArgumentException() BlobServiceClient service = GetServiceClient_OAuth(); // Act + BlobGetUserDelegationKeyOptions options = new BlobGetUserDelegationKeyOptions( + // ensure the time used is not UTC, as DateTimeOffset.Now could actually be UTC based on OS settings + // Use a custom time zone so we aren't dependent on OS having specific standard time zone. + expiresOn: TimeZoneInfo.ConvertTime( + Recording.Now.AddHours(1), + TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))); await TestHelper.AssertExpectedExceptionAsync( service.GetUserDelegationKeyAsync( - startsOn: null, - // ensure the time used is not UTC, as DateTimeOffset.Now could actually be UTC based on OS settings - // Use a custom time zone so we aren't dependent on OS having specific standard time zone. - expiresOn: TimeZoneInfo.ConvertTime( - Recording.Now.AddHours(1), - TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))), + options: options), e => Assert.AreEqual("expiresOn must be UTC", e.Message)); ; } diff --git a/sdk/storage/Azure.Storage.Common/CHANGELOG.md b/sdk/storage/Azure.Storage.Common/CHANGELOG.md index ecb697eb6d3d..070279d32289 100644 --- a/sdk/storage/Azure.Storage.Common/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Common/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 12.26.0-beta.2 (Unreleased) +## 12.27.0-beta.1 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed +- This release contains bug fixes to improve quality. ### Other Changes +- Changed the default concurrency upload count from 5 to Math.Clamp(Environment.ProcessorCount * 2, 8, 32). This controls the maximum number of concurrent tasks that will be used during large uploads, and this change should result in higher throughput for these operations by default in most environments. This can be reverted by enabling "Azure.Storage.UseLegacyDefaultConcurrency" in the AppContext switch or "AZURE_STORAGE_USE_LEGACY_DEFAULT_CONCURRENCY" in the environment variable. ## 12.26.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.net8.0.cs b/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.net8.0.cs index 5328c80c714e..dff34beba46b 100644 --- a/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.net8.0.cs +++ b/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.net8.0.cs @@ -183,7 +183,7 @@ public enum SasProtocol } public partial class SasQueryParameters { - public const string DefaultSasVersion = "2026-02-06"; + public const string DefaultSasVersion = "2026-04-06"; protected SasQueryParameters() { } protected SasQueryParameters(System.Collections.Generic.IDictionary values) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -192,7 +192,9 @@ protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasService protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?)) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null) { } + protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null, System.Collections.Generic.List requestHeaders = null, System.Collections.Generic.List requestQueryParameter = null) { } public string AgentObjectId { get { throw null; } } public string CacheControl { get { throw null; } } public string ContentDisposition { get { throw null; } } @@ -210,6 +212,8 @@ protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasService public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } } + public System.Collections.Generic.List RequestHeaders { get { throw null; } } + public System.Collections.Generic.List RequestQueryParameters { get { throw null; } } public string Resource { get { throw null; } } public Azure.Storage.Sas.AccountSasResourceTypes? ResourceTypes { get { throw null; } } public Azure.Storage.Sas.AccountSasServices? Services { get { throw null; } } @@ -224,7 +228,9 @@ protected internal void AppendProperties(System.Text.StringBuilder stringBuilder protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?)) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null) { throw null; } + protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null, System.Collections.Generic.List requestHeaders = null, System.Collections.Generic.List requestQueryParameter = null) { throw null; } public override string ToString() { throw null; } } } diff --git a/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.netstandard2.0.cs b/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.netstandard2.0.cs index b09b9bb5cad3..6c763343692c 100644 --- a/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.netstandard2.0.cs +++ b/sdk/storage/Azure.Storage.Common/api/Azure.Storage.Common.netstandard2.0.cs @@ -182,7 +182,7 @@ public enum SasProtocol } public partial class SasQueryParameters { - public const string DefaultSasVersion = "2026-02-06"; + public const string DefaultSasVersion = "2026-04-06"; protected SasQueryParameters() { } protected SasQueryParameters(System.Collections.Generic.IDictionary values) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -191,7 +191,9 @@ protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasService protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?)) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null) { } + protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null, System.Collections.Generic.List requestHeaders = null, System.Collections.Generic.List requestQueryParameter = null) { } public string AgentObjectId { get { throw null; } } public string CacheControl { get { throw null; } } public string ContentDisposition { get { throw null; } } @@ -209,6 +211,8 @@ protected SasQueryParameters(string version, Azure.Storage.Sas.AccountSasService public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } } + public System.Collections.Generic.List RequestHeaders { get { throw null; } } + public System.Collections.Generic.List RequestQueryParameters { get { throw null; } } public string Resource { get { throw null; } } public Azure.Storage.Sas.AccountSasResourceTypes? ResourceTypes { get { throw null; } } public Azure.Storage.Sas.AccountSasServices? Services { get { throw null; } } @@ -223,7 +227,9 @@ protected internal void AppendProperties(System.Text.StringBuilder stringBuilder protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?)) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null) { throw null; } + protected static Azure.Storage.Sas.SasQueryParameters Create(string version, Azure.Storage.Sas.AccountSasServices? services, Azure.Storage.Sas.AccountSasResourceTypes? resourceTypes, Azure.Storage.Sas.SasProtocol protocol, System.DateTimeOffset startsOn, System.DateTimeOffset expiresOn, Azure.Storage.Sas.SasIPRange ipRange, string identifier, string resource, string permissions, string signature, string cacheControl = null, string contentDisposition = null, string contentEncoding = null, string contentLanguage = null, string contentType = null, string authorizedAadObjectId = null, string unauthorizedAadObjectId = null, string correlationId = null, int? directoryDepth = default(int?), string encryptionScope = null, string delegatedUserObjectId = null, System.Collections.Generic.List requestHeaders = null, System.Collections.Generic.List requestQueryParameter = null) { throw null; } public override string ToString() { throw null; } } } diff --git a/sdk/storage/Azure.Storage.Common/samples/Azure.Storage.Common.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Common/samples/Azure.Storage.Common.Samples.Tests.csproj index 7d454aeaa0af..aeca4497a877 100644 --- a/sdk/storage/Azure.Storage.Common/samples/Azure.Storage.Common.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Common/samples/Azure.Storage.Common.Samples.Tests.csproj @@ -19,6 +19,7 @@ + PreserveNewest diff --git a/sdk/storage/Azure.Storage.Common/src/Azure.Storage.Common.csproj b/sdk/storage/Azure.Storage.Common/src/Azure.Storage.Common.csproj index d325235f8ae7..0eb90ebafa54 100644 --- a/sdk/storage/Azure.Storage.Common/src/Azure.Storage.Common.csproj +++ b/sdk/storage/Azure.Storage.Common/src/Azure.Storage.Common.csproj @@ -4,7 +4,7 @@ Microsoft Azure.Storage.Common client library - 12.26.0-beta.2 + 12.27.0-beta.1 12.25.0 CommonSDK;$(DefineConstants) diff --git a/sdk/storage/Azure.Storage.Common/src/Sas/SasQueryParameters.cs b/sdk/storage/Azure.Storage.Common/src/Sas/SasQueryParameters.cs index 881d4601d094..8e3c0a2ad4dc 100644 --- a/sdk/storage/Azure.Storage.Common/src/Sas/SasQueryParameters.cs +++ b/sdk/storage/Azure.Storage.Common/src/Sas/SasQueryParameters.cs @@ -100,6 +100,12 @@ public partial class SasQueryParameters // sduoid private string _delegatedUserObjectId; + // srh + private List _requestHeaders; + + // srq + private List _requestQueryParameters; + /// /// Gets the storage service version to use to authenticate requests /// made with this shared access signature, and the service version to @@ -251,6 +257,19 @@ public partial class SasQueryParameters /// issued to the user specified in this value. /// public string DelegatedUserObjectId => _delegatedUserObjectId ?? string.Empty; + + /// + /// Custom Request Header names to include in the SAS. Any usage of the SAS must + /// include these header names in the request. + /// + public List RequestHeaders => _requestHeaders ?? null; + + /// + /// Custom Request Query Parameter names to include in the SAS. Any usage of the SAS must + /// include these query parameter names in the request. + /// + public List RequestQueryParameters => _requestQueryParameters ?? null; + /// /// Gets the string-to-sign, a unique string constructed from the /// fields that must be verified in order to authenticate the request. @@ -354,6 +373,12 @@ protected SasQueryParameters(IDictionary values) case Constants.Sas.Parameters.DelegatedUserObjectIdUpper: _delegatedUserObjectId = kv.Value; break; + case Constants.Sas.Parameters.RequestHeadersUpper: + _requestHeaders = ParseStringToList(kv.Value); + break; + case Constants.Sas.Parameters.RequestQueryParametersUpper: + _requestQueryParameters = ParseStringToList(kv.Value); + break; // We didn't recognize the query parameter default: @@ -372,6 +397,64 @@ protected SasQueryParameters(IDictionary values) /// /// Creates a new SasQueryParameters instance. /// + protected SasQueryParameters( + string version, + AccountSasServices? services, + AccountSasResourceTypes? resourceTypes, + SasProtocol protocol, + DateTimeOffset startsOn, + DateTimeOffset expiresOn, + SasIPRange ipRange, + string identifier, + string resource, + string permissions, + string signature, + string cacheControl = default, + string contentDisposition = default, + string contentEncoding = default, + string contentLanguage = default, + string contentType = default, + string authorizedAadObjectId = default, + string unauthorizedAadObjectId = default, + string correlationId = default, + int? directoryDepth = default, + string encryptionScope = default, + string delegatedUserObjectId = default, + List requestHeaders = default, + List requestQueryParameter = default) + { + _version = version; + _services = (services, services?.ToPermissionsString()); + _resourceTypes = (resourceTypes, resourceTypes?.ToPermissionsString()); + _protocol = (protocol, protocol.ToProtocolString()); + _startTime = startsOn; + _startTimeString = startsOn.ToString(Constants.SasTimeFormatSeconds, CultureInfo.InvariantCulture); + _expiryTime = expiresOn; + _expiryTimeString = expiresOn.ToString(Constants.SasTimeFormatSeconds, CultureInfo.InvariantCulture); + _ipRange = ipRange; + _identifier = identifier; + _resource = resource; + _permissions = permissions; + _signature = signature; + _cacheControl = cacheControl; + _contentDisposition = contentDisposition; + _contentEncoding = contentEncoding; + _contentLanguage = contentLanguage; + _contentType = contentType; + _preauthorizedAgentObjectId = authorizedAadObjectId; + _agentObjectId = unauthorizedAadObjectId; + _correlationId = correlationId; + _directoryDepth = directoryDepth; + _encryptionScope = encryptionScope; + _delegatedUserObjectId = delegatedUserObjectId; + _requestHeaders = requestHeaders; + _requestQueryParameters = requestQueryParameter; + } + + /// + /// Creates a new SasQueryParameters instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] protected SasQueryParameters( string version, AccountSasServices? services, @@ -583,6 +666,61 @@ protected static SasQueryParameters Create(IDictionary values) = /// /// Creates a new SasQueryParameters instance. /// + protected static SasQueryParameters Create( + string version, + AccountSasServices? services, + AccountSasResourceTypes? resourceTypes, + SasProtocol protocol, + DateTimeOffset startsOn, + DateTimeOffset expiresOn, + SasIPRange ipRange, + string identifier, + string resource, + string permissions, + string signature, + string cacheControl = default, + string contentDisposition = default, + string contentEncoding = default, + string contentLanguage = default, + string contentType = default, + string authorizedAadObjectId = default, + string unauthorizedAadObjectId = default, + string correlationId = default, + int? directoryDepth = default, + string encryptionScope = default, + string delegatedUserObjectId = default, + List requestHeaders = default, + List requestQueryParameter = default) => + new SasQueryParameters( + version: version, + services: services, + resourceTypes: resourceTypes, + protocol: protocol, + startsOn: startsOn, + expiresOn: expiresOn, + ipRange: ipRange, + identifier: identifier, + resource: resource, + permissions: permissions, + signature: signature, + cacheControl: cacheControl, + contentDisposition: contentDisposition, + contentEncoding: contentEncoding, + contentLanguage: contentLanguage, + contentType: contentType, + authorizedAadObjectId: authorizedAadObjectId, + unauthorizedAadObjectId: unauthorizedAadObjectId, + correlationId: correlationId, + directoryDepth: directoryDepth, + encryptionScope: encryptionScope, + delegatedUserObjectId: delegatedUserObjectId, + requestHeaders: requestHeaders, + requestQueryParameter: requestQueryParameter); + + /// + /// Creates a new SasQueryParameters instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] protected static SasQueryParameters Create( string version, AccountSasServices? services, @@ -892,6 +1030,20 @@ protected internal void AppendProperties(StringBuilder stringBuilder) stringBuilder.AppendQueryParameter(Constants.Sas.Parameters.DelegatedUserObjectId, WebUtility.UrlEncode(DelegatedUserObjectId)); } + if (RequestHeaders != null && RequestHeaders.Count > 0) + { + stringBuilder.AppendQueryParameter( + Constants.Sas.Parameters.RequestHeaders, + ListToEncodedSasQueryParameterString(RequestHeaders)); + } + + if (RequestQueryParameters != null && RequestQueryParameters.Count > 0) + { + stringBuilder.AppendQueryParameter( + Constants.Sas.Parameters.RequestQueryParameters, + ListToEncodedSasQueryParameterString(RequestQueryParameters)); + } + if (!string.IsNullOrWhiteSpace(Signature)) { stringBuilder.AppendQueryParameter(Constants.Sas.Parameters.Signature, WebUtility.UrlEncode(Signature)); @@ -908,6 +1060,20 @@ private static DateTimeOffset ParseSasTime(string dateTimeString) return DateTimeOffset.ParseExact(dateTimeString, s_sasTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); } + private string ListToEncodedSasQueryParameterString(List list) + { + return list == null || list.Count == 0 + ? string.Empty + : string.Join(",", list.Select(WebUtility.UrlEncode)); + } + + private List ParseStringToList(string listString) + { + return string.IsNullOrEmpty(listString) + ? null + : listString.Split(',').ToList(); + } + private static readonly string[] s_sasTimeFormats = { Constants.SasTimeFormatSeconds, Constants.SasTimeFormatSubSeconds, diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/ChecksumExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/ChecksumExtensions.cs new file mode 100644 index 000000000000..48304640eee4 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/ChecksumExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Binary; + +namespace Azure.Storage; + +internal static class ChecksumExtensions +{ + public static void WriteCrc64(this ulong crc, Span dest) + => BinaryPrimitives.WriteUInt64LittleEndian(dest, crc); + + public static bool TryWriteCrc64(this ulong crc, Span dest) + => BinaryPrimitives.TryWriteUInt64LittleEndian(dest, crc); + + public static ulong ReadCrc64(this ReadOnlySpan crc) + => BinaryPrimitives.ReadUInt64LittleEndian(crc); + + public static bool TryReadCrc64(this ReadOnlySpan crc, out ulong value) + => BinaryPrimitives.TryReadUInt64LittleEndian(crc, out value); +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/CompatSwitches.cs b/sdk/storage/Azure.Storage.Common/src/Shared/CompatSwitches.cs index 04e4f7cd4cf9..231f5a3e1860 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/CompatSwitches.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/CompatSwitches.cs @@ -16,5 +16,10 @@ internal static class CompatSwitches public static bool DisableExpectContinueHeader => _disableExpectContinueHeader ??= AppContextSwitchHelper.GetConfigValue(Constants.DisableExpectContinueHeaderSwitchName, Constants.DisableExpectContinueHeaderEnvVar); + + private static bool? _useLegacyDefaultConcurrency; + + public static bool UseLegacyDefaultConcurrency => _useLegacyDefaultConcurrency + ??= AppContextSwitchHelper.GetConfigValue(Constants.UseLegacyDefaultConcurrencySwitchName, Constants.UseLegacyDefaultConcurrencyEnvVar); } } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs index 364b563d2fba..07ddd8ddb1fd 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Azure.Storage { @@ -25,7 +26,7 @@ internal static class Constants /// Gets the default service version to use when building shared access /// signatures. /// - public const string DefaultSasVersion = "2026-02-06"; + public const string DefaultSasVersion = "2026-04-06"; /// /// Max download range size while requesting a transactional hash. @@ -129,6 +130,9 @@ internal static class Constants public const string DisableExpectContinueHeaderSwitchName = "Azure.Storage.DisableExpectContinueHeader"; public const string DisableExpectContinueHeaderEnvVar = "AZURE_STORAGE_DISABLE_EXPECT_CONTINUE_HEADER"; + public const string UseLegacyDefaultConcurrencySwitchName = "Azure.Storage.UseLegacyDefaultConcurrency"; + public const string UseLegacyDefaultConcurrencyEnvVar = "AZURE_STORAGE_USE_LEGACY_DEFAULT_CONCURRENCY"; + public const string DefaultScope = "/.default"; /// @@ -241,7 +245,9 @@ internal static class Append internal static class Block { - public const int DefaultConcurrentTransfersCount = 5; + [EditorBrowsable(EditorBrowsableState.Never)] + public const int DefaultConcurrentTransfersCount = LegacyDefaultConcurrentTransfersCount; + public const int LegacyDefaultConcurrentTransfersCount = 5; public const int DefaultInitalDownloadRangeSize = 256 * Constants.MB; // 256 MB public const int Pre_2019_12_12_MaxUploadBytes = 256 * Constants.MB; // 256 MB public const long MaxUploadBytes = 5000L * Constants.MB; // 5000MB @@ -378,7 +384,13 @@ internal static class DataLake /// /// Default concurrent transfers count. /// - public const int DefaultConcurrentTransfersCount = 5; + [EditorBrowsable(EditorBrowsableState.Never)] + public const int DefaultConcurrentTransfersCount = LegacyDefaultConcurrentTransfersCount; + + /// + /// Legacy default concurrent transfers count. + /// + public const int LegacyDefaultConcurrentTransfersCount = 5; /// /// Max upload bytes for less than Service Version 2019-12-12. @@ -543,6 +555,8 @@ internal static class Sas { public const string ObjectId = "oid"; + public const string TenantId = "tid"; + internal static class Permissions { public const char Read = 'r'; @@ -600,6 +614,8 @@ internal static class Parameters public const string KeyServiceUpper = "SKS"; public const string KeyVersion = "skv"; public const string KeyVersionUpper = "SKV"; + public const string KeyDelegatedUserTenantId = "skdutid"; + public const string KeyDelegatedUserTenantIdUpper = "SKDUTID"; public const string CacheControl = "rscc"; public const string CacheControlUpper = "RSCC"; public const string ContentDisposition = "rscd"; @@ -622,6 +638,10 @@ internal static class Parameters public const string EncryptionScopeUpper = "SES"; public const string DelegatedUserObjectId = "sduoid"; public const string DelegatedUserObjectIdUpper = "SDUOID"; + public const string RequestHeaders = "srh"; + public const string RequestHeadersUpper = "SRH"; + public const string RequestQueryParameters = "srq"; + public const string RequestQueryParametersUpper = "SRQ"; } internal static class Resource @@ -677,6 +697,15 @@ internal static class AccountResources internal static readonly int[] PathStylePorts = { 10000, 10001, 10002, 10003, 10004, 10100, 10101, 10102, 10103, 10104, 11000, 11001, 11002, 11003, 11004, 11100, 11101, 11102, 11103, 11104 }; } + internal static class StructuredMessage + { + public const string StructuredMessageHeader = "x-ms-structured-body"; + public const string StructuredContentLength = "x-ms-structured-content-length"; + public const string CrcStructuredMessage = "XSM/1.0; properties=crc64"; + public const int DefaultSegmentContentLength = 4 * MB; + public const int MaxDownloadCrcWithHeader = 4 * MB; + } + internal static class ClientSideEncryption { public const string HttpMessagePropertyKeyV1 = "Azure.Storage.StorageTelemetryPolicy.ClientSideEncryption.V1"; diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/ContentRange.cs b/sdk/storage/Azure.Storage.Common/src/Shared/ContentRange.cs index 35bccf87d76c..d60c1f46be2f 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/ContentRange.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/ContentRange.cs @@ -82,20 +82,20 @@ public RangeUnit(string value) public long? End { get; } /// - /// Size of this range, measured in this instance's . + /// Size of the entire resource this range is from, measured in this instance's . /// - public long? Size { get; } + public long? TotalResourceLength { get; } /// /// Unit this range is measured in. Generally "bytes". /// public RangeUnit Unit { get; } - public ContentRange(RangeUnit unit, long? start, long? end, long? size) + public ContentRange(RangeUnit unit, long? start, long? end, long? totalResourceLength) { Start = start; End = end; - Size = size; + TotalResourceLength = totalResourceLength; Unit = unit; } @@ -113,7 +113,7 @@ public static ContentRange Parse(string headerValue) string unit = default; long? start = default; long? end = default; - long? size = default; + long? resourceSize = default; try { @@ -136,10 +136,10 @@ public static ContentRange Parse(string headerValue) var rawSize = tokens[blobSizeIndex]; if (rawSize != WildcardMarker) { - size = long.Parse(rawSize, CultureInfo.InvariantCulture); + resourceSize = long.Parse(rawSize, CultureInfo.InvariantCulture); } - return new ContentRange(unit, start, end, size); + return new ContentRange(unit, start, end, resourceSize); } catch (IndexOutOfRangeException) { @@ -166,7 +166,7 @@ public static HttpRange ToHttpRange(ContentRange contentRange) /// /// Indicates whether this instance and a specified are equal /// - public bool Equals(ContentRange other) => (other.Start == Start) && (other.End == End) && (other.Unit == Unit) && (other.Size == Size); + public bool Equals(ContentRange other) => (other.Start == Start) && (other.End == End) && (other.Unit == Unit) && (other.TotalResourceLength == TotalResourceLength); /// /// Determines if two values are the same. @@ -186,6 +186,6 @@ public static HttpRange ToHttpRange(ContentRange contentRange) /// [EditorBrowsable(EditorBrowsableState.Never)] - public override int GetHashCode() => HashCodeBuilder.Combine(Start, End, Size, Unit.GetHashCode()); + public override int GetHashCode() => HashCodeBuilder.Combine(Start, End, TotalResourceLength, Unit.GetHashCode()); } } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/ContentRangeExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/ContentRangeExtensions.cs new file mode 100644 index 000000000000..160a69b19a9c --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/ContentRangeExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Storage.Cryptography; + +internal static class ContentRangeExtensions +{ + public static long? GetContentRangeLengthOrDefault(this string contentRange) + => string.IsNullOrWhiteSpace(contentRange) + ? default : ContentRange.Parse(contentRange).GetRangeLength(); + + public static long GetRangeLength(this ContentRange contentRange) + => contentRange.End.Value - contentRange.Start.Value + 1; +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs index d112219c9a5f..5701fb22c06e 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.IO; using System.Linq; using System.Security.Authentication; using System.Xml.Serialization; @@ -114,9 +115,18 @@ public static ArgumentException VersionNotSupported(string paramName) public static RequestFailedException ClientRequestIdMismatch(Response response, string echo, string original) => new RequestFailedException(response.Status, $"Response x-ms-client-request-id '{echo}' does not match the original expected request id, '{original}'.", null); + public static InvalidDataException StructuredMessageNotAcknowledgedGET(Response response) + => new InvalidDataException($"Response does not acknowledge structured message was requested. Unknown data structure in response body."); + + public static InvalidDataException StructuredMessageNotAcknowledgedPUT(Response response) + => new InvalidDataException($"Response does not acknowledge structured message was sent. Unexpected data may have been persisted to storage."); + public static ArgumentException TransactionalHashingNotSupportedWithClientSideEncryption() => new ArgumentException("Client-side encryption and transactional hashing are not supported at the same time."); + public static InvalidDataException ExpectedStructuredMessage() + => new InvalidDataException($"Expected {Constants.StructuredMessage.StructuredMessageHeader} in response, but found none."); + public static void VerifyHttpsTokenAuth(Uri uri) { if (uri.Scheme != Constants.Https) diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Errors.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Errors.cs index fa531e9a274e..1c708a08110b 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Errors.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Errors.cs @@ -72,6 +72,9 @@ public static ArgumentException CannotDeferTransactionalHashVerification() public static ArgumentException CannotInitializeWriteStreamWithData() => new ArgumentException("Initialized buffer for StorageWriteStream must be empty."); + public static InvalidDataException InvalidStructuredMessage(string optionalMessage = default) + => new InvalidDataException(("Invalid structured message data. " + optionalMessage ?? "").Trim()); + internal static void VerifyStreamPosition(Stream stream, string streamName) { if (stream != null && stream.CanSeek && stream.Length > 0 && stream.Position >= stream.Length) @@ -80,6 +83,22 @@ internal static void VerifyStreamPosition(Stream stream, string streamName) } } + internal static void AssertBufferMinimumSize(ReadOnlySpan buffer, int minSize, string paramName) + { + if (buffer.Length < minSize) + { + throw new ArgumentException($"Expected buffer Length of at least {minSize} bytes. Got {buffer.Length}.", paramName); + } + } + + internal static void AssertBufferExactSize(ReadOnlySpan buffer, int size, string paramName) + { + if (buffer.Length != size) + { + throw new ArgumentException($"Expected buffer Length of exactly {size} bytes. Got {buffer.Length}.", paramName); + } + } + public static void ThrowIfParamNull(object obj, string paramName) { if (obj == null) diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/LazyLoadingReadOnlyStream.cs b/sdk/storage/Azure.Storage.Common/src/Shared/LazyLoadingReadOnlyStream.cs index e5f6e57a942d..1e21eaf5c448 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/LazyLoadingReadOnlyStream.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/LazyLoadingReadOnlyStream.cs @@ -254,41 +254,9 @@ private async Task DownloadInternal(bool async, CancellationToken cancellat response = await _downloadInternalFunc(range, _validationOptions, async, cancellationToken).ConfigureAwait(false); using Stream networkStream = response.Value.Content; - - // The number of bytes we just downloaded. - long downloadSize = GetResponseRange(response.GetRawResponse()).Length.Value; - - // The number of bytes we copied in the last loop. - int copiedBytes; - - // Bytes we have copied so far. - int totalCopiedBytes = 0; - - // Bytes remaining to copy. It is save to truncate the long because we asked for a max of int _buffer size bytes. - int remainingBytes = (int)downloadSize; - - do - { - if (async) - { - copiedBytes = await networkStream.ReadAsync( - buffer: _buffer, - offset: totalCopiedBytes, - count: remainingBytes, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - else - { - copiedBytes = networkStream.Read( - buffer: _buffer, - offset: totalCopiedBytes, - count: remainingBytes); - } - - totalCopiedBytes += copiedBytes; - remainingBytes -= copiedBytes; - } - while (copiedBytes != 0); + // use stream copy to ensure consumption of any trailing metadata (e.g. structured message) + // allow buffer limits to catch the error of data size mismatch + int totalCopiedBytes = (int) await networkStream.CopyToInternal(new MemoryStream(_buffer), async, cancellationToken).ConfigureAwait((false)); _bufferPosition = 0; _bufferLength = totalCopiedBytes; @@ -296,7 +264,7 @@ private async Task DownloadInternal(bool async, CancellationToken cancellat // if we deferred transactional hash validation on download, validate now // currently we always defer but that may change - if (_validationOptions != default && _validationOptions.ChecksumAlgorithm != StorageChecksumAlgorithm.None && !_validationOptions.AutoValidateChecksum) + if (_validationOptions != default && _validationOptions.ChecksumAlgorithm == StorageChecksumAlgorithm.MD5 && !_validationOptions.AutoValidateChecksum) // TODO better condition { ContentHasher.AssertResponseHashMatch(_buffer, _bufferPosition, _bufferLength, _validationOptions.ChecksumAlgorithm, response.GetRawResponse()); } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/PartitionedUploader.cs b/sdk/storage/Azure.Storage.Common/src/Shared/PartitionedUploader.cs index 2107ec374595..6cd8f9dc5e81 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/PartitionedUploader.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/PartitionedUploader.cs @@ -203,6 +203,11 @@ public struct Behaviors /// private readonly string _operationName; + /// + /// The default conconcurrency transfer count. + /// + private readonly int DefaultConcurrentTransfersCount = Math.Min(Math.Max(Environment.ProcessorCount * 2, 8), 32); + public PartitionedUploader( Behaviors behaviors, StorageTransferOptions transferOptions, @@ -235,7 +240,9 @@ public PartitionedUploader( } else { - _maxWorkerCount = Constants.Blob.Block.DefaultConcurrentTransfersCount; + _maxWorkerCount = CompatSwitches.UseLegacyDefaultConcurrency + ? Constants.Blob.Block.LegacyDefaultConcurrentTransfersCount + : DefaultConcurrentTransfersCount; } // Set _singleUploadThreshold diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs index 4f883ec03970..8491e6dddd44 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs @@ -188,6 +188,56 @@ internal static string FormatTimesForSasSigning(DateTimeOffset time) => // "yyyy-MM-ddTHH:mm:ssZ" (time == new DateTimeOffset()) ? "" : time.ToString(Constants.SasTimeFormatSeconds, CultureInfo.InvariantCulture); + internal static string FormatRequestHeadersForSasSigning(Dictionary requestHeaders) + { + if (requestHeaders == null || requestHeaders.Count == 0) + { + return null; + } + StringBuilder sb = new StringBuilder(); + foreach (var entry in requestHeaders) + { + sb + .Append(entry.Key) + .Append(':') + .Append(entry.Value) + .Append('\n'); + } + return sb.ToString(); + } + + internal static string FormatRequestQueryParametersForSasSigning(Dictionary requestQueryParameters) + { + if (requestQueryParameters == null || requestQueryParameters.Count == 0) + { + return null; + } + StringBuilder sb = new StringBuilder(); + foreach (var entry in requestQueryParameters) + { + sb + .Append('\n') + .Append(entry.Key) + .Append(':') + .Append(entry.Value); + } + return sb.ToString(); + } + + internal static List ConvertRequestDictToKeyList(Dictionary dict) + { + if (dict == null) + { + return null; + } + List list = new List(); + foreach (var kvp in dict) + { + list.Add(kvp.Key); + } + return list; + } + /// /// Helper method to add query param key value pairs to StringBuilder /// diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs index 5b6155c08d7f..8ffc858c05f6 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs @@ -61,6 +61,9 @@ internal static void ParseKeyProperties( case Constants.Sas.Parameters.KeyVersionUpper: parameters.KeyProperties.Version = kv.Value; break; + case Constants.Sas.Parameters.KeyDelegatedUserTenantIdUpper: + parameters.KeyProperties.DelegatedUserTenantId = kv.Value; + break; default: isSasKey = false; diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageCrc64Composer.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageCrc64Composer.cs index ab6b76d78a87..307ff23b2114 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageCrc64Composer.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageCrc64Composer.cs @@ -12,22 +12,52 @@ namespace Azure.Storage /// internal static class StorageCrc64Composer { - public static Memory Compose(params (byte[] Crc64, long OriginalDataLength)[] partitions) + public static byte[] Compose(params (byte[] Crc64, long OriginalDataLength)[] partitions) + => Compose(partitions.AsEnumerable()); + + public static byte[] Compose(IEnumerable<(byte[] Crc64, long OriginalDataLength)> partitions) { - return Compose(partitions.AsEnumerable()); + ulong result = Compose(partitions.Select(tup => (BitConverter.ToUInt64(tup.Crc64, 0), tup.OriginalDataLength))); + return BitConverter.GetBytes(result); } - public static Memory Compose(IEnumerable<(byte[] Crc64, long OriginalDataLength)> partitions) + public static byte[] Compose(params (ReadOnlyMemory Crc64, long OriginalDataLength)[] partitions) + => Compose(partitions.AsEnumerable()); + + public static byte[] Compose(IEnumerable<(ReadOnlyMemory Crc64, long OriginalDataLength)> partitions) { - ulong result = Compose(partitions.Select(tup => (BitConverter.ToUInt64(tup.Crc64, 0), tup.OriginalDataLength))); - return new Memory(BitConverter.GetBytes(result)); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + ulong result = Compose(partitions.Select(tup => (BitConverter.ToUInt64(tup.Crc64.Span), tup.OriginalDataLength))); +#else + ulong result = Compose(partitions.Select(tup => (System.BitConverter.ToUInt64(tup.Crc64.ToArray(), 0), tup.OriginalDataLength))); +#endif + return BitConverter.GetBytes(result); } + public static byte[] Compose( + ReadOnlySpan leftCrc64, long leftOriginalDataLength, + ReadOnlySpan rightCrc64, long rightOriginalDataLength) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + ulong result = Compose( + (BitConverter.ToUInt64(leftCrc64), leftOriginalDataLength), + (BitConverter.ToUInt64(rightCrc64), rightOriginalDataLength)); +#else + ulong result = Compose( + (BitConverter.ToUInt64(leftCrc64.ToArray(), 0), leftOriginalDataLength), + (BitConverter.ToUInt64(rightCrc64.ToArray(), 0), rightOriginalDataLength)); +#endif + return BitConverter.GetBytes(result); + } + + public static ulong Compose(params (ulong Crc64, long OriginalDataLength)[] partitions) + => Compose(partitions.AsEnumerable()); + public static ulong Compose(IEnumerable<(ulong Crc64, long OriginalDataLength)> partitions) { ulong composedCrc = 0; long composedDataLength = 0; - foreach (var tup in partitions) + foreach ((ulong crc64, long originalDataLength) in partitions) { composedCrc = StorageCrc64Calculator.Concatenate( uInitialCrcAB: 0, @@ -35,9 +65,9 @@ public static ulong Compose(IEnumerable<(ulong Crc64, long OriginalDataLength)> uFinalCrcA: composedCrc, uSizeA: (ulong) composedDataLength, uInitialCrcB: 0, - uFinalCrcB: tup.Crc64, - uSizeB: (ulong)tup.OriginalDataLength); - composedDataLength += tup.OriginalDataLength; + uFinalCrcB: crc64, + uSizeB: (ulong)originalDataLength); + composedDataLength += originalDataLength; } return composedCrc; } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageRequestValidationPipelinePolicy.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageRequestValidationPipelinePolicy.cs index 0cef4f4d8d4e..9f4ddb5249e8 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageRequestValidationPipelinePolicy.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageRequestValidationPipelinePolicy.cs @@ -33,6 +33,35 @@ public override void OnReceivedResponse(HttpMessage message) { throw Errors.ClientRequestIdMismatch(message.Response, echo.First(), original); } + + if (message.Request.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader) && + message.Request.Headers.Contains(Constants.StructuredMessage.StructuredContentLength)) + { + AssertStructuredMessageAcknowledgedPUT(message); + } + else if (message.Request.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) + { + AssertStructuredMessageAcknowledgedGET(message); + } + } + + private static void AssertStructuredMessageAcknowledgedPUT(HttpMessage message) + { + if (!message.Response.IsError && + !message.Response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) + { + throw Errors.StructuredMessageNotAcknowledgedPUT(message.Response); + } + } + + private static void AssertStructuredMessageAcknowledgedGET(HttpMessage message) + { + if (!message.Response.IsError && + !(message.Response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader) && + message.Response.Headers.Contains(Constants.StructuredMessage.StructuredContentLength))) + { + throw Errors.StructuredMessageNotAcknowledgedGET(message.Response); + } } } } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageVersionExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageVersionExtensions.cs index ef7457121333..b9ace8dc8325 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageVersionExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageVersionExtensions.cs @@ -56,7 +56,7 @@ internal static class StorageVersionExtensions /// internal const ServiceVersion MaxVersion = #if BlobSDK || QueueSDK || FileSDK || DataLakeSDK || ChangeFeedSDK || DataMovementSDK || BlobDataMovementSDK || ShareDataMovementSDK - ServiceVersion.V2026_02_06; + ServiceVersion.V2026_04_06; #else ERROR_STORAGE_SERVICE_NOT_DEFINED; #endif @@ -99,6 +99,7 @@ public static string ToVersionString(this ServiceVersion version) => ServiceVersion.V2025_07_05 => "2025-07-05", ServiceVersion.V2025_11_05 => "2025-11-05", ServiceVersion.V2026_02_06 => "2026-02-06", + ServiceVersion.V2026_04_06 => "2026-04-06", #endif _ => throw Errors.VersionNotSupported(nameof(version)) }; @@ -170,6 +171,8 @@ public static Azure.Storage.Blobs.BlobClientOptions.ServiceVersion AsBlobsVersio Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2025_11_05, Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion.V2026_02_06 => Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2026_02_06, + Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion.V2026_04_06 => + Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2026_04_06, _ => throw Errors.VersionNotSupported(nameof(version)) }; #endif diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StreamExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StreamExtensions.cs index e899ba2eb695..6195dfc94149 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StreamExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StreamExtensions.cs @@ -52,7 +52,7 @@ public static async Task WriteInternal( } } - public static Task CopyToInternal( + public static Task CopyToInternal( this Stream src, Stream dest, bool async, @@ -83,21 +83,33 @@ public static Task CopyToInternal( /// Cancellation token for the operation. /// /// - public static async Task CopyToInternal( + public static async Task CopyToInternal( this Stream src, Stream dest, int bufferSize, bool async, CancellationToken cancellationToken) { + using IDisposable _ = ArrayPool.Shared.RentDisposable(bufferSize, out byte[] buffer); + long totalRead = 0; + int read; if (async) { - await src.CopyToAsync(dest, bufferSize, cancellationToken).ConfigureAwait(false); + while (0 < (read = await src.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false))) + { + totalRead += read; + await dest.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); + } } else { - src.CopyTo(dest, bufferSize); + while (0 < (read = src.Read(buffer, 0, buffer.Length))) + { + totalRead += read; + dest.Write(buffer, 0, read); + } } + return totalRead; } public static async Task CopyToExactInternal( diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessage.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessage.cs new file mode 100644 index 000000000000..a0a46837797b --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessage.cs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using Azure.Storage.Common; + +namespace Azure.Storage.Shared; + +internal static class StructuredMessage +{ + public const int Crc64Length = 8; + + [Flags] + public enum Flags + { + None = 0, + StorageCrc64 = 1, + } + + public static class V1_0 + { + public const byte MessageVersionByte = 1; + + public const int StreamHeaderLength = 13; + public const int StreamHeaderVersionOffset = 0; + public const int StreamHeaderMessageLengthOffset = 1; + public const int StreamHeaderFlagsOffset = 9; + public const int StreamHeaderSegmentCountOffset = 11; + + public const int SegmentHeaderLength = 10; + public const int SegmentHeaderNumOffset = 0; + public const int SegmentHeaderContentLengthOffset = 2; + + #region Stream Header + public static void ReadStreamHeader( + ReadOnlySpan buffer, + out long messageLength, + out Flags flags, + out int totalSegments) + { + Errors.AssertBufferExactSize(buffer, 13, nameof(buffer)); + if (buffer[StreamHeaderVersionOffset] != 1) + { + throw new InvalidDataException("Unrecognized version of structured message."); + } + messageLength = (long)BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(StreamHeaderMessageLengthOffset, 8)); + flags = (Flags)BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(StreamHeaderFlagsOffset, 2)); + totalSegments = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(StreamHeaderSegmentCountOffset, 2)); + } + + public static int WriteStreamHeader( + Span buffer, + long messageLength, + Flags flags, + int totalSegments) + { + const int versionOffset = 0; + const int messageLengthOffset = 1; + const int flagsOffset = 9; + const int numSegmentsOffset = 11; + + Errors.AssertBufferMinimumSize(buffer, StreamHeaderLength, nameof(buffer)); + + buffer[versionOffset] = MessageVersionByte; + BinaryPrimitives.WriteUInt64LittleEndian(buffer.Slice(messageLengthOffset, 8), (ulong)messageLength); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(flagsOffset, 2), (ushort)flags); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(numSegmentsOffset, 2), (ushort)totalSegments); + + return StreamHeaderLength; + } + + /// + /// Gets stream header in a buffer rented from the provided ArrayPool. + /// + /// + /// Disposable to return the buffer to the pool. + /// + public static IDisposable GetStreamHeaderBytes( + ArrayPool pool, + out Memory bytes, + long messageLength, + Flags flags, + int totalSegments) + { + Argument.AssertNotNull(pool, nameof(pool)); + IDisposable disposable = pool.RentAsMemoryDisposable(StreamHeaderLength, out bytes); + WriteStreamHeader(bytes.Span, messageLength, flags, totalSegments); + return disposable; + } + #endregion + + #region StreamFooter + public static int GetStreamFooterSize(Flags flags) + => flags.HasFlag(Flags.StorageCrc64) ? Crc64Length : 0; + + public static void ReadStreamFooter( + ReadOnlySpan buffer, + Flags flags, + out ulong crc64) + { + int expectedBufferSize = GetSegmentFooterSize(flags); + Errors.AssertBufferExactSize(buffer, expectedBufferSize, nameof(buffer)); + + crc64 = flags.HasFlag(Flags.StorageCrc64) ? buffer.ReadCrc64() : default; + } + + public static int WriteStreamFooter(Span buffer, ReadOnlySpan crc64 = default) + { + int requiredSpace = 0; + if (!crc64.IsEmpty) + { + Errors.AssertBufferExactSize(crc64, Crc64Length, nameof(crc64)); + requiredSpace += Crc64Length; + } + + Errors.AssertBufferMinimumSize(buffer, requiredSpace, nameof(buffer)); + int offset = 0; + if (!crc64.IsEmpty) + { + crc64.CopyTo(buffer.Slice(offset, Crc64Length)); + offset += Crc64Length; + } + + return offset; + } + + /// + /// Gets stream header in a buffer rented from the provided ArrayPool. + /// + /// + /// Disposable to return the buffer to the pool. + /// + public static IDisposable GetStreamFooterBytes( + ArrayPool pool, + out Memory bytes, + ReadOnlySpan crc64 = default) + { + Argument.AssertNotNull(pool, nameof(pool)); + IDisposable disposable = pool.RentAsMemoryDisposable(StreamHeaderLength, out bytes); + WriteStreamFooter(bytes.Span, crc64); + return disposable; + } + #endregion + + #region SegmentHeader + public static void ReadSegmentHeader( + ReadOnlySpan buffer, + out int segmentNum, + out long contentLength) + { + Errors.AssertBufferExactSize(buffer, 10, nameof(buffer)); + segmentNum = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2)); + contentLength = (long)BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(2, 8)); + } + + public static int WriteSegmentHeader(Span buffer, int segmentNum, long segmentLength) + { + const int segmentNumOffset = 0; + const int segmentLengthOffset = 2; + + Errors.AssertBufferMinimumSize(buffer, SegmentHeaderLength, nameof(buffer)); + + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(segmentNumOffset, 2), (ushort)segmentNum); + BinaryPrimitives.WriteUInt64LittleEndian(buffer.Slice(segmentLengthOffset, 8), (ulong)segmentLength); + + return SegmentHeaderLength; + } + + /// + /// Gets segment header in a buffer rented from the provided ArrayPool. + /// + /// + /// Disposable to return the buffer to the pool. + /// + public static IDisposable GetSegmentHeaderBytes( + ArrayPool pool, + out Memory bytes, + int segmentNum, + long segmentLength) + { + Argument.AssertNotNull(pool, nameof(pool)); + IDisposable disposable = pool.RentAsMemoryDisposable(SegmentHeaderLength, out bytes); + WriteSegmentHeader(bytes.Span, segmentNum, segmentLength); + return disposable; + } + #endregion + + #region SegmentFooter + public static int GetSegmentFooterSize(Flags flags) + => flags.HasFlag(Flags.StorageCrc64) ? Crc64Length : 0; + + public static void ReadSegmentFooter( + ReadOnlySpan buffer, + Flags flags, + out ulong crc64) + { + int expectedBufferSize = GetSegmentFooterSize(flags); + Errors.AssertBufferExactSize(buffer, expectedBufferSize, nameof(buffer)); + + crc64 = flags.HasFlag(Flags.StorageCrc64) ? buffer.ReadCrc64() : default; + } + + public static int WriteSegmentFooter(Span buffer, ReadOnlySpan crc64 = default) + { + int requiredSpace = 0; + if (!crc64.IsEmpty) + { + Errors.AssertBufferExactSize(crc64, Crc64Length, nameof(crc64)); + requiredSpace += Crc64Length; + } + + Errors.AssertBufferMinimumSize(buffer, requiredSpace, nameof(buffer)); + int offset = 0; + if (!crc64.IsEmpty) + { + crc64.CopyTo(buffer.Slice(offset, Crc64Length)); + offset += Crc64Length; + } + + return offset; + } + + /// + /// Gets stream header in a buffer rented from the provided ArrayPool. + /// + /// + /// Disposable to return the buffer to the pool. + /// + public static IDisposable GetSegmentFooterBytes( + ArrayPool pool, + out Memory bytes, + ReadOnlySpan crc64 = default) + { + Argument.AssertNotNull(pool, nameof(pool)); + IDisposable disposable = pool.RentAsMemoryDisposable(StreamHeaderLength, out bytes); + WriteSegmentFooter(bytes.Span, crc64); + return disposable; + } + #endregion + } +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingRetriableStream.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingRetriableStream.cs new file mode 100644 index 000000000000..22dfaef25997 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingRetriableStream.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Storage.Shared; + +internal class StructuredMessageDecodingRetriableStream : Stream +{ + public class DecodedData + { + public ulong Crc { get; set; } + } + + private readonly Stream _innerRetriable; + private long _decodedBytesRead; + + private readonly StructuredMessage.Flags _expectedFlags; + private readonly List _decodedDatas; + private readonly Action _onComplete; + + private StorageCrc64HashAlgorithm _totalContentCrc; + + private readonly Func _decodingStreamFactory; + private readonly Func> _decodingAsyncStreamFactory; + + public StructuredMessageDecodingRetriableStream( + Stream initialDecodingStream, + StructuredMessageDecodingStream.RawDecodedData initialDecodedData, + StructuredMessage.Flags expectedFlags, + Func decodingStreamFactory, + Func> decodingAsyncStreamFactory, + Action onComplete, + ResponseClassifier responseClassifier, + int maxRetries) + { + _decodingStreamFactory = decodingStreamFactory; + _decodingAsyncStreamFactory = decodingAsyncStreamFactory; + _innerRetriable = RetriableStream.Create(initialDecodingStream, StreamFactory, StreamFactoryAsync, responseClassifier, maxRetries); + _decodedDatas = new() { initialDecodedData }; + _expectedFlags = expectedFlags; + _onComplete = onComplete; + + if (expectedFlags.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + _totalContentCrc = StorageCrc64HashAlgorithm.Create(); + } + } + + private Stream StreamFactory(long _) + { + long offset = _decodedDatas.SelectMany(d => d.SegmentCrcs).Select(s => s.SegmentLen).Sum(); + (Stream decodingStream, StructuredMessageDecodingStream.RawDecodedData decodedData) = _decodingStreamFactory(offset); + _decodedDatas.Add(decodedData); + FastForwardInternal(decodingStream, _decodedBytesRead - offset, false).EnsureCompleted(); + return decodingStream; + } + + private async ValueTask StreamFactoryAsync(long _) + { + long offset = _decodedDatas.SelectMany(d => d.SegmentCrcs).Select(s => s.SegmentLen).Sum(); + (Stream decodingStream, StructuredMessageDecodingStream.RawDecodedData decodedData) = await _decodingAsyncStreamFactory(offset).ConfigureAwait(false); + _decodedDatas.Add(decodedData); + await FastForwardInternal(decodingStream, _decodedBytesRead - offset, true).ConfigureAwait(false); + return decodingStream; + } + + private static async ValueTask FastForwardInternal(Stream stream, long bytes, bool async) + { + using (ArrayPool.Shared.RentDisposable(4 * Constants.KB, out byte[] buffer)) + { + if (async) + { + while (bytes > 0) + { + bytes -= await stream.ReadAsync(buffer, 0, (int)Math.Min(bytes, buffer.Length)).ConfigureAwait(false); + } + } + else + { + while (bytes > 0) + { + bytes -= stream.Read(buffer, 0, (int)Math.Min(bytes, buffer.Length)); + } + } + } + } + + protected override void Dispose(bool disposing) + { + _decodedDatas.Clear(); + _innerRetriable.Dispose(); + } + + private void OnCompleted() + { + DecodedData final = new(); + if (_totalContentCrc != null) + { + final.Crc = ValidateCrc(); + } + _onComplete?.Invoke(final); + } + + private ulong ValidateCrc() + { + using IDisposable _ = ArrayPool.Shared.RentDisposable(StructuredMessage.Crc64Length * 2, out byte[] buf); + Span calculatedBytes = new(buf, 0, StructuredMessage.Crc64Length); + _totalContentCrc.GetCurrentHash(calculatedBytes); + ulong calculated = BinaryPrimitives.ReadUInt64LittleEndian(calculatedBytes); + + ulong reported = _decodedDatas.Count == 1 + ? _decodedDatas.First().TotalCrc.Value + : StorageCrc64Composer.Compose(_decodedDatas.SelectMany(d => d.SegmentCrcs)); + + if (calculated != reported) + { + Span reportedBytes = new(buf, calculatedBytes.Length, StructuredMessage.Crc64Length); + BinaryPrimitives.WriteUInt64LittleEndian(reportedBytes, reported); + throw Errors.ChecksumMismatch(calculatedBytes, reportedBytes); + } + + return calculated; + } + + #region Read + public override int Read(byte[] buffer, int offset, int count) + { + int read = _innerRetriable.Read(buffer, offset, count); + _decodedBytesRead += read; + if (read == 0) + { + OnCompleted(); + } + else + { + _totalContentCrc?.Append(new ReadOnlySpan(buffer, offset, read)); + } + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = await _innerRetriable.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + _decodedBytesRead += read; + if (read == 0) + { + OnCompleted(); + } + else + { + _totalContentCrc?.Append(new ReadOnlySpan(buffer, offset, read)); + } + return read; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override int Read(Span buffer) + { + int read = _innerRetriable.Read(buffer); + _decodedBytesRead += read; + if (read == 0) + { + OnCompleted(); + } + else + { + _totalContentCrc?.Append(buffer.Slice(0, read)); + } + return read; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int read = await _innerRetriable.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _decodedBytesRead += read; + if (read == 0) + { + OnCompleted(); + } + else + { + _totalContentCrc?.Append(buffer.Span.Slice(0, read)); + } + return read; + } +#endif + + public override int ReadByte() + { + int val = _innerRetriable.ReadByte(); + _decodedBytesRead += 1; + if (val == -1) + { + OnCompleted(); + } + return val; + } + + public override int EndRead(IAsyncResult asyncResult) + { + int read = _innerRetriable.EndRead(asyncResult); + _decodedBytesRead += read; + if (read == 0) + { + OnCompleted(); + } + return read; + } + #endregion + + #region Passthru + public override bool CanRead => _innerRetriable.CanRead; + + public override bool CanSeek => _innerRetriable.CanSeek; + + public override bool CanWrite => _innerRetriable.CanWrite; + + public override bool CanTimeout => _innerRetriable.CanTimeout; + + public override long Length => _innerRetriable.Length; + + public override long Position { get => _innerRetriable.Position; set => _innerRetriable.Position = value; } + + public override void Flush() => _innerRetriable.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => _innerRetriable.FlushAsync(cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) => _innerRetriable.Seek(offset, origin); + + public override void SetLength(long value) => _innerRetriable.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _innerRetriable.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerRetriable.WriteAsync(buffer, offset, count, cancellationToken); + + public override void WriteByte(byte value) => _innerRetriable.WriteByte(value); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => _innerRetriable.BeginWrite(buffer, offset, count, callback, state); + + public override void EndWrite(IAsyncResult asyncResult) => _innerRetriable.EndWrite(asyncResult); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => _innerRetriable.BeginRead(buffer, offset, count, callback, state); + + public override int ReadTimeout { get => _innerRetriable.ReadTimeout; set => _innerRetriable.ReadTimeout = value; } + + public override int WriteTimeout { get => _innerRetriable.WriteTimeout; set => _innerRetriable.WriteTimeout = value; } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override void Write(ReadOnlySpan buffer) => _innerRetriable.Write(buffer); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _innerRetriable.WriteAsync(buffer, cancellationToken); +#endif + #endregion +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingStream.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingStream.cs new file mode 100644 index 000000000000..e6b193ae1826 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageDecodingStream.cs @@ -0,0 +1,542 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.Storage.Common; + +namespace Azure.Storage.Shared; + +/// +/// Decodes a structured message stream as the data is read. +/// +/// +/// Wraps the inner stream in a , which avoids using its internal +/// buffer if individual Read() calls are larger than it. This ensures one of the three scenarios +/// +/// +/// Read buffer >= stream buffer: +/// There is enough space in the read buffer for inline metadata to be safely +/// extracted in only one read to the true inner stream. +/// +/// +/// Read buffer < next inline metadata: +/// The stream buffer has been activated, and we can read multiple small times from the inner stream +/// without multi-reading the real stream, even when partway through an existing stream buffer. +/// +/// +/// Else: +/// Same as #1, but also the already-allocated stream buffer has been used to slightly improve +/// resource churn when reading inner stream. +/// +/// +/// +internal class StructuredMessageDecodingStream : Stream +{ + internal class RawDecodedData + { + public long? InnerStreamLength { get; set; } + public int? TotalSegments { get; set; } + public StructuredMessage.Flags? Flags { get; set; } + public List<(ulong SegmentCrc, long SegmentLen)> SegmentCrcs { get; } = new(); + public ulong? TotalCrc { get; set; } + public bool DecodeCompleted { get; set; } + } + + private enum SMRegion + { + StreamHeader, + StreamFooter, + SegmentHeader, + SegmentFooter, + SegmentContent, + } + + private readonly Stream _innerBufferedStream; + + private byte[] _metadataBuffer = ArrayPool.Shared.Rent(Constants.KB); + private int _metadataBufferOffset = 0; + private int _metadataBufferLength = 0; + + private int _streamHeaderLength; + private int _streamFooterLength; + private int _segmentHeaderLength; + private int _segmentFooterLength; + + private long? _expectedInnerStreamLength; + + private bool _disposed; + + private readonly RawDecodedData _decodedData; + private StorageCrc64HashAlgorithm _totalContentCrc; + private StorageCrc64HashAlgorithm _segmentCrc; + + private readonly bool _validateChecksums; + + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override bool CanSeek => false; + + public override bool CanTimeout => _innerBufferedStream.CanTimeout; + + public override int ReadTimeout => _innerBufferedStream.ReadTimeout; + + public override int WriteTimeout => _innerBufferedStream.WriteTimeout; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public static (Stream DecodedStream, RawDecodedData DecodedData) WrapStream( + Stream innerStream, + long? expextedStreamLength = default) + { + RawDecodedData data = new(); + return (new StructuredMessageDecodingStream(innerStream, data, expextedStreamLength), data); + } + + private StructuredMessageDecodingStream( + Stream innerStream, + RawDecodedData decodedData, + long? expectedStreamLength) + { + Argument.AssertNotNull(innerStream, nameof(innerStream)); + Argument.AssertNotNull(decodedData, nameof(decodedData)); + + _expectedInnerStreamLength = expectedStreamLength; + _innerBufferedStream = new BufferedStream(innerStream); + _decodedData = decodedData; + + // Assumes stream will be structured message 1.0. Will validate this when consuming stream. + _streamHeaderLength = StructuredMessage.V1_0.StreamHeaderLength; + _segmentHeaderLength = StructuredMessage.V1_0.SegmentHeaderLength; + + _validateChecksums = true; + } + + #region Write + public override void Flush() => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + #endregion + + #region Read + public override int Read(byte[] buf, int offset, int count) + { + int decodedRead; + int read; + do + { + read = _innerBufferedStream.Read(buf, offset, count); + _innerStreamConsumed += read; + decodedRead = Decode(new Span(buf, offset, read)); + } while (decodedRead <= 0 && read > 0); + + if (read <= 0) + { + AssertDecodeFinished(); + } + + return decodedRead; + } + + public override async Task ReadAsync(byte[] buf, int offset, int count, CancellationToken cancellationToken) + { + int decodedRead; + int read; + do + { + read = await _innerBufferedStream.ReadAsync(buf, offset, count, cancellationToken).ConfigureAwait(false); + _innerStreamConsumed += read; + decodedRead = Decode(new Span(buf, offset, read)); + } while (decodedRead <= 0 && read > 0); + + if (read <= 0) + { + AssertDecodeFinished(); + } + + return decodedRead; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override int Read(Span buf) + { + int decodedRead; + int read; + do + { + read = _innerBufferedStream.Read(buf); + _innerStreamConsumed += read; + decodedRead = Decode(buf.Slice(0, read)); + } while (decodedRead <= 0 && read > 0); + + if (read <= 0) + { + AssertDecodeFinished(); + } + + return decodedRead; + } + + public override async ValueTask ReadAsync(Memory buf, CancellationToken cancellationToken = default) + { + int decodedRead; + int read; + do + { + read = await _innerBufferedStream.ReadAsync(buf).ConfigureAwait(false); + _innerStreamConsumed += read; + decodedRead = Decode(buf.Slice(0, read).Span); + } while (decodedRead <= 0 && read > 0); + + if (read <= 0) + { + AssertDecodeFinished(); + } + + return decodedRead; + } +#endif + + private void AssertDecodeFinished() + { + if (_streamFooterLength > 0 && !_decodedData.DecodeCompleted) + { + throw Errors.InvalidStructuredMessage("Premature end of stream."); + } + _decodedData.DecodeCompleted = true; + } + + private long _innerStreamConsumed = 0; + private long _decodedContentConsumed = 0; + private SMRegion _currentRegion = SMRegion.StreamHeader; + private int _currentSegmentNum = 0; + private long _currentSegmentContentLength; + private long _currentSegmentContentRemaining; + private long CurrentRegionLength => _currentRegion switch + { + SMRegion.StreamHeader => _streamHeaderLength, + SMRegion.StreamFooter => _streamFooterLength, + SMRegion.SegmentHeader => _segmentHeaderLength, + SMRegion.SegmentFooter => _segmentFooterLength, + SMRegion.SegmentContent => _currentSegmentContentLength, + _ => 0, + }; + + /// + /// Decodes given bytes in place. Decoding based on internal stream position info. + /// Decoded data size will be less than or equal to encoded data length. + /// + /// + /// Length of the decoded data in . + /// + private int Decode(Span buffer) + { + if (buffer.IsEmpty) + { + return 0; + } + List<(int Offset, int Count)> gaps = new(); + + int bufferConsumed = ProcessMetadataBuffer(buffer); + + if (bufferConsumed > 0) + { + gaps.Add((0, bufferConsumed)); + } + + while (bufferConsumed < buffer.Length) + { + if (_currentRegion == SMRegion.SegmentContent) + { + int read = (int)Math.Min(buffer.Length - bufferConsumed, _currentSegmentContentRemaining); + _totalContentCrc?.Append(buffer.Slice(bufferConsumed, read)); + _segmentCrc?.Append(buffer.Slice(bufferConsumed, read)); + bufferConsumed += read; + _decodedContentConsumed += read; + _currentSegmentContentRemaining -= read; + if (_currentSegmentContentRemaining == 0) + { + _currentRegion = SMRegion.SegmentFooter; + } + } + else if (buffer.Length - bufferConsumed < CurrentRegionLength) + { + SavePartialMetadata(buffer.Slice(bufferConsumed)); + gaps.Add((bufferConsumed, buffer.Length - bufferConsumed)); + bufferConsumed = buffer.Length; + } + else + { + int processed = _currentRegion switch + { + SMRegion.StreamHeader => ProcessStreamHeader(buffer.Slice(bufferConsumed)), + SMRegion.StreamFooter => ProcessStreamFooter(buffer.Slice(bufferConsumed)), + SMRegion.SegmentHeader => ProcessSegmentHeader(buffer.Slice(bufferConsumed)), + SMRegion.SegmentFooter => ProcessSegmentFooter(buffer.Slice(bufferConsumed)), + _ => 0, + }; + // TODO surface error if processed is 0 + gaps.Add((bufferConsumed, processed)); + bufferConsumed += processed; + } + } + + if (gaps.Count == 0) + { + return buffer.Length; + } + + // gaps is already sorted by offset due to how it was assembled + int gap = 0; + for (int i = gaps.First().Offset; i < buffer.Length; i++) + { + if (gaps.Count > 0 && gaps.First().Offset == i) + { + int count = gaps.First().Count; + gap += count; + i += count - 1; + gaps.RemoveAt(0); + } + else + { + buffer[i - gap] = buffer[i]; + } + } + return buffer.Length - gap; + } + + /// + /// Processes metadata in the internal buffer, if any. Appends any necessary data + /// from the append buffer to complete metadata. + /// + /// + /// Bytes consumed from . + /// + private int ProcessMetadataBuffer(ReadOnlySpan append) + { + if (_metadataBufferLength == 0) + { + return 0; + } + if (_currentRegion == SMRegion.SegmentContent) + { + return 0; + } + int appended = 0; + if (_metadataBufferLength < CurrentRegionLength && append.Length > 0) + { + appended = Math.Min((int)CurrentRegionLength - _metadataBufferLength, append.Length); + SavePartialMetadata(append.Slice(0, appended)); + } + if (_metadataBufferLength == CurrentRegionLength) + { + Span metadata = new(_metadataBuffer, _metadataBufferOffset, (int)CurrentRegionLength); + switch (_currentRegion) + { + case SMRegion.StreamHeader: + ProcessStreamHeader(metadata); + break; + case SMRegion.StreamFooter: + ProcessStreamFooter(metadata); + break; + case SMRegion.SegmentHeader: + ProcessSegmentHeader(metadata); + break; + case SMRegion.SegmentFooter: + ProcessSegmentFooter(metadata); + break; + } + _metadataBufferOffset = 0; + _metadataBufferLength = 0; + } + return appended; + } + + private void SavePartialMetadata(ReadOnlySpan span) + { + // safety array resize w/ArrayPool + if (_metadataBufferLength + span.Length > _metadataBuffer.Length) + { + ResizeMetadataBuffer(2 * (_metadataBufferLength + span.Length)); + } + + // realign any existing content if necessary + if (_metadataBufferLength != 0 && _metadataBufferOffset != 0) + { + // don't use Array.Copy() to move elements in the same array + for (int i = 0; i < _metadataBufferLength; i++) + { + _metadataBuffer[i] = _metadataBuffer[i + _metadataBufferOffset]; + } + _metadataBufferOffset = 0; + } + + span.CopyTo(new Span(_metadataBuffer, _metadataBufferOffset + _metadataBufferLength, span.Length)); + _metadataBufferLength += span.Length; + } + + private int ProcessStreamHeader(ReadOnlySpan span) + { + StructuredMessage.V1_0.ReadStreamHeader( + span.Slice(0, _streamHeaderLength), + out long streamLength, + out StructuredMessage.Flags flags, + out int totalSegments); + + _decodedData.InnerStreamLength = streamLength; + _decodedData.Flags = flags; + _decodedData.TotalSegments = totalSegments; + + if (_expectedInnerStreamLength.HasValue && _expectedInnerStreamLength.Value != streamLength) + { + throw Errors.InvalidStructuredMessage("Unexpected message size."); + } + + if (_decodedData.Flags.Value.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + _segmentFooterLength = StructuredMessage.Crc64Length; + _streamFooterLength = StructuredMessage.Crc64Length; + if (_validateChecksums) + { + _segmentCrc = StorageCrc64HashAlgorithm.Create(); + _totalContentCrc = StorageCrc64HashAlgorithm.Create(); + } + } + _currentRegion = SMRegion.SegmentHeader; + return _streamHeaderLength; + } + + private int ProcessStreamFooter(ReadOnlySpan span) + { + int footerLen = StructuredMessage.V1_0.GetStreamFooterSize(_decodedData.Flags.Value); + StructuredMessage.V1_0.ReadStreamFooter( + span.Slice(0, footerLen), + _decodedData.Flags.Value, + out ulong reportedCrc); + if (_decodedData.Flags.Value.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + if (_validateChecksums) + { + ValidateCrc64(_totalContentCrc, reportedCrc); + } + _decodedData.TotalCrc = reportedCrc; + } + + if (_innerStreamConsumed != _decodedData.InnerStreamLength) + { + throw Errors.InvalidStructuredMessage("Unexpected message size."); + } + if (_currentSegmentNum != _decodedData.TotalSegments) + { + throw Errors.InvalidStructuredMessage("Missing expected message segments."); + } + + _decodedData.DecodeCompleted = true; + return footerLen; + } + + private int ProcessSegmentHeader(ReadOnlySpan span) + { + StructuredMessage.V1_0.ReadSegmentHeader( + span.Slice(0, _segmentHeaderLength), + out int newSegNum, + out _currentSegmentContentLength); + _currentSegmentContentRemaining = _currentSegmentContentLength; + if (newSegNum != _currentSegmentNum + 1) + { + throw Errors.InvalidStructuredMessage("Unexpected segment number in structured message."); + } + _currentSegmentNum = newSegNum; + _currentRegion = SMRegion.SegmentContent; + return _segmentHeaderLength; + } + + private int ProcessSegmentFooter(ReadOnlySpan span) + { + int footerLen = StructuredMessage.V1_0.GetSegmentFooterSize(_decodedData.Flags.Value); + StructuredMessage.V1_0.ReadSegmentFooter( + span.Slice(0, footerLen), + _decodedData.Flags.Value, + out ulong reportedCrc); + if (_decodedData.Flags.Value.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + if (_validateChecksums) + { + ValidateCrc64(_segmentCrc, reportedCrc); + _segmentCrc = StorageCrc64HashAlgorithm.Create(); + } + _decodedData.SegmentCrcs.Add((reportedCrc, _currentSegmentContentLength)); + } + _currentRegion = _currentSegmentNum == _decodedData.TotalSegments ? SMRegion.StreamFooter : SMRegion.SegmentHeader; + return footerLen; + } + + private static void ValidateCrc64(StorageCrc64HashAlgorithm calculation, ulong reported) + { + using IDisposable _ = ArrayPool.Shared.RentDisposable(StructuredMessage.Crc64Length * 2, out byte[] buf); + Span calculatedBytes = new(buf, 0, StructuredMessage.Crc64Length); + Span reportedBytes = new(buf, calculatedBytes.Length, StructuredMessage.Crc64Length); + calculation.GetCurrentHash(calculatedBytes); + reported.WriteCrc64(reportedBytes); + if (!calculatedBytes.SequenceEqual(reportedBytes)) + { + throw Errors.ChecksumMismatch(calculatedBytes, reportedBytes); + } + } + #endregion + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_disposed) + { + return; + } + + if (disposing) + { + _innerBufferedStream.Dispose(); + _disposed = true; + } + } + + private void ResizeMetadataBuffer(int newSize) + { + byte[] newBuf = ArrayPool.Shared.Rent(newSize); + Array.Copy(_metadataBuffer, _metadataBufferOffset, newBuf, 0, _metadataBufferLength); + ArrayPool.Shared.Return(_metadataBuffer); + _metadataBuffer = newBuf; + } + + private void AlignMetadataBuffer() + { + if (_metadataBufferOffset != 0 && _metadataBufferLength != 0) + { + for (int i = 0; i < _metadataBufferLength; i++) + { + _metadataBuffer[i] = _metadataBuffer[_metadataBufferOffset + i]; + } + _metadataBufferOffset = 0; + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageEncodingStream.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageEncodingStream.cs new file mode 100644 index 000000000000..cb0ef340155e --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessageEncodingStream.cs @@ -0,0 +1,545 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Storage.Common; + +namespace Azure.Storage.Shared; + +internal class StructuredMessageEncodingStream : Stream +{ + private readonly Stream _innerStream; + + private readonly int _streamHeaderLength; + private readonly int _streamFooterLength; + private readonly int _segmentHeaderLength; + private readonly int _segmentFooterLength; + private readonly int _segmentContentLength; + + private readonly StructuredMessage.Flags _flags; + private bool _disposed; + + private bool UseCrcSegment => _flags.HasFlag(StructuredMessage.Flags.StorageCrc64); + private readonly StorageCrc64HashAlgorithm _totalCrc; + private StorageCrc64HashAlgorithm _segmentCrc; + private readonly byte[] _segmentCrcs; + private int _latestSegmentCrcd = 0; + + #region Segments + /// + /// Gets the 1-indexed segment number the underlying stream is currently positioned in. + /// 1-indexed to match segment labelling as specified by SM spec. + /// + private int CurrentInnerSegment => (int)Math.Floor(_innerStream.Position / (float)_segmentContentLength) + 1; + + /// + /// Gets the 1-indexed segment number the encoded data stream is currently positioned in. + /// 1-indexed to match segment labelling as specified by SM spec. + /// + private int CurrentEncodingSegment + { + get + { + // edge case: always on final segment when at end of inner stream + if (_innerStream.Position == _innerStream.Length) + { + return TotalSegments; + } + // when writing footer, inner stream is positioned at next segment, + // but this stream is still writing the previous one + if (_currentRegion == SMRegion.SegmentFooter) + { + return CurrentInnerSegment - 1; + } + return CurrentInnerSegment; + } + } + + /// + /// Segment length including header and footer. + /// + private int SegmentTotalLength => _segmentHeaderLength + _segmentContentLength + _segmentFooterLength; + + private int TotalSegments => GetTotalSegments(_innerStream, _segmentContentLength); + private static int GetTotalSegments(Stream innerStream, long segmentContentLength) + { + return (int)Math.Ceiling(innerStream.Length / (float)segmentContentLength); + } + #endregion + + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanTimeout => _innerStream.CanTimeout; + + public override int ReadTimeout => _innerStream.ReadTimeout; + + public override int WriteTimeout => _innerStream.WriteTimeout; + + public override long Length => + _streamHeaderLength + _streamFooterLength + + (_segmentHeaderLength + _segmentFooterLength) * TotalSegments + + _innerStream.Length; + + #region Position + private enum SMRegion + { + StreamHeader, + StreamFooter, + SegmentHeader, + SegmentFooter, + SegmentContent, + } + + private SMRegion _currentRegion = SMRegion.StreamHeader; + private int _currentRegionPosition = 0; + + private long _maxSeekPosition = 0; + + public override long Position + { + get + { + return _currentRegion switch + { + SMRegion.StreamHeader => _currentRegionPosition, + SMRegion.StreamFooter => _streamHeaderLength + + TotalSegments * (_segmentHeaderLength + _segmentFooterLength) + + _innerStream.Length + + _currentRegionPosition, + SMRegion.SegmentHeader => _innerStream.Position + + _streamHeaderLength + + (CurrentEncodingSegment - 1) * (_segmentHeaderLength + _segmentFooterLength) + + _currentRegionPosition, + SMRegion.SegmentFooter => _innerStream.Position + + _streamHeaderLength + + // Inner stream has moved to next segment but we're still writing the previous segment footer + CurrentEncodingSegment * (_segmentHeaderLength + _segmentFooterLength) - + _segmentFooterLength + _currentRegionPosition, + SMRegion.SegmentContent => _innerStream.Position + + _streamHeaderLength + + CurrentEncodingSegment * (_segmentHeaderLength + _segmentFooterLength) - + _segmentFooterLength, + _ => throw new InvalidDataException($"{nameof(StructuredMessageEncodingStream)} invalid state."), + }; + } + set + { + Argument.AssertInRange(value, 0, _maxSeekPosition, nameof(value)); + if (value < _streamHeaderLength) + { + _currentRegion = SMRegion.StreamHeader; + _currentRegionPosition = (int)value; + _innerStream.Position = 0; + return; + } + if (value >= Length - _streamFooterLength) + { + _currentRegion = SMRegion.StreamFooter; + _currentRegionPosition = (int)(value - (Length - _streamFooterLength)); + _innerStream.Position = _innerStream.Length; + return; + } + int newSegmentNum = 1 + (int)Math.Floor((value - _streamHeaderLength) / (double)(_segmentHeaderLength + _segmentFooterLength + _segmentContentLength)); + int segmentPosition = (int)(value - _streamHeaderLength - + ((newSegmentNum - 1) * (_segmentHeaderLength + _segmentFooterLength + _segmentContentLength))); + + if (segmentPosition < _segmentHeaderLength) + { + _currentRegion = SMRegion.SegmentHeader; + _currentRegionPosition = (int)((value - _streamHeaderLength) % SegmentTotalLength); + _innerStream.Position = (newSegmentNum - 1) * _segmentContentLength; + return; + } + if (segmentPosition < _segmentHeaderLength + _segmentContentLength) + { + _currentRegion = SMRegion.SegmentContent; + _currentRegionPosition = (int)((value - _streamHeaderLength) % SegmentTotalLength) - + _segmentHeaderLength; + _innerStream.Position = (newSegmentNum - 1) * _segmentContentLength + _currentRegionPosition; + return; + } + + _currentRegion = SMRegion.SegmentFooter; + _currentRegionPosition = (int)((value - _streamHeaderLength) % SegmentTotalLength) - + _segmentHeaderLength - _segmentContentLength; + _innerStream.Position = newSegmentNum * _segmentContentLength; + } + } + #endregion + + public StructuredMessageEncodingStream( + Stream innerStream, + int segmentContentLength, + StructuredMessage.Flags flags) + { + Argument.AssertNotNull(innerStream, nameof(innerStream)); + if (innerStream.GetLengthOrDefault() == default) + { + throw new ArgumentException("Stream must have known length.", nameof(innerStream)); + } + if (innerStream.Position != 0) + { + throw new ArgumentException("Stream must be at starting position.", nameof(innerStream)); + } + // stream logic likely breaks down with segment length of 1; enforce >=2 rather than just positive number + // real world scenarios will probably use a minimum of tens of KB + Argument.AssertInRange(segmentContentLength, 2, int.MaxValue, nameof(segmentContentLength)); + + _flags = flags; + _segmentContentLength = segmentContentLength; + + _streamHeaderLength = StructuredMessage.V1_0.StreamHeaderLength; + _streamFooterLength = UseCrcSegment ? StructuredMessage.Crc64Length : 0; + _segmentHeaderLength = StructuredMessage.V1_0.SegmentHeaderLength; + _segmentFooterLength = UseCrcSegment ? StructuredMessage.Crc64Length : 0; + + if (UseCrcSegment) + { + _totalCrc = StorageCrc64HashAlgorithm.Create(); + _segmentCrc = StorageCrc64HashAlgorithm.Create(); + _segmentCrcs = ArrayPool.Shared.Rent( + GetTotalSegments(innerStream, segmentContentLength) * StructuredMessage.Crc64Length); + innerStream = ChecksumCalculatingStream.GetReadStream(innerStream, span => + { + _totalCrc.Append(span); + _segmentCrc.Append(span); + }); + } + + _innerStream = innerStream; + } + + #region Write + public override void Flush() => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + #endregion + + #region Read + public override int Read(byte[] buffer, int offset, int count) + => ReadInternal(buffer, offset, count, async: false, cancellationToken: default).EnsureCompleted(); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadInternal(buffer, offset, count, async: true, cancellationToken).ConfigureAwait(false); + + private async ValueTask ReadInternal(byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < count && Position < Length) + { + int subreadOffset = offset + totalRead; + int subreadCount = count - totalRead; + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += await ReadFromInnerStreamInternal( + buffer, subreadOffset, subreadCount, async, cancellationToken).ConfigureAwait(false); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override int Read(Span buffer) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < buffer.Length && Position < Length) + { + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(buffer.Slice(totalRead)); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += ReadFromInnerStream(buffer.Slice(totalRead)); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < buffer.Length && Position < Length) + { + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(buffer.Slice(totalRead).Span); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += await ReadFromInnerStreamAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } +#endif + + #region Read Headers/Footers + private int ReadFromStreamHeader(Span buffer) + { + int read = Math.Min(buffer.Length, _streamHeaderLength - _currentRegionPosition); + using IDisposable _ = StructuredMessage.V1_0.GetStreamHeaderBytes( + ArrayPool.Shared, out Memory headerBytes, Length, _flags, TotalSegments); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _streamHeaderLength) + { + _currentRegion = SMRegion.SegmentHeader; + _currentRegionPosition = 0; + } + + return read; + } + + private int ReadFromStreamFooter(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentFooterLength - _currentRegionPosition); + if (read <= 0) + { + return 0; + } + + using IDisposable _ = StructuredMessage.V1_0.GetStreamFooterBytes( + ArrayPool.Shared, + out Memory footerBytes, + crc64: UseCrcSegment + ? _totalCrc.GetCurrentHash() // TODO array pooling + : default); + footerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + return read; + } + + private int ReadFromSegmentHeader(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentHeaderLength - _currentRegionPosition); + using IDisposable _ = StructuredMessage.V1_0.GetSegmentHeaderBytes( + ArrayPool.Shared, + out Memory headerBytes, + CurrentInnerSegment, + Math.Min(_segmentContentLength, _innerStream.Length - _innerStream.Position)); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _segmentHeaderLength) + { + _currentRegion = SMRegion.SegmentContent; + _currentRegionPosition = 0; + } + + return read; + } + + private int ReadFromSegmentFooter(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentFooterLength - _currentRegionPosition); + if (read < 0) + { + return 0; + } + + using IDisposable _ = StructuredMessage.V1_0.GetSegmentFooterBytes( + ArrayPool.Shared, + out Memory headerBytes, + crc64: UseCrcSegment + ? new Span( + _segmentCrcs, + (CurrentEncodingSegment-1) * _totalCrc.HashLengthInBytes, + _totalCrc.HashLengthInBytes) + : default); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _segmentFooterLength) + { + _currentRegion = _innerStream.Position == _innerStream.Length + ? SMRegion.StreamFooter : SMRegion.SegmentHeader; + _currentRegionPosition = 0; + } + + return read; + } + #endregion + + #region ReadUnderlyingStream + private int MaxInnerStreamRead => _segmentContentLength - _currentRegionPosition; + + private void CleanupContentSegment() + { + if (_currentRegionPosition == _segmentContentLength || _innerStream.Position >= _innerStream.Length) + { + _currentRegion = SMRegion.SegmentFooter; + _currentRegionPosition = 0; + if (UseCrcSegment && CurrentEncodingSegment - 1 == _latestSegmentCrcd) + { + _segmentCrc.GetCurrentHash(new Span( + _segmentCrcs, + _latestSegmentCrcd * _segmentCrc.HashLengthInBytes, + _segmentCrc.HashLengthInBytes)); + _latestSegmentCrcd++; + _segmentCrc = StorageCrc64HashAlgorithm.Create(); + } + } + } + + private async ValueTask ReadFromInnerStreamInternal( + byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken) + { + int read = async + ? await _innerStream.ReadAsync(buffer, offset, Math.Min(count, MaxInnerStreamRead)).ConfigureAwait(false) + : _innerStream.Read(buffer, offset, Math.Min(count, MaxInnerStreamRead)); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + private int ReadFromInnerStream(Span buffer) + { + if (MaxInnerStreamRead < buffer.Length) + { + buffer = buffer.Slice(0, MaxInnerStreamRead); + } + int read = _innerStream.Read(buffer); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } + + private async ValueTask ReadFromInnerStreamAsync(Memory buffer, CancellationToken cancellationToken) + { + if (MaxInnerStreamRead < buffer.Length) + { + buffer = buffer.Slice(0, MaxInnerStreamRead); + } + int read = await _innerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } +#endif + #endregion + + // don't allow stream to seek too far forward. track how far the stream has been naturally read. + private void UpdateLatestPosition() + { + if (_maxSeekPosition < Position) + { + _maxSeekPosition = Position; + } + } + #endregion + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = Length + offset; + break; + } + return Position; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_disposed) + { + return; + } + + if (disposing) + { + _innerStream.Dispose(); + _disposed = true; + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessagePrecalculatedCrcWrapperStream.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessagePrecalculatedCrcWrapperStream.cs new file mode 100644 index 000000000000..3569ef433973 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StructuredMessagePrecalculatedCrcWrapperStream.cs @@ -0,0 +1,451 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Storage.Common; + +namespace Azure.Storage.Shared; + +internal class StructuredMessagePrecalculatedCrcWrapperStream : Stream +{ + private readonly Stream _innerStream; + + private readonly int _streamHeaderLength; + private readonly int _streamFooterLength; + private readonly int _segmentHeaderLength; + private readonly int _segmentFooterLength; + + private bool _disposed; + + private readonly byte[] _crc; + + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanTimeout => _innerStream.CanTimeout; + + public override int ReadTimeout => _innerStream.ReadTimeout; + + public override int WriteTimeout => _innerStream.WriteTimeout; + + public override long Length => + _streamHeaderLength + _streamFooterLength + + _segmentHeaderLength + _segmentFooterLength + + _innerStream.Length; + + #region Position + private enum SMRegion + { + StreamHeader, + StreamFooter, + SegmentHeader, + SegmentFooter, + SegmentContent, + } + + private SMRegion _currentRegion = SMRegion.StreamHeader; + private int _currentRegionPosition = 0; + + private long _maxSeekPosition = 0; + + public override long Position + { + get + { + return _currentRegion switch + { + SMRegion.StreamHeader => _currentRegionPosition, + SMRegion.SegmentHeader => _innerStream.Position + + _streamHeaderLength + + _currentRegionPosition, + SMRegion.SegmentContent => _streamHeaderLength + + _segmentHeaderLength + + _innerStream.Position, + SMRegion.SegmentFooter => _streamHeaderLength + + _segmentHeaderLength + + _innerStream.Length + + _currentRegionPosition, + SMRegion.StreamFooter => _streamHeaderLength + + _segmentHeaderLength + + _innerStream.Length + + _segmentFooterLength + + _currentRegionPosition, + _ => throw new InvalidDataException($"{nameof(StructuredMessageEncodingStream)} invalid state."), + }; + } + set + { + Argument.AssertInRange(value, 0, _maxSeekPosition, nameof(value)); + if (value < _streamHeaderLength) + { + _currentRegion = SMRegion.StreamHeader; + _currentRegionPosition = (int)value; + _innerStream.Position = 0; + return; + } + if (value < _streamHeaderLength + _segmentHeaderLength) + { + _currentRegion = SMRegion.SegmentHeader; + _currentRegionPosition = (int)(value - _streamHeaderLength); + _innerStream.Position = 0; + return; + } + if (value < _streamHeaderLength + _segmentHeaderLength + _innerStream.Length) + { + _currentRegion = SMRegion.SegmentContent; + _currentRegionPosition = (int)(value - _streamHeaderLength - _segmentHeaderLength); + _innerStream.Position = value - _streamHeaderLength - _segmentHeaderLength; + return; + } + if (value < _streamHeaderLength + _segmentHeaderLength + _innerStream.Length + _segmentFooterLength) + { + _currentRegion = SMRegion.SegmentFooter; + _currentRegionPosition = (int)(value - _streamHeaderLength - _segmentHeaderLength - _innerStream.Length); + _innerStream.Position = _innerStream.Length; + return; + } + + _currentRegion = SMRegion.StreamFooter; + _currentRegionPosition = (int)(value - _streamHeaderLength - _segmentHeaderLength - _innerStream.Length - _segmentFooterLength); + _innerStream.Position = _innerStream.Length; + } + } + #endregion + + public StructuredMessagePrecalculatedCrcWrapperStream( + Stream innerStream, + ReadOnlySpan precalculatedCrc) + { + Argument.AssertNotNull(innerStream, nameof(innerStream)); + if (innerStream.GetLengthOrDefault() == default) + { + throw new ArgumentException("Stream must have known length.", nameof(innerStream)); + } + if (innerStream.Position != 0) + { + throw new ArgumentException("Stream must be at starting position.", nameof(innerStream)); + } + + _streamHeaderLength = StructuredMessage.V1_0.StreamHeaderLength; + _streamFooterLength = StructuredMessage.Crc64Length; + _segmentHeaderLength = StructuredMessage.V1_0.SegmentHeaderLength; + _segmentFooterLength = StructuredMessage.Crc64Length; + + _crc = ArrayPool.Shared.Rent(StructuredMessage.Crc64Length); + precalculatedCrc.CopyTo(_crc); + + _innerStream = innerStream; + } + + #region Write + public override void Flush() => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + #endregion + + #region Read + public override int Read(byte[] buffer, int offset, int count) + => ReadInternal(buffer, offset, count, async: false, cancellationToken: default).EnsureCompleted(); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadInternal(buffer, offset, count, async: true, cancellationToken).ConfigureAwait(false); + + private async ValueTask ReadInternal(byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < count && Position < Length) + { + int subreadOffset = offset + totalRead; + int subreadCount = count - totalRead; + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(new Span(buffer, subreadOffset, subreadCount)); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += await ReadFromInnerStreamInternal( + buffer, subreadOffset, subreadCount, async, cancellationToken).ConfigureAwait(false); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override int Read(Span buffer) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < buffer.Length && Position < Length) + { + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(buffer.Slice(totalRead)); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(buffer.Slice(totalRead)); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += ReadFromInnerStream(buffer.Slice(totalRead)); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int totalRead = 0; + bool readInner = false; + while (totalRead < buffer.Length && Position < Length) + { + switch (_currentRegion) + { + case SMRegion.StreamHeader: + totalRead += ReadFromStreamHeader(buffer.Slice(totalRead).Span); + break; + case SMRegion.StreamFooter: + totalRead += ReadFromStreamFooter(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentHeader: + totalRead += ReadFromSegmentHeader(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentFooter: + totalRead += ReadFromSegmentFooter(buffer.Slice(totalRead).Span); + break; + case SMRegion.SegmentContent: + // don't double read from stream. Allow caller to multi-read when desired. + if (readInner) + { + UpdateLatestPosition(); + return totalRead; + } + totalRead += await ReadFromInnerStreamAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); + readInner = true; + break; + default: + break; + } + } + UpdateLatestPosition(); + return totalRead; + } +#endif + + #region Read Headers/Footers + private int ReadFromStreamHeader(Span buffer) + { + int read = Math.Min(buffer.Length, _streamHeaderLength - _currentRegionPosition); + using IDisposable _ = StructuredMessage.V1_0.GetStreamHeaderBytes( + ArrayPool.Shared, + out Memory headerBytes, + Length, + StructuredMessage.Flags.StorageCrc64, + totalSegments: 1); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _streamHeaderLength) + { + _currentRegion = SMRegion.SegmentHeader; + _currentRegionPosition = 0; + } + + return read; + } + + private int ReadFromStreamFooter(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentFooterLength - _currentRegionPosition); + if (read <= 0) + { + return 0; + } + + using IDisposable _ = StructuredMessage.V1_0.GetStreamFooterBytes( + ArrayPool.Shared, + out Memory footerBytes, + new ReadOnlySpan(_crc, 0, StructuredMessage.Crc64Length)); + footerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + return read; + } + + private int ReadFromSegmentHeader(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentHeaderLength - _currentRegionPosition); + using IDisposable _ = StructuredMessage.V1_0.GetSegmentHeaderBytes( + ArrayPool.Shared, + out Memory headerBytes, + segmentNum: 1, + _innerStream.Length); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _segmentHeaderLength) + { + _currentRegion = SMRegion.SegmentContent; + _currentRegionPosition = 0; + } + + return read; + } + + private int ReadFromSegmentFooter(Span buffer) + { + int read = Math.Min(buffer.Length, _segmentFooterLength - _currentRegionPosition); + if (read < 0) + { + return 0; + } + + using IDisposable _ = StructuredMessage.V1_0.GetSegmentFooterBytes( + ArrayPool.Shared, + out Memory headerBytes, + new ReadOnlySpan(_crc, 0, StructuredMessage.Crc64Length)); + headerBytes.Slice(_currentRegionPosition, read).Span.CopyTo(buffer); + _currentRegionPosition += read; + + if (_currentRegionPosition == _segmentFooterLength) + { + _currentRegion = _innerStream.Position == _innerStream.Length + ? SMRegion.StreamFooter : SMRegion.SegmentHeader; + _currentRegionPosition = 0; + } + + return read; + } + #endregion + + #region ReadUnderlyingStream + private void CleanupContentSegment() + { + if (_innerStream.Position >= _innerStream.Length) + { + _currentRegion = SMRegion.SegmentFooter; + _currentRegionPosition = 0; + } + } + + private async ValueTask ReadFromInnerStreamInternal( + byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken) + { + int read = async + ? await _innerStream.ReadAsync(buffer, offset, count).ConfigureAwait(false) + : _innerStream.Read(buffer, offset, count); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + private int ReadFromInnerStream(Span buffer) + { + int read = _innerStream.Read(buffer); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } + + private async ValueTask ReadFromInnerStreamAsync(Memory buffer, CancellationToken cancellationToken) + { + int read = await _innerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _currentRegionPosition += read; + CleanupContentSegment(); + return read; + } +#endif + #endregion + + // don't allow stream to seek too far forward. track how far the stream has been naturally read. + private void UpdateLatestPosition() + { + if (_maxSeekPosition < Position) + { + _maxSeekPosition = Position; + } + } + #endregion + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = Length + offset; + break; + } + return Position; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_disposed) + { + return; + } + + if (disposing) + { + ArrayPool.Shared.Return(_crc); + _innerStream.Dispose(); + _disposed = true; + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/TransferValidationOptionsExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/TransferValidationOptionsExtensions.cs index af21588b4ae0..763d38524038 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/TransferValidationOptionsExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/TransferValidationOptionsExtensions.cs @@ -9,14 +9,7 @@ public static StorageChecksumAlgorithm ResolveAuto(this StorageChecksumAlgorithm { if (checksumAlgorithm == StorageChecksumAlgorithm.Auto) { -#if BlobSDK || DataLakeSDK || CommonSDK return StorageChecksumAlgorithm.StorageCrc64; -#elif FileSDK // file shares don't support crc64 - return StorageChecksumAlgorithm.MD5; -#else - throw new System.NotSupportedException( - $"{typeof(TransferValidationOptionsExtensions).FullName}.{nameof(ResolveAuto)} is not supported."); -#endif } return checksumAlgorithm; } diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/UserDelegationKeyProperties.cs b/sdk/storage/Azure.Storage.Common/src/Shared/UserDelegationKeyProperties.cs index 28cf51dbb77c..481aae67db6e 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/UserDelegationKeyProperties.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/UserDelegationKeyProperties.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Net; using System.Text; +using Azure.Core; namespace Azure.Storage.Sas { @@ -32,6 +33,9 @@ internal class UserDelegationKeyProperties // skv internal string Version { get; set; } + // skdutid + public string DelegatedUserTenantId { get; set; } + /// /// Builds up the UserDelegationKey portion of the SAS query parameter string. /// @@ -66,6 +70,11 @@ public void AppendProperties(StringBuilder stringBuilder) { stringBuilder.AppendQueryParameter(Constants.Sas.Parameters.KeyVersion, Version); } + + if (!string.IsNullOrWhiteSpace(DelegatedUserTenantId)) + { + stringBuilder.AppendQueryParameter(Constants.Sas.Parameters.KeyDelegatedUserTenantId, DelegatedUserTenantId); + } } } } diff --git a/sdk/storage/Azure.Storage.Common/tests/Azure.Storage.Common.Tests.csproj b/sdk/storage/Azure.Storage.Common/tests/Azure.Storage.Common.Tests.csproj index b30f9cafbb41..39b3e3dade11 100644 --- a/sdk/storage/Azure.Storage.Common/tests/Azure.Storage.Common.Tests.csproj +++ b/sdk/storage/Azure.Storage.Common/tests/Azure.Storage.Common.Tests.csproj @@ -16,9 +16,12 @@ + + + @@ -33,6 +36,7 @@ + @@ -51,6 +55,11 @@ + + + + + diff --git a/sdk/storage/Azure.Storage.Common/tests/CommonTestBase.cs b/sdk/storage/Azure.Storage.Common/tests/CommonTestBase.cs index eba1dd050e1e..91b3ea18b9df 100644 --- a/sdk/storage/Azure.Storage.Common/tests/CommonTestBase.cs +++ b/sdk/storage/Azure.Storage.Common/tests/CommonTestBase.cs @@ -39,7 +39,8 @@ namespace Azure.Storage.Test BlobClientOptions.ServiceVersion.V2025_07_05, BlobClientOptions.ServiceVersion.V2025_11_05, BlobClientOptions.ServiceVersion.V2026_02_06, - RecordingServiceVersion = BlobClientOptions.ServiceVersion.V2026_02_06, + BlobClientOptions.ServiceVersion.V2026_04_06, + RecordingServiceVersion = BlobClientOptions.ServiceVersion.V2026_04_06, LiveServiceVersions = new object[] { BlobClientOptions.ServiceVersion.V2026_02_06, })] public abstract class CommonTestBase : StorageTestBase { diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/CustomRequestHeadersAndQueryParametersPolicy.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/CustomRequestHeadersAndQueryParametersPolicy.cs new file mode 100644 index 000000000000..06b45d6b7d2e --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/CustomRequestHeadersAndQueryParametersPolicy.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Storage.Test.Shared +{ + internal class CustomRequestHeadersAndQueryParametersPolicy : HttpPipelineSynchronousPolicy + { + private Dictionary> _requestHeaders = new(); + private Dictionary> _queryParameters = new(); + + public CustomRequestHeadersAndQueryParametersPolicy() { } + + public void AddRequestHeader(string name, string value) + { + if (!_requestHeaders.TryGetValue(name, out var values)) + { + values = new List(); + _requestHeaders[name] = values; + } + values.Add(value); + } + + public void AddQueryParameter(string name, string value) + { + if (!_queryParameters.TryGetValue(name, out var values)) + { + values = new List(); + _queryParameters[name] = values; + } + values.Add(value); + } + + public override void OnSendingRequest(HttpMessage message) + { + foreach (var header in _requestHeaders) + { + foreach (var value in header.Value) + { + message.Request.Headers.Add(header.Key, value); + } + } + foreach (var param in _queryParameters) + { + foreach (var value in param.Value) + { + message.Request.Uri.AppendQuery(param.Key, value); + } + } + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/FaultyStream.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/FaultyStream.cs index 7411eb149931..f4e4b92ed73c 100644 --- a/sdk/storage/Azure.Storage.Common/tests/Shared/FaultyStream.cs +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/FaultyStream.cs @@ -15,6 +15,7 @@ internal class FaultyStream : Stream private readonly Exception _exceptionToRaise; private int _remainingExceptions; private Action _onFault; + private long _position = 0; public FaultyStream( Stream innerStream, @@ -40,7 +41,7 @@ public FaultyStream( public override long Position { - get => _innerStream.Position; + get => CanSeek ? _innerStream.Position : _position; set => _innerStream.Position = value; } @@ -53,7 +54,9 @@ public override int Read(byte[] buffer, int offset, int count) { if (_remainingExceptions == 0 || Position + count <= _raiseExceptionAt || _raiseExceptionAt >= _innerStream.Length) { - return _innerStream.Read(buffer, offset, count); + int read = _innerStream.Read(buffer, offset, count); + _position += read; + return read; } else { @@ -61,11 +64,13 @@ public override int Read(byte[] buffer, int offset, int count) } } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_remainingExceptions == 0 || Position + count <= _raiseExceptionAt || _raiseExceptionAt >= _innerStream.Length) { - return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + int read = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + _position += read; + return read; } else { diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/ObserveStructuredMessagePolicy.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/ObserveStructuredMessagePolicy.cs new file mode 100644 index 000000000000..828c41179bba --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/ObserveStructuredMessagePolicy.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Storage.Shared; + +namespace Azure.Storage.Test.Shared +{ + internal class ObserveStructuredMessagePolicy : HttpPipelineSynchronousPolicy + { + private readonly HashSet _requestScopes = new(); + + private readonly HashSet _responseScopes = new(); + + public ObserveStructuredMessagePolicy() + { + } + + public override void OnSendingRequest(HttpMessage message) + { + if (_requestScopes.Count > 0) + { + byte[] encodedContent; + byte[] underlyingContent; + StructuredMessageDecodingStream.RawDecodedData decodedData; + using (MemoryStream ms = new()) + { + message.Request.Content.WriteTo(ms, default); + encodedContent = ms.ToArray(); + using (MemoryStream ms2 = new()) + { + (Stream s, decodedData) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedContent)); + s.CopyTo(ms2); + underlyingContent = ms2.ToArray(); + } + } + } + } + + public override void OnReceivedResponse(HttpMessage message) + { + } + + public IDisposable CheckRequestScope() => CheckMessageScope.CheckRequestScope(this); + + public IDisposable CheckResponseScope() => CheckMessageScope.CheckResponseScope(this); + + private class CheckMessageScope : IDisposable + { + private bool _isRequestScope; + private ObserveStructuredMessagePolicy _policy; + + public static CheckMessageScope CheckRequestScope(ObserveStructuredMessagePolicy policy) + { + CheckMessageScope result = new() + { + _isRequestScope = true, + _policy = policy + }; + result._policy._requestScopes.Add(result); + return result; + } + + public static CheckMessageScope CheckResponseScope(ObserveStructuredMessagePolicy policy) + { + CheckMessageScope result = new() + { + _isRequestScope = false, + _policy = policy + }; + result._policy._responseScopes.Add(result); + return result; + } + + public void Dispose() + { + (_isRequestScope ? _policy._requestScopes : _policy._responseScopes).Remove(this); + } + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/RequestExtensions.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/RequestExtensions.cs new file mode 100644 index 000000000000..ad395e862f82 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/RequestExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Text; +using Azure.Core; +using NUnit.Framework; + +namespace Azure.Storage; + +public static partial class RequestExtensions +{ + public static string AssertHeaderPresent(this Request request, string headerName) + { + if (request.Headers.TryGetValue(headerName, out string value)) + { + return headerName == Constants.StructuredMessage.StructuredMessageHeader ? null : value; + } + StringBuilder sb = new StringBuilder() + .AppendLine($"`{headerName}` expected on request but was not found.") + .AppendLine($"{request.Method} {request.Uri}") + .AppendLine(string.Join("\n", request.Headers.Select(h => $"{h.Name}: {h.Value}s"))) + ; + Assert.Fail(sb.ToString()); + return null; + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/TamperStreamContentsPolicy.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/TamperStreamContentsPolicy.cs index f4198e9dfd53..7e6c78117f53 100644 --- a/sdk/storage/Azure.Storage.Common/tests/Shared/TamperStreamContentsPolicy.cs +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/TamperStreamContentsPolicy.cs @@ -14,7 +14,7 @@ internal class TamperStreamContentsPolicy : HttpPipelineSynchronousPolicy /// /// Default tampering that changes the first byte of the stream. /// - private static readonly Func _defaultStreamTransform = stream => + private static Func GetTamperByteStreamTransform(long position) => stream => { if (stream is not MemoryStream) { @@ -23,10 +23,10 @@ internal class TamperStreamContentsPolicy : HttpPipelineSynchronousPolicy stream = buffer; } - stream.Position = 0; + stream.Position = position; var firstByte = stream.ReadByte(); - stream.Position = 0; + stream.Position = position; stream.WriteByte((byte)((firstByte + 1) % byte.MaxValue)); stream.Position = 0; @@ -37,9 +37,12 @@ internal class TamperStreamContentsPolicy : HttpPipelineSynchronousPolicy public TamperStreamContentsPolicy(Func streamTransform = default) { - _streamTransform = streamTransform ?? _defaultStreamTransform; + _streamTransform = streamTransform ?? GetTamperByteStreamTransform(0); } + public static TamperStreamContentsPolicy TamperByteAt(long position) + => new(GetTamperByteStreamTransform(position)); + public bool TransformRequestBody { get; set; } public bool TransformResponseBody { get; set; } diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/TestConstants.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/TestConstants.cs index efc7648fa413..dd3bfc852a00 100644 --- a/sdk/storage/Azure.Storage.Common/tests/Shared/TestConstants.cs +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/TestConstants.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Net; using System.Security.Cryptography; using System.Text; @@ -46,7 +47,16 @@ public class SasConstants public string KeyTenantId { get; } = "KeyTid"; public string KeyService { get; } = "KeyService"; public string KeyVersion { get; } = "KeyVersion"; + public string KeyDelegatedTenantId { get; } = "DelegatedTid"; public string DelegatedObjectId { get; } = "DelegatedOid"; + public Dictionary RequestHeaders { get; } = new Dictionary() + { + { "foo", "bar" } + }; + public Dictionary RequestQueryParameters { get; } = new Dictionary() + { + { "hello", "world" } + }; public string KeyValue { get; } = Convert.ToBase64String(Encoding.UTF8.GetBytes("value")); public SasProtocol Protocol { get; } = SasProtocol.Https; diff --git a/sdk/storage/Azure.Storage.Common/tests/Shared/TransferValidationTestBase.cs b/sdk/storage/Azure.Storage.Common/tests/Shared/TransferValidationTestBase.cs index c18492d2fb4d..248acf881196 100644 --- a/sdk/storage/Azure.Storage.Common/tests/Shared/TransferValidationTestBase.cs +++ b/sdk/storage/Azure.Storage.Common/tests/Shared/TransferValidationTestBase.cs @@ -5,10 +5,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; using Azure.Core; +using Azure.Core.Diagnostics; +using Azure.Core.Pipeline; using Azure.Core.TestFramework; -using FastSerialization; +using Azure.Storage.Shared; using NUnit.Framework; namespace Azure.Storage.Test.Shared @@ -190,21 +193,15 @@ protected string GetNewResourceName() /// The actual checksum value expected to be on the request, if known. Defaults to no specific value expected or checked. /// /// An assertion to put into a pipeline policy. - internal static Action GetRequestChecksumAssertion(StorageChecksumAlgorithm algorithm, Func isChecksumExpected = default, byte[] expectedChecksum = default) + internal static Action GetRequestChecksumHeaderAssertion(StorageChecksumAlgorithm algorithm, Func isChecksumExpected = default, byte[] expectedChecksum = default) { // action to assert a request header is as expected - void AssertChecksum(RequestHeaders headers, string headerName) + void AssertChecksum(Request req, string headerName) { - if (headers.TryGetValue(headerName, out string checksum)) + string checksum = req.AssertHeaderPresent(headerName); + if (expectedChecksum != default) { - if (expectedChecksum != default) - { - Assert.AreEqual(Convert.ToBase64String(expectedChecksum), checksum); - } - } - else - { - Assert.Fail($"{headerName} expected on request but was not found."); + Assert.AreEqual(Convert.ToBase64String(expectedChecksum), checksum); } }; @@ -219,14 +216,39 @@ void AssertChecksum(RequestHeaders headers, string headerName) switch (algorithm.ResolveAuto()) { case StorageChecksumAlgorithm.MD5: - AssertChecksum(request.Headers, "Content-MD5"); + AssertChecksum(request, "Content-MD5"); break; case StorageChecksumAlgorithm.StorageCrc64: - AssertChecksum(request.Headers, "x-ms-content-crc64"); + AssertChecksum(request, Constants.StructuredMessage.StructuredMessageHeader); break; default: - throw new Exception($"Bad {nameof(StorageChecksumAlgorithm)} provided to {nameof(GetRequestChecksumAssertion)}."); + throw new Exception($"Bad {nameof(StorageChecksumAlgorithm)} provided to {nameof(GetRequestChecksumHeaderAssertion)}."); + } + }; + } + + internal static Action GetRequestStructuredMessageAssertion( + StructuredMessage.Flags flags, + Func isStructuredMessageExpected = default, + long? structuredContentSegmentLength = default) + { + return request => + { + // filter some requests out with predicate + if (isStructuredMessageExpected != default && !isStructuredMessageExpected(request)) + { + return; } + + Assert.That(request.Headers.TryGetValue("x-ms-structured-body", out string structuredBody)); + Assert.That(structuredBody, Does.Contain("XSM/1.0")); + if (flags.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + Assert.That(structuredBody, Does.Contain("crc64")); + } + + Assert.That(request.Headers.TryGetValue("Content-Length", out string contentLength)); + Assert.That(request.Headers.TryGetValue("x-ms-structured-content-length", out string structuredContentLength)); }; } @@ -278,32 +300,66 @@ void AssertChecksum(ResponseHeaders headers, string headerName) AssertChecksum(response.Headers, "Content-MD5"); break; case StorageChecksumAlgorithm.StorageCrc64: - AssertChecksum(response.Headers, "x-ms-content-crc64"); + AssertChecksum(response.Headers, Constants.StructuredMessage.StructuredMessageHeader); break; default: - throw new Exception($"Bad {nameof(StorageChecksumAlgorithm)} provided to {nameof(GetRequestChecksumAssertion)}."); + throw new Exception($"Bad {nameof(StorageChecksumAlgorithm)} provided to {nameof(GetRequestChecksumHeaderAssertion)}."); } }; } + internal static Action GetResponseStructuredMessageAssertion( + StructuredMessage.Flags flags, + Func isStructuredMessageExpected = default) + { + return response => + { + // filter some requests out with predicate + if (isStructuredMessageExpected != default && !isStructuredMessageExpected(response)) + { + return; + } + + Assert.That(response.Headers.TryGetValue("x-ms-structured-body", out string structuredBody)); + Assert.That(structuredBody, Does.Contain("XSM/1.0")); + if (flags.HasFlag(StructuredMessage.Flags.StorageCrc64)) + { + Assert.That(structuredBody, Does.Contain("crc64")); + } + + Assert.That(response.Headers.TryGetValue("Content-Length", out string contentLength)); + Assert.That(response.Headers.TryGetValue("x-ms-structured-content-length", out string structuredContentLength)); + }; + } + /// /// Asserts the service returned an error that expected checksum did not match checksum on upload. /// /// Async action to upload data to service. /// Checksum algorithm used. - internal static void AssertWriteChecksumMismatch(AsyncTestDelegate writeAction, StorageChecksumAlgorithm algorithm) + internal static void AssertWriteChecksumMismatch( + AsyncTestDelegate writeAction, + StorageChecksumAlgorithm algorithm, + bool expectStructuredMessage = false) { var exception = ThrowsOrInconclusiveAsync(writeAction); - switch (algorithm.ResolveAuto()) + if (expectStructuredMessage) { - case StorageChecksumAlgorithm.MD5: - Assert.AreEqual("Md5Mismatch", exception.ErrorCode); - break; - case StorageChecksumAlgorithm.StorageCrc64: - Assert.AreEqual("Crc64Mismatch", exception.ErrorCode); - break; - default: - throw new ArgumentException("Test arguments contain bad algorithm specifier."); + Assert.That(exception.ErrorCode, Is.EqualTo("Crc64Mismatch")); + } + else + { + switch (algorithm.ResolveAuto()) + { + case StorageChecksumAlgorithm.MD5: + Assert.That(exception.ErrorCode, Is.EqualTo("Md5Mismatch")); + break; + case StorageChecksumAlgorithm.StorageCrc64: + Assert.That(exception.ErrorCode, Is.EqualTo("Crc64Mismatch")); + break; + default: + throw new ArgumentException("Test arguments contain bad algorithm specifier."); + } } } #endregion @@ -348,6 +404,7 @@ public virtual async Task UploadPartitionSuccessfulHashComputation(StorageChecks await using IDisposingContainer disposingContainer = await GetDisposingContainerAsync(); // Arrange + bool expectStructuredMessage = algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64; const int dataLength = Constants.KB; var data = GetRandomBuffer(dataLength); var validationOptions = new UploadTransferValidationOptions @@ -356,7 +413,10 @@ public virtual async Task UploadPartitionSuccessfulHashComputation(StorageChecks }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(algorithm)); + var assertion = algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, null, dataLength) + : GetRequestChecksumHeaderAssertion(algorithm); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -406,7 +466,11 @@ public virtual async Task UploadPartitionUsePrecalculatedHash(StorageChecksumAlg }; // make pipeline assertion for checking precalculated checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(algorithm, expectedChecksum: precalculatedChecksum)); + // precalculated partition upload will never use structured message. always check header + var assertion = GetRequestChecksumHeaderAssertion( + algorithm, + expectedChecksum: algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 ? default : precalculatedChecksum); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -423,12 +487,12 @@ public virtual async Task UploadPartitionUsePrecalculatedHash(StorageChecksumAlg AsyncTestDelegate operation = async () => await UploadPartitionAsync(client, stream, validationOptions); // Assert - AssertWriteChecksumMismatch(operation, algorithm); + AssertWriteChecksumMismatch(operation, algorithm, algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64); } } [TestCaseSource(nameof(GetValidationAlgorithms))] - public virtual async Task UploadPartitionMismatchedHashThrows(StorageChecksumAlgorithm algorithm) + public virtual async Task UploadPartitionTamperedStreamThrows(StorageChecksumAlgorithm algorithm) { await using IDisposingContainer disposingContainer = await GetDisposingContainerAsync(); @@ -441,7 +505,7 @@ public virtual async Task UploadPartitionMismatchedHashThrows(StorageChecksumAlg }; // Tamper with stream contents in the pipeline to simulate silent failure in the transit layer - var streamTamperPolicy = new TamperStreamContentsPolicy(); + var streamTamperPolicy = TamperStreamContentsPolicy.TamperByteAt(100); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(streamTamperPolicy, HttpPipelinePosition.PerCall); @@ -456,9 +520,10 @@ public virtual async Task UploadPartitionMismatchedHashThrows(StorageChecksumAlg // Act streamTamperPolicy.TransformRequestBody = true; AsyncTestDelegate operation = async () => await UploadPartitionAsync(client, stream, validationOptions); - + using var listener = AzureEventSourceListener.CreateConsoleLogger(); // Assert - AssertWriteChecksumMismatch(operation, algorithm); + AssertWriteChecksumMismatch(operation, algorithm, + expectStructuredMessage: algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64); } } @@ -473,7 +538,10 @@ public virtual async Task UploadPartitionUsesDefaultClientValidationOptions( var data = GetRandomBuffer(dataLength); // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(clientAlgorithm)); + var assertion = clientAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, null, dataLength) + : GetRequestChecksumHeaderAssertion(clientAlgorithm); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -512,7 +580,10 @@ public virtual async Task UploadPartitionOverwritesDefaultClientValidationOption }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(overrideAlgorithm)); + var assertion = overrideAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, null, dataLength) + : GetRequestChecksumHeaderAssertion(overrideAlgorithm); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -555,10 +626,14 @@ public virtual async Task UploadPartitionDisablesDefaultClientValidationOptions( { Assert.Fail($"Hash found when none expected."); } - if (request.Headers.Contains("x-ms-content-crc64")) + if (request.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } + if (request.Headers.Contains("x-ms-structured-body")) + { + Assert.Fail($"Structured body used when none expected."); + } }); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -601,9 +676,11 @@ public virtual async Task OpenWriteSuccessfulHashComputation( }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(algorithm)); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumHeaderAssertion(algorithm)); var clientOptions = ClientBuilder.GetOptions(); + //ObserveStructuredMessagePolicy observe = new(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); + //clientOptions.AddPolicy(observe, HttpPipelinePosition.BeforeTransport); var client = await GetResourceClientAsync( disposingContainer.Container, @@ -616,6 +693,7 @@ public virtual async Task OpenWriteSuccessfulHashComputation( using var writeStream = await OpenWriteAsync(client, validationOptions, streamBufferSize); // Assert + //using var obsv = observe.CheckRequestScope(); using (checksumPipelineAssertion.CheckRequestScope()) { foreach (var _ in Enumerable.Range(0, streamWrites)) @@ -644,7 +722,7 @@ public virtual async Task OpenWriteMismatchedHashThrows(StorageChecksumAlgorithm // Tamper with stream contents in the pipeline to simulate silent failure in the transit layer var clientOptions = ClientBuilder.GetOptions(); - var tamperPolicy = new TamperStreamContentsPolicy(); + var tamperPolicy = TamperStreamContentsPolicy.TamperByteAt(100); clientOptions.AddPolicy(tamperPolicy, HttpPipelinePosition.PerCall); var client = await GetResourceClientAsync( @@ -682,7 +760,7 @@ public virtual async Task OpenWriteUsesDefaultClientValidationOptions( var data = GetRandomBuffer(dataLength); // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(clientAlgorithm)); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumHeaderAssertion(clientAlgorithm)); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -726,7 +804,7 @@ public virtual async Task OpenWriteOverwritesDefaultClientValidationOptions( }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion(overrideAlgorithm)); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumHeaderAssertion(overrideAlgorithm)); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -774,7 +852,7 @@ public virtual async Task OpenWriteDisablesDefaultClientValidationOptions( { Assert.Fail($"Hash found when none expected."); } - if (request.Headers.Contains("x-ms-content-crc64")) + if (request.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } @@ -886,7 +964,7 @@ public virtual async Task ParallelUploadSplitSuccessfulHashComputation(StorageCh // make pipeline assertion for checking checksum was present on upload var checksumPipelineAssertion = new AssertMessageContentsPolicy( - checkRequest: GetRequestChecksumAssertion(algorithm, isChecksumExpected: ParallelUploadIsChecksumExpected)); + checkRequest: GetRequestChecksumHeaderAssertion(algorithm, isChecksumExpected: ParallelUploadIsChecksumExpected)); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -923,8 +1001,10 @@ public virtual async Task ParallelUploadOneShotSuccessfulHashComputation(Storage }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy( - checkRequest: GetRequestChecksumAssertion(algorithm, isChecksumExpected: ParallelUploadIsChecksumExpected)); + var assertion = algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, ParallelUploadIsChecksumExpected, dataLength) + : GetRequestChecksumHeaderAssertion(algorithm, isChecksumExpected: ParallelUploadIsChecksumExpected); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -981,7 +1061,7 @@ public virtual async Task ParallelUploadPrecalculatedComposableHashAccepted(Stor PrecalculatedChecksum = hash }; - var client = await GetResourceClientAsync(disposingContainer.Container, dataLength); + var client = await GetResourceClientAsync(disposingContainer.Container, dataLength, createResource: true); // Act await DoesNotThrowOrInconclusiveAsync( @@ -1011,8 +1091,10 @@ public virtual async Task ParallelUploadUsesDefaultClientValidationOptions( }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion( - clientAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected)); + var assertion = clientAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && !split + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, ParallelUploadIsChecksumExpected, dataLength) + : GetRequestChecksumHeaderAssertion(clientAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -1063,8 +1145,10 @@ public virtual async Task ParallelUploadOverwritesDefaultClientValidationOptions }; // make pipeline assertion for checking checksum was present on upload - var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: GetRequestChecksumAssertion( - overrideAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected)); + var assertion = overrideAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64 && !split + ? GetRequestStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64, ParallelUploadIsChecksumExpected, dataLength) + : GetRequestChecksumHeaderAssertion(overrideAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected); + var checksumPipelineAssertion = new AssertMessageContentsPolicy(checkRequest: assertion); var clientOptions = ClientBuilder.GetOptions(); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); @@ -1119,7 +1203,7 @@ public virtual async Task ParallelUploadDisablesDefaultClientValidationOptions( { Assert.Fail($"Hash found when none expected."); } - if (request.Headers.Contains("x-ms-content-crc64")) + if (request.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } @@ -1184,15 +1268,17 @@ public virtual async Task ParallelDownloadSuccessfulHashVerification( }; // Act - var dest = new MemoryStream(); + byte[] dest; + using (MemoryStream ms = new()) using (checksumPipelineAssertion.CheckRequestScope()) { - await ParallelDownloadAsync(client, dest, validationOptions, transferOptions); + await ParallelDownloadAsync(client, ms, validationOptions, transferOptions); + dest = ms.ToArray(); } // Assert // Assertion was in the pipeline and the SDK not throwing means the checksum was validated - Assert.IsTrue(dest.ToArray().SequenceEqual(data)); + Assert.IsTrue(dest.SequenceEqual(data)); } [Test] @@ -1357,7 +1443,7 @@ public virtual async Task ParallelDownloadDisablesDefaultClientValidationOptions { Assert.Fail($"Hash found when none expected."); } - if (response.Headers.Contains("x-ms-content-crc64")) + if (response.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } @@ -1565,7 +1651,7 @@ public virtual async Task OpenReadDisablesDefaultClientValidationOptions( { Assert.Fail($"Hash found when none expected."); } - if (response.Headers.Contains("x-ms-content-crc64")) + if (response.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } @@ -1615,7 +1701,7 @@ public virtual async Task DownloadSuccessfulHashVerification(StorageChecksumAlgo var validationOptions = new DownloadTransferValidationOptions { ChecksumAlgorithm = algorithm }; // Act - var dest = new MemoryStream(); + using var dest = new MemoryStream(); var response = await DownloadPartitionAsync(client, dest, validationOptions, new HttpRange(length: data.Length)); // Assert @@ -1626,13 +1712,71 @@ public virtual async Task DownloadSuccessfulHashVerification(StorageChecksumAlgo Assert.True(response.Headers.Contains("Content-MD5")); break; case StorageChecksumAlgorithm.StorageCrc64: - Assert.True(response.Headers.Contains("x-ms-content-crc64")); + Assert.True(response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)); break; default: Assert.Fail("Test can't validate given algorithm type."); break; } - Assert.IsTrue(dest.ToArray().SequenceEqual(data)); + var result = dest.ToArray(); + Assert.IsTrue(result.SequenceEqual(data)); + } + + [TestCase(StorageChecksumAlgorithm.StorageCrc64, Constants.StructuredMessage.MaxDownloadCrcWithHeader, false, false)] + [TestCase(StorageChecksumAlgorithm.StorageCrc64, Constants.StructuredMessage.MaxDownloadCrcWithHeader-1, false, false)] + [TestCase(StorageChecksumAlgorithm.StorageCrc64, Constants.StructuredMessage.MaxDownloadCrcWithHeader+1, true, false)] + [TestCase(StorageChecksumAlgorithm.MD5, Constants.StructuredMessage.MaxDownloadCrcWithHeader+1, false, true)] + public virtual async Task DownloadApporpriatelyUsesStructuredMessage( + StorageChecksumAlgorithm algorithm, + int? downloadLen, + bool expectStructuredMessage, + bool expectThrow) + { + await using IDisposingContainer disposingContainer = await GetDisposingContainerAsync(); + + // Arrange + const int dataLength = Constants.KB; + var data = GetRandomBuffer(dataLength); + + var resourceName = GetNewResourceName(); + var client = await GetResourceClientAsync( + disposingContainer.Container, + resourceLength: dataLength, + createResource: true, + resourceName: resourceName); + await SetupDataAsync(client, new MemoryStream(data)); + + // make pipeline assertion for checking checksum was present on download + HttpPipelinePolicy checksumPipelineAssertion = new AssertMessageContentsPolicy(checkResponse: expectStructuredMessage + ? GetResponseStructuredMessageAssertion(StructuredMessage.Flags.StorageCrc64) + : GetResponseChecksumAssertion(algorithm)); + TClientOptions clientOptions = ClientBuilder.GetOptions(); + clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); + + client = await GetResourceClientAsync( + disposingContainer.Container, + resourceLength: dataLength, + resourceName: resourceName, + createResource: false, + downloadAlgorithm: algorithm, + options: clientOptions); + + var validationOptions = new DownloadTransferValidationOptions { ChecksumAlgorithm = algorithm }; + + // Act + var dest = new MemoryStream(); + AsyncTestDelegate operation = async () => await DownloadPartitionAsync( + client, dest, validationOptions, downloadLen.HasValue ? new HttpRange(length: downloadLen.Value) : default); + // Assert (policies checked use of content validation) + if (expectThrow) + { + Assert.That(operation, Throws.TypeOf()); + } + else + { + Assert.That(operation, Throws.Nothing); + Assert.IsTrue(dest.ToArray().SequenceEqual(data)); + } } [Test, Combinatorial] @@ -1658,7 +1802,9 @@ public virtual async Task DownloadHashMismatchThrows( // alter response contents in pipeline, forcing a checksum mismatch on verification step var clientOptions = ClientBuilder.GetOptions(); - clientOptions.AddPolicy(new TamperStreamContentsPolicy() { TransformResponseBody = true }, HttpPipelinePosition.PerCall); + var tamperPolicy = TamperStreamContentsPolicy.TamperByteAt(50); + tamperPolicy.TransformResponseBody = true; + clientOptions.AddPolicy(tamperPolicy, HttpPipelinePosition.PerCall); client = await GetResourceClientAsync( disposingContainer.Container, createResource: false, @@ -1670,7 +1816,7 @@ public virtual async Task DownloadHashMismatchThrows( AsyncTestDelegate operation = async () => await DownloadPartitionAsync(client, dest, validationOptions, new HttpRange(length: data.Length)); // Assert - if (validate) + if (validate || algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64) { // SDK responsible for finding bad checksum. Throw. ThrowsOrInconclusiveAsync(operation); @@ -1728,7 +1874,7 @@ public virtual async Task DownloadUsesDefaultClientValidationOptions( Assert.True(response.Headers.Contains("Content-MD5")); break; case StorageChecksumAlgorithm.StorageCrc64: - Assert.True(response.Headers.Contains("x-ms-content-crc64")); + Assert.True(response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)); break; default: Assert.Fail("Test can't validate given algorithm type."); @@ -1788,7 +1934,7 @@ public virtual async Task DownloadOverwritesDefaultClientValidationOptions( Assert.True(response.Headers.Contains("Content-MD5")); break; case StorageChecksumAlgorithm.StorageCrc64: - Assert.True(response.Headers.Contains("x-ms-content-crc64")); + Assert.True(response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)); break; default: Assert.Fail("Test can't validate given algorithm type."); @@ -1827,7 +1973,7 @@ public virtual async Task DownloadDisablesDefaultClientValidationOptions( { Assert.Fail($"Hash found when none expected."); } - if (response.Headers.Contains("x-ms-content-crc64")) + if (response.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)) { Assert.Fail($"Hash found when none expected."); } @@ -1850,7 +1996,54 @@ public virtual async Task DownloadDisablesDefaultClientValidationOptions( // Assert // no policies this time; just check response headers Assert.False(response.Headers.Contains("Content-MD5")); - Assert.False(response.Headers.Contains("x-ms-content-crc64")); + Assert.False(response.Headers.Contains(Constants.StructuredMessage.CrcStructuredMessage)); + Assert.IsTrue(dest.ToArray().SequenceEqual(data)); + } + + [Test] + public virtual async Task DownloadRecoversFromInterruptWithValidation( + [ValueSource(nameof(GetValidationAlgorithms))] StorageChecksumAlgorithm algorithm) + { + using var _ = AzureEventSourceListener.CreateConsoleLogger(); + int dataLen = algorithm.ResolveAuto() switch { + StorageChecksumAlgorithm.StorageCrc64 => 5 * Constants.MB, // >4MB for multisegment + _ => Constants.KB, + }; + + await using IDisposingContainer disposingContainer = await GetDisposingContainerAsync(); + + // Arrange + var data = GetRandomBuffer(dataLen); + + TClientOptions options = ClientBuilder.GetOptions(); + options.AddPolicy(new FaultyDownloadPipelinePolicy(dataLen - 512, new IOException(), () => { }), HttpPipelinePosition.BeforeTransport); + var client = await GetResourceClientAsync( + disposingContainer.Container, + resourceLength: dataLen, + createResource: true, + options: options); + await SetupDataAsync(client, new MemoryStream(data)); + + var validationOptions = new DownloadTransferValidationOptions { ChecksumAlgorithm = algorithm }; + + // Act + var dest = new MemoryStream(); + var response = await DownloadPartitionAsync(client, dest, validationOptions, new HttpRange(length: data.Length)); + + // Assert + // no policies this time; just check response headers + switch (algorithm.ResolveAuto()) + { + case StorageChecksumAlgorithm.MD5: + Assert.True(response.Headers.Contains("Content-MD5")); + break; + case StorageChecksumAlgorithm.StorageCrc64: + Assert.True(response.Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)); + break; + default: + Assert.Fail("Test can't validate given algorithm type."); + break; + } Assert.IsTrue(dest.ToArray().SequenceEqual(data)); } #endregion @@ -1891,7 +2084,7 @@ public async Task RoundtripWIthDefaults() // make pipeline assertion for checking checksum was present on upload AND download var checksumPipelineAssertion = new AssertMessageContentsPolicy( - checkRequest: GetRequestChecksumAssertion(expectedAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected), + checkRequest: GetRequestChecksumHeaderAssertion(expectedAlgorithm, isChecksumExpected: ParallelUploadIsChecksumExpected), checkResponse: GetResponseChecksumAssertion(expectedAlgorithm)); clientOptions.AddPolicy(checksumPipelineAssertion, HttpPipelinePosition.PerCall); diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingRetriableStreamTests.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingRetriableStreamTests.cs new file mode 100644 index 000000000000..a0f9158040b1 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingRetriableStreamTests.cs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Storage.Shared; +using Azure.Storage.Test.Shared; +using Microsoft.Diagnostics.Tracing.Parsers.AspNet; +using Moq; +using NUnit.Framework; + +namespace Azure.Storage.Tests; + +[TestFixture(true)] +[TestFixture(false)] +public class StructuredMessageDecodingRetriableStreamTests +{ + public bool Async { get; } + + public StructuredMessageDecodingRetriableStreamTests(bool async) + { + Async = async; + } + + private Mock AllExceptionsRetry() + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(rc => rc.IsRetriableException(It.IsAny())).Returns(true); + return mock; + } + + [Test] + public async ValueTask UninterruptedStream() + { + byte[] data = new Random().NextBytesInline(4 * Constants.KB).ToArray(); + byte[] dest = new byte[data.Length]; + + // mock with a simple MemoryStream rather than an actual StructuredMessageDecodingStream + using (Stream src = new MemoryStream(data)) + using (Stream retriableSrc = new StructuredMessageDecodingRetriableStream(src, new(), default, default, default, default, default, 1)) + using (Stream dst = new MemoryStream(dest)) + { + await retriableSrc.CopyToInternal(dst, Async, default); + } + + Assert.AreEqual(data, dest); + } + + [Test] + public async Task Interrupt_DataIntact([Values(true, false)] bool multipleInterrupts) + { + const int segments = 4; + const int segmentLen = Constants.KB; + const int readLen = 128; + const int interruptPos = segmentLen + (3 * readLen) + 10; + + Random r = new(); + byte[] data = r.NextBytesInline(segments * Constants.KB).ToArray(); + byte[] dest = new byte[data.Length]; + + // Mock a decoded data for the mocked StructuredMessageDecodingStream + StructuredMessageDecodingStream.RawDecodedData initialDecodedData = new() + { + TotalSegments = segments, + InnerStreamLength = data.Length, + Flags = StructuredMessage.Flags.StorageCrc64 + }; + // for test purposes, initialize a DecodedData, since we are not actively decoding in this test + initialDecodedData.SegmentCrcs.Add((BinaryPrimitives.ReadUInt64LittleEndian(r.NextBytesInline(StructuredMessage.Crc64Length)), segmentLen)); + + (Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData) Factory(long offset, bool faulty) + { + Stream stream = new MemoryStream(data, (int)offset, data.Length - (int)offset); + if (faulty) + { + stream = new FaultyStream(stream, interruptPos, 1, new Exception(), () => { }); + } + // Mock a decoded data for the mocked StructuredMessageDecodingStream + StructuredMessageDecodingStream.RawDecodedData decodedData = new() + { + TotalSegments = segments, + InnerStreamLength = data.Length, + Flags = StructuredMessage.Flags.StorageCrc64, + }; + // for test purposes, initialize a DecodedData, since we are not actively decoding in this test + initialDecodedData.SegmentCrcs.Add((BinaryPrimitives.ReadUInt64LittleEndian(r.NextBytesInline(StructuredMessage.Crc64Length)), segmentLen)); + return (stream, decodedData); + } + + // mock with a simple MemoryStream rather than an actual StructuredMessageDecodingStream + using (Stream src = new MemoryStream(data)) + using (Stream faultySrc = new FaultyStream(src, interruptPos, 1, new Exception(), () => { })) + using (Stream retriableSrc = new StructuredMessageDecodingRetriableStream( + faultySrc, + initialDecodedData, + default, + offset => Factory(offset, multipleInterrupts), + offset => new ValueTask<(Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData)>(Factory(offset, multipleInterrupts)), + null, + AllExceptionsRetry().Object, + int.MaxValue)) + using (Stream dst = new MemoryStream(dest)) + { + await retriableSrc.CopyToInternal(dst, readLen, Async, default); + } + + Assert.AreEqual(data, dest); + } + + [Test] + public async Task Interrupt_AppropriateRewind() + { + const int segments = 2; + const int segmentLen = Constants.KB; + const int dataLen = segments * segmentLen; + const int readLen = segmentLen / 4; + const int interruptOffset = 10; + const int interruptPos = segmentLen + (2 * readLen) + interruptOffset; + Random r = new(); + + // Mock a decoded data for the mocked StructuredMessageDecodingStream + StructuredMessageDecodingStream.RawDecodedData initialDecodedData = new() + { + TotalSegments = segments, + InnerStreamLength = segments * segmentLen, + Flags = StructuredMessage.Flags.StorageCrc64, + }; + // By the time of interrupt, there will be one segment reported + initialDecodedData.SegmentCrcs.Add((BinaryPrimitives.ReadUInt64LittleEndian(r.NextBytesInline(StructuredMessage.Crc64Length)), segmentLen)); + + Mock mock = new(MockBehavior.Strict); + mock.SetupGet(s => s.CanRead).Returns(true); + mock.SetupGet(s => s.CanSeek).Returns(false); + if (Async) + { + mock.SetupSequence(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), default)) + .Returns(Task.FromResult(readLen)) // start first segment + .Returns(Task.FromResult(readLen)) + .Returns(Task.FromResult(readLen)) + .Returns(Task.FromResult(readLen)) // finish first segment + .Returns(Task.FromResult(readLen)) // start second segment + .Returns(Task.FromResult(readLen)) + // faulty stream interrupt + .Returns(Task.FromResult(readLen * 2)) // restart second segment. fast-forward uses an internal 4KB buffer, so it will leap the 512 byte catchup all at once + .Returns(Task.FromResult(readLen)) + .Returns(Task.FromResult(readLen)) // end second segment + .Returns(Task.FromResult(0)) // signal end of stream + .Returns(Task.FromResult(0)) // second signal needed for stream wrapping reasons + ; + } + else + { + mock.SetupSequence(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(readLen) // start first segment + .Returns(readLen) + .Returns(readLen) + .Returns(readLen) // finish first segment + .Returns(readLen) // start second segment + .Returns(readLen) + // faulty stream interrupt + .Returns(readLen * 2) // restart second segment. fast-forward uses an internal 4KB buffer, so it will leap the 512 byte catchup all at once + .Returns(readLen) + .Returns(readLen) // end second segment + .Returns(0) // signal end of stream + .Returns(0) // second signal needed for stream wrapping reasons + ; + } + Stream faultySrc = new FaultyStream(mock.Object, interruptPos, 1, new Exception(), default); + Stream retriableSrc = new StructuredMessageDecodingRetriableStream( + faultySrc, + initialDecodedData, + default, + offset => (mock.Object, new()), + offset => new(Task.FromResult((mock.Object, new StructuredMessageDecodingStream.RawDecodedData()))), + null, + AllExceptionsRetry().Object, + 1); + + int totalRead = 0; + int read = 0; + byte[] buf = new byte[readLen]; + if (Async) + { + while ((read = await retriableSrc.ReadAsync(buf, 0, buf.Length)) > 0) + { + totalRead += read; + } + } + else + { + while ((read = retriableSrc.Read(buf, 0, buf.Length)) > 0) + { + totalRead += read; + } + } + await retriableSrc.CopyToInternal(Stream.Null, readLen, Async, default); + + // Asserts we read exactly the data length, excluding the fastforward of the inner stream + Assert.That(totalRead, Is.EqualTo(dataLen)); + } + + [Test] + public async Task Interrupt_ProperDecode([Values(true, false)] bool multipleInterrupts) + { + // decoding stream inserts a buffered layer of 4 KB. use larger sizes to avoid interference from it. + const int segments = 4; + const int segmentLen = 128 * Constants.KB; + const int readLen = 8 * Constants.KB; + const int interruptPos = segmentLen + (3 * readLen) + 10; + + Random r = new(); + byte[] data = r.NextBytesInline(segments * Constants.KB).ToArray(); + byte[] dest = new byte[data.Length]; + + (Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData) Factory(long offset, bool faulty) + { + Stream stream = new MemoryStream(data, (int)offset, data.Length - (int)offset); + stream = new StructuredMessageEncodingStream(stream, segmentLen, StructuredMessage.Flags.StorageCrc64); + if (faulty) + { + stream = new FaultyStream(stream, interruptPos, 1, new Exception(), () => { }); + } + return StructuredMessageDecodingStream.WrapStream(stream); + } + + (Stream decodingStream, StructuredMessageDecodingStream.RawDecodedData decodedData) = Factory(0, true); + using Stream retriableSrc = new StructuredMessageDecodingRetriableStream( + decodingStream, + decodedData, + default, + offset => Factory(offset, multipleInterrupts), + offset => new ValueTask<(Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData)>(Factory(offset, multipleInterrupts)), + null, + AllExceptionsRetry().Object, + int.MaxValue); + using Stream dst = new MemoryStream(dest); + + await retriableSrc.CopyToInternal(dst, readLen, Async, default); + + Assert.AreEqual(data, dest); + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingStreamTests.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingStreamTests.cs new file mode 100644 index 000000000000..2789672df497 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageDecodingStreamTests.cs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Binary; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.Storage.Blobs.Tests; +using Azure.Storage.Shared; +using NUnit.Framework; +using static Azure.Storage.Shared.StructuredMessage; + +namespace Azure.Storage.Tests +{ + [TestFixture(ReadMethod.SyncArray)] + [TestFixture(ReadMethod.AsyncArray)] +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [TestFixture(ReadMethod.SyncSpan)] + [TestFixture(ReadMethod.AsyncMemory)] +#endif + public class StructuredMessageDecodingStreamTests + { + // Cannot just implement as passthru in the stream + // Must test each one + public enum ReadMethod + { + SyncArray, + AsyncArray, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + SyncSpan, + AsyncMemory +#endif + } + + public ReadMethod Method { get; } + + public StructuredMessageDecodingStreamTests(ReadMethod method) + { + Method = method; + } + + private class CopyStreamException : Exception + { + public long TotalCopied { get; } + + public CopyStreamException(Exception inner, long totalCopied) + : base($"Failed read after {totalCopied}-many bytes.", inner) + { + TotalCopied = totalCopied; + } + } + private async ValueTask CopyStream(Stream source, Stream destination, int bufferSize = 81920) // number default for CopyTo impl + { + byte[] buf = new byte[bufferSize]; + int read; + long totalRead = 0; + try + { + switch (Method) + { + case ReadMethod.SyncArray: + while ((read = source.Read(buf, 0, bufferSize)) > 0) + { + totalRead += read; + destination.Write(buf, 0, read); + } + break; + case ReadMethod.AsyncArray: + while ((read = await source.ReadAsync(buf, 0, bufferSize)) > 0) + { + totalRead += read; + await destination.WriteAsync(buf, 0, read); + } + break; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + case ReadMethod.SyncSpan: + while ((read = source.Read(new Span(buf))) > 0) + { + totalRead += read; + destination.Write(new Span(buf, 0, read)); + } + break; + case ReadMethod.AsyncMemory: + while ((read = await source.ReadAsync(new Memory(buf))) > 0) + { + totalRead += read; + await destination.WriteAsync(new Memory(buf, 0, read)); + } + break; +#endif + } + destination.Flush(); + } + catch (Exception ex) + { + throw new CopyStreamException(ex, totalRead); + } + return totalRead; + } + + [Test] + [Pairwise] + public async Task DecodesData( + [Values(2048, 2005)] int dataLength, + [Values(default, 512)] int? seglen, + [Values(8*Constants.KB, 512, 530, 3)] int readLen, + [Values(true, false)] bool useCrc) + { + int segmentContentLength = seglen ?? int.MaxValue; + Flags flags = useCrc ? Flags.StorageCrc64 : Flags.None; + + byte[] originalData = new byte[dataLength]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentContentLength, flags); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + byte[] decodedData; + using (MemoryStream dest = new()) + { + await CopyStream(decodingStream, dest, readLen); + decodedData = dest.ToArray(); + } + + Assert.That(new Span(decodedData).SequenceEqual(originalData)); + } + + [Test] + public void BadStreamBadVersion() + { + byte[] originalData = new byte[1024]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, 256, Flags.StorageCrc64); + + encodedData[0] = byte.MaxValue; + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [Test] + public async Task BadSegmentCrcThrows() + { + const int segmentLength = 256; + Random r = new(); + + byte[] originalData = new byte[2048]; + r.NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentLength, Flags.StorageCrc64); + + const int badBytePos = 1024; + encodedData[badBytePos] = (byte)~encodedData[badBytePos]; + + MemoryStream encodedDataStream = new(encodedData); + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(encodedDataStream); + + // manual try/catch to validate the proccess failed mid-stream rather than the end + const int copyBufferSize = 4; + bool caught = false; + try + { + await CopyStream(decodingStream, Stream.Null, copyBufferSize); + } + catch (CopyStreamException ex) + { + caught = true; + Assert.That(ex.TotalCopied, Is.LessThanOrEqualTo(badBytePos)); + } + Assert.That(caught); + } + + [Test] + public void BadStreamCrcThrows() + { + const int segmentLength = 256; + Random r = new(); + + byte[] originalData = new byte[2048]; + r.NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentLength, Flags.StorageCrc64); + + encodedData[originalData.Length - 1] = (byte)~encodedData[originalData.Length - 1]; + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [Test] + public void BadStreamWrongContentLength() + { + byte[] originalData = new byte[1024]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, 256, Flags.StorageCrc64); + + BinaryPrimitives.WriteInt64LittleEndian(new Span(encodedData, V1_0.StreamHeaderMessageLengthOffset, 8), 123456789L); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [TestCase(-1)] + [TestCase(1)] + public void BadStreamWrongSegmentCount(int difference) + { + const int dataSize = 1024; + const int segmentSize = 256; + const int numSegments = 4; + + byte[] originalData = new byte[dataSize]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentSize, Flags.StorageCrc64); + + // rewrite the segment count to be different than the actual number of segments + BinaryPrimitives.WriteInt16LittleEndian( + new Span(encodedData, V1_0.StreamHeaderSegmentCountOffset, 2), (short)(numSegments + difference)); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [Test] + public void BadStreamWrongSegmentNum() + { + byte[] originalData = new byte[1024]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, 256, Flags.StorageCrc64); + + BinaryPrimitives.WriteInt16LittleEndian( + new Span(encodedData, V1_0.StreamHeaderLength + V1_0.SegmentHeaderNumOffset, 2), 123); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(encodedData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [Test] + [Combinatorial] + public async Task BadStreamWrongContentLength( + [Values(-1, 1)] int difference, + [Values(true, false)] bool lengthProvided) + { + byte[] originalData = new byte[1024]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, 256, Flags.StorageCrc64); + + BinaryPrimitives.WriteInt64LittleEndian( + new Span(encodedData, V1_0.StreamHeaderMessageLengthOffset, 8), + encodedData.Length + difference); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream( + new MemoryStream(encodedData), + lengthProvided ? (long?)encodedData.Length : default); + + // manual try/catch with tiny buffer to validate the proccess failed mid-stream rather than the end + const int copyBufferSize = 4; + bool caught = false; + try + { + await CopyStream(decodingStream, Stream.Null, copyBufferSize); + } + catch (CopyStreamException ex) + { + caught = true; + if (lengthProvided) + { + Assert.That(ex.TotalCopied, Is.EqualTo(0)); + } + else + { + Assert.That(ex.TotalCopied, Is.EqualTo(originalData.Length)); + } + } + Assert.That(caught); + } + + [Test] + public void BadStreamMissingExpectedStreamFooter() + { + byte[] originalData = new byte[1024]; + new Random().NextBytes(originalData); + byte[] encodedData = StructuredMessageHelper.MakeEncodedData(originalData, 256, Flags.StorageCrc64); + + byte[] brokenData = new byte[encodedData.Length - Crc64Length]; + new Span(encodedData, 0, encodedData.Length - Crc64Length).CopyTo(brokenData); + + (Stream decodingStream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream(brokenData)); + Assert.That(async () => await CopyStream(decodingStream, Stream.Null), Throws.InnerException.TypeOf()); + } + + [Test] + public void NoSeek() + { + (Stream stream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream()); + + Assert.That(stream.CanSeek, Is.False); + Assert.That(() => stream.Length, Throws.TypeOf()); + Assert.That(() => stream.Position, Throws.TypeOf()); + Assert.That(() => stream.Position = 0, Throws.TypeOf()); + Assert.That(() => stream.Seek(0, SeekOrigin.Begin), Throws.TypeOf()); + } + + [Test] + public void NoWrite() + { + (Stream stream, _) = StructuredMessageDecodingStream.WrapStream(new MemoryStream()); + byte[] data = new byte[1024]; + new Random().NextBytes(data); + + Assert.That(stream.CanWrite, Is.False); + Assert.That(() => stream.Write(data, 0, data.Length), + Throws.TypeOf()); + Assert.That(async () => await stream.WriteAsync(data, 0, data.Length, CancellationToken.None), + Throws.TypeOf()); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + Assert.That(() => stream.Write(new Span(data)), + Throws.TypeOf()); + Assert.That(async () => await stream.WriteAsync(new Memory(data), CancellationToken.None), + Throws.TypeOf()); +#endif + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageEncodingStreamTests.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageEncodingStreamTests.cs new file mode 100644 index 000000000000..e0f91dee7de3 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageEncodingStreamTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Azure.Storage.Blobs.Tests; +using Azure.Storage.Shared; +using NUnit.Framework; +using static Azure.Storage.Shared.StructuredMessage; + +namespace Azure.Storage.Tests +{ + [TestFixture(ReadMethod.SyncArray)] + [TestFixture(ReadMethod.AsyncArray)] +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [TestFixture(ReadMethod.SyncSpan)] + [TestFixture(ReadMethod.AsyncMemory)] +#endif + public class StructuredMessageEncodingStreamTests + { + // Cannot just implement as passthru in the stream + // Must test each one + public enum ReadMethod + { + SyncArray, + AsyncArray, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + SyncSpan, + AsyncMemory +#endif + } + + public ReadMethod Method { get; } + + public StructuredMessageEncodingStreamTests(ReadMethod method) + { + Method = method; + } + + private async ValueTask CopyStream(Stream source, Stream destination, int bufferSize = 81920) // number default for CopyTo impl + { + byte[] buf = new byte[bufferSize]; + int read; + switch (Method) + { + case ReadMethod.SyncArray: + while ((read = source.Read(buf, 0, bufferSize)) > 0) + { + destination.Write(buf, 0, read); + } + break; + case ReadMethod.AsyncArray: + while ((read = await source.ReadAsync(buf, 0, bufferSize)) > 0) + { + await destination.WriteAsync(buf, 0, read); + } + break; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + case ReadMethod.SyncSpan: + while ((read = source.Read(new Span(buf))) > 0) + { + destination.Write(new Span(buf, 0, read)); + } + break; + case ReadMethod.AsyncMemory: + while ((read = await source.ReadAsync(new Memory(buf))) > 0) + { + await destination.WriteAsync(new Memory(buf, 0, read)); + } + break; +#endif + } + destination.Flush(); + } + + [Test] + [Pairwise] + public async Task EncodesData( + [Values(2048, 2005)] int dataLength, + [Values(default, 512)] int? seglen, + [Values(8 * Constants.KB, 512, 530, 3)] int readLen, + [Values(true, false)] bool useCrc) + { + int segmentContentLength = seglen ?? int.MaxValue; + Flags flags = useCrc ? Flags.StorageCrc64 : Flags.None; + + byte[] originalData = new byte[dataLength]; + new Random().NextBytes(originalData); + byte[] expectedEncodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentContentLength, flags); + + Stream encodingStream = new StructuredMessageEncodingStream(new MemoryStream(originalData), segmentContentLength, flags); + byte[] encodedData; + using (MemoryStream dest = new()) + { + await CopyStream(encodingStream, dest, readLen); + encodedData = dest.ToArray(); + } + + Assert.That(new Span(encodedData).SequenceEqual(expectedEncodedData)); + } + + [TestCase(0, 0)] // start + [TestCase(5, 0)] // partway through stream header + [TestCase(V1_0.StreamHeaderLength, 0)] // start of segment + [TestCase(V1_0.StreamHeaderLength + 3, 0)] // partway through segment header + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength, 0)] // start of segment content + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 123, 123)] // partway through segment content + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 512, 512)] // start of segment footer + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 515, 512)] // partway through segment footer + [TestCase(V1_0.StreamHeaderLength + 3*V1_0.SegmentHeaderLength + 2*Crc64Length + 1500, 1500)] // partway through not first segment content + public async Task Seek(int targetRewindOffset, int expectedInnerStreamPosition) + { + const int segmentLength = 512; + const int dataLength = 2055; + byte[] data = new byte[dataLength]; + new Random().NextBytes(data); + + MemoryStream dataStream = new(data); + StructuredMessageEncodingStream encodingStream = new(dataStream, segmentLength, Flags.StorageCrc64); + + // no support for seeking past existing read, need to consume whole stream before seeking + await CopyStream(encodingStream, Stream.Null); + + encodingStream.Position = targetRewindOffset; + Assert.That(encodingStream.Position, Is.EqualTo(targetRewindOffset)); + Assert.That(dataStream.Position, Is.EqualTo(expectedInnerStreamPosition)); + } + + [TestCase(0)] // start + [TestCase(5)] // partway through stream header + [TestCase(V1_0.StreamHeaderLength)] // start of segment + [TestCase(V1_0.StreamHeaderLength + 3)] // partway through segment header + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength)] // start of segment content + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 123)] // partway through segment content + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 512)] // start of segment footer + [TestCase(V1_0.StreamHeaderLength + V1_0.SegmentHeaderLength + 515)] // partway through segment footer + [TestCase(V1_0.StreamHeaderLength + 2 * V1_0.SegmentHeaderLength + Crc64Length + 1500)] // partway through not first segment content + public async Task SupportsRewind(int targetRewindOffset) + { + const int segmentLength = 512; + const int dataLength = 2055; + byte[] data = new byte[dataLength]; + new Random().NextBytes(data); + + Stream encodingStream = new StructuredMessageEncodingStream(new MemoryStream(data), segmentLength, Flags.StorageCrc64); + byte[] encodedData1; + using (MemoryStream dest = new()) + { + await CopyStream(encodingStream, dest); + encodedData1 = dest.ToArray(); + } + encodingStream.Position = targetRewindOffset; + byte[] encodedData2; + using (MemoryStream dest = new()) + { + await CopyStream(encodingStream, dest); + encodedData2 = dest.ToArray(); + } + + Assert.That(new Span(encodedData1).Slice(targetRewindOffset).SequenceEqual(encodedData2)); + } + + [Test] + public async Task SupportsFastForward() + { + const int segmentLength = 512; + const int dataLength = 2055; + byte[] data = new byte[dataLength]; + new Random().NextBytes(data); + + // must have read stream to fastforward. so read whole stream upfront & save result to check later + Stream encodingStream = new StructuredMessageEncodingStream(new MemoryStream(data), segmentLength, Flags.StorageCrc64); + byte[] encodedData; + using (MemoryStream dest = new()) + { + await CopyStream(encodingStream, dest); + encodedData = dest.ToArray(); + } + + encodingStream.Position = 0; + + bool skip = false; + const int increment = 499; + while (encodingStream.Position < encodingStream.Length) + { + if (skip) + { + encodingStream.Position = Math.Min(dataLength, encodingStream.Position + increment); + skip = !skip; + continue; + } + ReadOnlyMemory expected = new(encodedData, (int)encodingStream.Position, + (int)Math.Min(increment, encodedData.Length - encodingStream.Position)); + ReadOnlyMemory actual; + using (MemoryStream dest = new(increment)) + { + await CopyStream(WindowStream.GetWindow(encodingStream, increment), dest); + actual = dest.ToArray(); + } + Assert.That(expected.Span.SequenceEqual(actual.Span)); + skip = !skip; + } + } + + [Test] + public void NotSupportsFastForwardBeyondLatestRead() + { + const int segmentLength = 512; + const int dataLength = 2055; + byte[] data = new byte[dataLength]; + new Random().NextBytes(data); + + Stream encodingStream = new StructuredMessageEncodingStream(new MemoryStream(data), segmentLength, Flags.StorageCrc64); + + Assert.That(() => encodingStream.Position = 123, Throws.TypeOf()); + } + + [Test] + [Pairwise] + public async Task WrapperStreamCorrectData( + [Values(2048, 2005)] int dataLength, + [Values(8 * Constants.KB, 512, 530, 3)] int readLen) + { + int segmentContentLength = dataLength; + Flags flags = Flags.StorageCrc64; + + byte[] originalData = new byte[dataLength]; + new Random().NextBytes(originalData); + byte[] crc = CrcInline(originalData); + byte[] expectedEncodedData = StructuredMessageHelper.MakeEncodedData(originalData, segmentContentLength, flags); + + Stream encodingStream = new StructuredMessagePrecalculatedCrcWrapperStream(new MemoryStream(originalData), crc); + byte[] encodedData; + using (MemoryStream dest = new()) + { + await CopyStream(encodingStream, dest, readLen); + encodedData = dest.ToArray(); + } + + Assert.That(new Span(encodedData).SequenceEqual(expectedEncodedData)); + } + + private static void AssertExpectedStreamHeader(ReadOnlySpan actual, int originalDataLength, Flags flags, int expectedSegments) + { + int expectedFooterLen = flags.HasFlag(Flags.StorageCrc64) ? Crc64Length : 0; + + Assert.That(actual.Length, Is.EqualTo(V1_0.StreamHeaderLength)); + Assert.That(actual[0], Is.EqualTo(1)); + Assert.That(BinaryPrimitives.ReadInt64LittleEndian(actual.Slice(1, 8)), + Is.EqualTo(V1_0.StreamHeaderLength + expectedSegments * (V1_0.SegmentHeaderLength + expectedFooterLen) + originalDataLength)); + Assert.That(BinaryPrimitives.ReadInt16LittleEndian(actual.Slice(9, 2)), Is.EqualTo((short)flags)); + Assert.That(BinaryPrimitives.ReadInt16LittleEndian(actual.Slice(11, 2)), Is.EqualTo((short)expectedSegments)); + } + + private static void AssertExpectedSegmentHeader(ReadOnlySpan actual, int segmentNum, long contentLength) + { + Assert.That(BinaryPrimitives.ReadInt16LittleEndian(actual.Slice(0, 2)), Is.EqualTo((short) segmentNum)); + Assert.That(BinaryPrimitives.ReadInt64LittleEndian(actual.Slice(2, 8)), Is.EqualTo(contentLength)); + } + + private static byte[] CrcInline(ReadOnlySpan data) + { + var crc = StorageCrc64HashAlgorithm.Create(); + crc.Append(data); + return crc.GetCurrentHash(); + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageHelper.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageHelper.cs new file mode 100644 index 000000000000..59e80320d96a --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageHelper.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Storage.Shared; +using static Azure.Storage.Shared.StructuredMessage; + +namespace Azure.Storage.Blobs.Tests +{ + internal class StructuredMessageHelper + { + public static byte[] MakeEncodedData(ReadOnlySpan data, long segmentContentLength, Flags flags) + { + int segmentCount = (int) Math.Ceiling(data.Length / (double)segmentContentLength); + int segmentFooterLen = flags.HasFlag(Flags.StorageCrc64) ? 8 : 0; + int streamFooterLen = flags.HasFlag(Flags.StorageCrc64) ? 8 : 0; + + byte[] encodedData = new byte[ + V1_0.StreamHeaderLength + + segmentCount*(V1_0.SegmentHeaderLength + segmentFooterLen) + + streamFooterLen + + data.Length]; + V1_0.WriteStreamHeader( + new Span(encodedData, 0, V1_0.StreamHeaderLength), + encodedData.Length, + flags, + segmentCount); + + int i = V1_0.StreamHeaderLength; + int j = 0; + foreach (int seg in Enumerable.Range(1, segmentCount)) + { + int segContentLen = Math.Min((int)segmentContentLength, data.Length - j); + V1_0.WriteSegmentHeader( + new Span(encodedData, i, V1_0.SegmentHeaderLength), + seg, + segContentLen); + i += V1_0.SegmentHeaderLength; + + data.Slice(j, segContentLen) + .CopyTo(new Span(encodedData).Slice(i)); + i += segContentLen; + + if (flags.HasFlag(Flags.StorageCrc64)) + { + var crc = StorageCrc64HashAlgorithm.Create(); + crc.Append(data.Slice(j, segContentLen)); + crc.GetCurrentHash(new Span(encodedData, i, Crc64Length)); + i += Crc64Length; + } + j += segContentLen; + } + + if (flags.HasFlag(Flags.StorageCrc64)) + { + var crc = StorageCrc64HashAlgorithm.Create(); + crc.Append(data); + crc.GetCurrentHash(new Span(encodedData, i, Crc64Length)); + } + + return encodedData; + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageStreamRoundtripTests.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageStreamRoundtripTests.cs new file mode 100644 index 000000000000..61583aa1ebe4 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageStreamRoundtripTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Azure.Storage.Shared; +using NUnit.Framework; +using static Azure.Storage.Shared.StructuredMessage; + +namespace Azure.Storage.Tests +{ + [TestFixture(ReadMethod.SyncArray)] + [TestFixture(ReadMethod.AsyncArray)] +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [TestFixture(ReadMethod.SyncSpan)] + [TestFixture(ReadMethod.AsyncMemory)] +#endif + public class StructuredMessageStreamRoundtripTests + { + // Cannot just implement as passthru in the stream + // Must test each one + public enum ReadMethod + { + SyncArray, + AsyncArray, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + SyncSpan, + AsyncMemory +#endif + } + + public ReadMethod Method { get; } + + public StructuredMessageStreamRoundtripTests(ReadMethod method) + { + Method = method; + } + + private class CopyStreamException : Exception + { + public long TotalCopied { get; } + + public CopyStreamException(Exception inner, long totalCopied) + : base($"Failed read after {totalCopied}-many bytes.", inner) + { + TotalCopied = totalCopied; + } + } + private async ValueTask CopyStream(Stream source, Stream destination, int bufferSize = 81920) // number default for CopyTo impl + { + byte[] buf = new byte[bufferSize]; + int read; + long totalRead = 0; + try + { + switch (Method) + { + case ReadMethod.SyncArray: + while ((read = source.Read(buf, 0, bufferSize)) > 0) + { + totalRead += read; + destination.Write(buf, 0, read); + } + break; + case ReadMethod.AsyncArray: + while ((read = await source.ReadAsync(buf, 0, bufferSize)) > 0) + { + totalRead += read; + await destination.WriteAsync(buf, 0, read); + } + break; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + case ReadMethod.SyncSpan: + while ((read = source.Read(new Span(buf))) > 0) + { + totalRead += read; + destination.Write(new Span(buf, 0, read)); + } + break; + case ReadMethod.AsyncMemory: + while ((read = await source.ReadAsync(new Memory(buf))) > 0) + { + totalRead += read; + await destination.WriteAsync(new Memory(buf, 0, read)); + } + break; +#endif + } + destination.Flush(); + } + catch (Exception ex) + { + throw new CopyStreamException(ex, totalRead); + } + return totalRead; + } + + [Test] + [Pairwise] + public async Task RoundTrip( + [Values(2048, 2005)] int dataLength, + [Values(default, 512)] int? seglen, + [Values(8 * Constants.KB, 512, 530, 3)] int readLen, + [Values(true, false)] bool useCrc) + { + int segmentLength = seglen ?? int.MaxValue; + Flags flags = useCrc ? Flags.StorageCrc64 : Flags.None; + + byte[] originalData = new byte[dataLength]; + new Random().NextBytes(originalData); + + byte[] roundtripData; + using (MemoryStream source = new(originalData)) + using (Stream encode = new StructuredMessageEncodingStream(source, segmentLength, flags)) + using (Stream decode = StructuredMessageDecodingStream.WrapStream(encode).DecodedStream) + using (MemoryStream dest = new()) + { + await CopyStream(source, dest, readLen); + roundtripData = dest.ToArray(); + } + + Assert.That(originalData.SequenceEqual(roundtripData)); + } + } +} diff --git a/sdk/storage/Azure.Storage.Common/tests/StructuredMessageTests.cs b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageTests.cs new file mode 100644 index 000000000000..b4f1dfe17824 --- /dev/null +++ b/sdk/storage/Azure.Storage.Common/tests/StructuredMessageTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using NUnit.Framework; +using static Azure.Storage.Shared.StructuredMessage; + +namespace Azure.Storage.Tests +{ + public class StructuredMessageTests + { + [TestCase(1024, Flags.None, 2)] + [TestCase(2000, Flags.StorageCrc64, 4)] + public void EncodeStreamHeader(int messageLength, int flags, int numSegments) + { + Span encoding = new(new byte[V1_0.StreamHeaderLength]); + V1_0.WriteStreamHeader(encoding, messageLength, (Flags)flags, numSegments); + + Assert.That(encoding[0], Is.EqualTo((byte)1)); + Assert.That(BinaryPrimitives.ReadUInt64LittleEndian(encoding.Slice(1, 8)), Is.EqualTo(messageLength)); + Assert.That(BinaryPrimitives.ReadUInt16LittleEndian(encoding.Slice(9, 2)), Is.EqualTo(flags)); + Assert.That(BinaryPrimitives.ReadUInt16LittleEndian(encoding.Slice(11, 2)), Is.EqualTo(numSegments)); + } + + [TestCase(V1_0.StreamHeaderLength)] + [TestCase(V1_0.StreamHeaderLength + 1)] + [TestCase(V1_0.StreamHeaderLength - 1)] + public void EncodeStreamHeaderRejectBadBufferSize(int bufferSize) + { + Random r = new(); + byte[] encoding = new byte[bufferSize]; + + void Action() => V1_0.WriteStreamHeader(encoding, r.Next(2, int.MaxValue), Flags.StorageCrc64, r.Next(2, int.MaxValue)); + if (bufferSize < V1_0.StreamHeaderLength) + { + Assert.That(Action, Throws.ArgumentException); + } + else + { + Assert.That(Action, Throws.Nothing); + } + } + + [TestCase(1, 1024)] + [TestCase(5, 39578)] + public void EncodeSegmentHeader(int segmentNum, int contentLength) + { + Span encoding = new(new byte[V1_0.SegmentHeaderLength]); + V1_0.WriteSegmentHeader(encoding, segmentNum, contentLength); + + Assert.That(BinaryPrimitives.ReadUInt16LittleEndian(encoding.Slice(0, 2)), Is.EqualTo(segmentNum)); + Assert.That(BinaryPrimitives.ReadUInt64LittleEndian(encoding.Slice(2, 8)), Is.EqualTo(contentLength)); + } + + [TestCase(V1_0.SegmentHeaderLength)] + [TestCase(V1_0.SegmentHeaderLength + 1)] + [TestCase(V1_0.SegmentHeaderLength - 1)] + public void EncodeSegmentHeaderRejectBadBufferSize(int bufferSize) + { + Random r = new(); + byte[] encoding = new byte[bufferSize]; + + void Action() => V1_0.WriteSegmentHeader(encoding, r.Next(1, int.MaxValue), r.Next(2, int.MaxValue)); + if (bufferSize < V1_0.SegmentHeaderLength) + { + Assert.That(Action, Throws.ArgumentException); + } + else + { + Assert.That(Action, Throws.Nothing); + } + } + + [TestCase(true)] + [TestCase(false)] + public void EncodeSegmentFooter(bool useCrc) + { + Span encoding = new(new byte[Crc64Length]); + Span crc = useCrc ? new Random().NextBytesInline(Crc64Length) : default; + V1_0.WriteSegmentFooter(encoding, crc); + + if (useCrc) + { + Assert.That(encoding.SequenceEqual(crc), Is.True); + } + else + { + Assert.That(encoding.SequenceEqual(new Span(new byte[Crc64Length])), Is.True); + } + } + + [TestCase(Crc64Length)] + [TestCase(Crc64Length + 1)] + [TestCase(Crc64Length - 1)] + public void EncodeSegmentFooterRejectBadBufferSize(int bufferSize) + { + byte[] encoding = new byte[bufferSize]; + byte[] crc = new byte[Crc64Length]; + new Random().NextBytes(crc); + + void Action() => V1_0.WriteSegmentFooter(encoding, crc); + if (bufferSize < Crc64Length) + { + Assert.That(Action, Throws.ArgumentException); + } + else + { + Assert.That(Action, Throws.Nothing); + } + } + } +} diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs.Files.Shares/tests/Azure.Storage.DataMovement.Blobs.Files.Shares.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement.Blobs.Files.Shares/tests/Azure.Storage.DataMovement.Blobs.Files.Shares.Tests.csproj index 2daa772f7437..aabe9591a388 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs.Files.Shares/tests/Azure.Storage.DataMovement.Blobs.Files.Shares.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs.Files.Shares/tests/Azure.Storage.DataMovement.Blobs.Files.Shares.Tests.csproj @@ -34,6 +34,7 @@ + diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/samples/Azure.Storage.DataMovement.Blobs.Samples.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement.Blobs/samples/Azure.Storage.DataMovement.Blobs.Samples.Tests.csproj index 7ab901e963e0..30d4b1f79daa 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/samples/Azure.Storage.DataMovement.Blobs.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/samples/Azure.Storage.DataMovement.Blobs.Samples.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/src/DataMovementBlobsExtensions.cs b/sdk/storage/Azure.Storage.DataMovement.Blobs/src/DataMovementBlobsExtensions.cs index 2c85f4b23f82..e634e05f5608 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/src/DataMovementBlobsExtensions.cs +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/src/DataMovementBlobsExtensions.cs @@ -101,7 +101,7 @@ internal static StorageResourceItemProperties ToStorageResourceItemProperties(th ContentRange contentRange = !string.IsNullOrWhiteSpace(result?.Details?.ContentRange) ? ContentRange.Parse(result.Details.ContentRange) : default; if (contentRange != default) { - size = contentRange.Size; + size = contentRange.TotalResourceLength; } return new StorageResourceItemProperties() @@ -155,7 +155,7 @@ internal static StorageResourceReadStreamResult ToReadStreamStorageResourceInfo( if (contentRange != default) { range = ContentRange.ToHttpRange(contentRange); - size = contentRange.Size; + size = contentRange.TotalResourceLength; } else if (result.Details.ContentLength > 0) { diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/Azure.Storage.DataMovement.Blobs.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/Azure.Storage.DataMovement.Blobs.Tests.csproj index 9f8e04ed39ed..ed6d0b33dc81 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/Azure.Storage.DataMovement.Blobs.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/Azure.Storage.DataMovement.Blobs.Tests.csproj @@ -22,11 +22,15 @@ + + + + @@ -40,6 +44,7 @@ + diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/samples/Azure.Storage.DataMovement.Files.Shares.Samples.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/samples/Azure.Storage.DataMovement.Files.Shares.Samples.Tests.csproj index 9cde066f64eb..6a472b9f7415 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/samples/Azure.Storage.DataMovement.Files.Shares.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/samples/Azure.Storage.DataMovement.Files.Shares.Samples.Tests.csproj @@ -1,4 +1,4 @@ - + $(RequiredTargetFrameworks) Microsoft Azure.Storage.DataMovement.Files.Shares client library samples @@ -11,6 +11,7 @@ + diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/DataMovementSharesExtensions.cs b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/DataMovementSharesExtensions.cs index b4e74f76041a..6eb1df9193da 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/DataMovementSharesExtensions.cs +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/DataMovementSharesExtensions.cs @@ -665,7 +665,7 @@ internal static StorageResourceReadStreamResult ToStorageResourceReadStreamResul ContentRange contentRange = !string.IsNullOrWhiteSpace(info?.Details?.ContentRange) ? ContentRange.Parse(info.Details.ContentRange) : default; if (contentRange != default) { - size = contentRange.Size; + size = contentRange.TotalResourceLength; } return new StorageResourceReadStreamResult( @@ -673,7 +673,7 @@ internal static StorageResourceReadStreamResult ToStorageResourceReadStreamResul range: ContentRange.ToHttpRange(contentRange), new StorageResourceItemProperties() { - ResourceLength = contentRange.Size, + ResourceLength = contentRange.TotalResourceLength, ETag = info.Details.ETag, LastModifiedTime = info.Details.LastModified, RawProperties = properties diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/tests/Azure.Storage.DataMovement.Files.Shares.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/tests/Azure.Storage.DataMovement.Files.Shares.Tests.csproj index 517ed4bdce44..009bd06c7380 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/tests/Azure.Storage.DataMovement.Files.Shares.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/tests/Azure.Storage.DataMovement.Files.Shares.Tests.csproj @@ -26,6 +26,7 @@ + diff --git a/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj b/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj index 109fdf224a43..9bd3a1729db4 100644 --- a/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj +++ b/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj @@ -1,4 +1,4 @@ - + $(RequiredTargetFrameworks) true diff --git a/sdk/storage/Azure.Storage.DataMovement/tests/Azure.Storage.DataMovement.Tests.csproj b/sdk/storage/Azure.Storage.DataMovement/tests/Azure.Storage.DataMovement.Tests.csproj index b5e3c4235997..7a40eb802644 100644 --- a/sdk/storage/Azure.Storage.DataMovement/tests/Azure.Storage.DataMovement.Tests.csproj +++ b/sdk/storage/Azure.Storage.DataMovement/tests/Azure.Storage.DataMovement.Tests.csproj @@ -34,6 +34,7 @@ + diff --git a/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md b/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md index f84fa2987367..f7cdc3515e09 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md @@ -1,14 +1,15 @@ # Release History -## 12.25.0-beta.2 (Unreleased) +## 12.26.0-beta.1 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed +- Added support for service version 2026-04-06. +- Added support for Content Validation via Structured Message +- Added cross-tenant support for Principal-Bound User Delegation SAS. +- Added support for Dynamic User Delegation SAS. ### Other Changes +- Changed the default concurrency transfer count from 5 to Math.Clamp(Environment.ProcessorCount * 2, 8, 32). This controls the maximum number of concurrent tasks that will be used during large downloads or uploads, and this change should result in higher throughput for these operations by default in most environments. This can be reverted by enabling "Azure.Storage.UseLegacyDefaultConcurrency" in the AppContext switch or "AZURE_STORAGE_USE_LEGACY_DEFAULT_CONCURRENCY" in the environment variable. ## 12.25.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net8.0.cs b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net8.0.cs index 087f3b0c0ed0..6437c6aa9e98 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net8.0.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net8.0.cs @@ -40,6 +40,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class DataLakeDirectoryClient : Azure.Storage.Files.DataLake.DataLakePathClient @@ -481,8 +482,12 @@ public DataLakeServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedK public virtual Azure.AsyncPageable GetFileSystemsAsync(Azure.Storage.Files.DataLake.Models.FileSystemTraits traits, string prefix, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response GetProperties(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetPropertiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Files.DataLake.Models.DataLakeGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Files.DataLake.Models.DataLakeGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Files.DataLake.Models.DataLakeServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Files.DataLake.Models.DataLakeServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response UndeleteFileSystem(string deletedFileSystemName, string deleteFileSystemVersion, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -759,6 +764,13 @@ public DataLakeGetPathsOptions() { } public string StartFrom { get { throw null; } set { } } public bool UserPrincipalName { get { throw null; } set { } } } + public partial class DataLakeGetUserDelegationKeyOptions + { + public DataLakeGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class DataLakeLease { internal DataLakeLease() { } @@ -840,7 +852,9 @@ public static partial class DataLakeModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.DataLake.Models.PathProperties PathProperties(System.DateTimeOffset lastModified, System.DateTimeOffset creationTime, System.Collections.Generic.IDictionary metadata, System.DateTimeOffset copyCompletionTime, string copyStatusDescription, string copyId, string copyProgress, System.Uri copySource, Azure.Storage.Files.DataLake.Models.CopyStatus copyStatus, bool isIncrementalCopy, Azure.Storage.Files.DataLake.Models.DataLakeLeaseDuration leaseDuration, Azure.Storage.Files.DataLake.Models.DataLakeLeaseState leaseState, Azure.Storage.Files.DataLake.Models.DataLakeLeaseStatus leaseStatus, long contentLength, string contentType, Azure.ETag eTag, byte[] contentHash, string contentEncoding, string contentDisposition, string contentLanguage, string cacheControl, string acceptRanges, bool isServerEncrypted, string encryptionKeySha256, string accessTier, string archiveStatus, System.DateTimeOffset accessTierChangeTime, bool isDirectory, string encryptionContext, string owner, string group, string permissions) { throw null; } public static Azure.Storage.Files.DataLake.Models.PathProperties PathProperties(System.DateTimeOffset lastModified, System.DateTimeOffset creationTime, System.Collections.Generic.IDictionary metadata, System.DateTimeOffset copyCompletionTime, string copyStatusDescription, string copyId, string copyProgress, System.Uri copySource, Azure.Storage.Files.DataLake.Models.CopyStatus copyStatus, bool isIncrementalCopy, Azure.Storage.Files.DataLake.Models.DataLakeLeaseDuration leaseDuration, Azure.Storage.Files.DataLake.Models.DataLakeLeaseState leaseState, Azure.Storage.Files.DataLake.Models.DataLakeLeaseStatus leaseStatus, long contentLength, string contentType, Azure.ETag eTag, byte[] contentHash, string contentEncoding, string contentDisposition, string contentLanguage, string cacheControl, string acceptRanges, bool isServerEncrypted, string encryptionKeySha256, string accessTier, string archiveStatus, System.DateTimeOffset accessTierChangeTime, bool isDirectory, string encryptionContext, string owner, string group, string permissions, System.Collections.Generic.IList accessControlList) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.DataLake.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStart, System.DateTimeOffset signedExpiry, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Files.DataLake.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStart = default(System.DateTimeOffset), System.DateTimeOffset signedExpiry = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } public partial class DataLakeOpenReadOptions { @@ -1282,6 +1296,7 @@ public enum RolePermissions public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -1352,6 +1367,8 @@ public DataLakeSasBuilder(Azure.Storage.Sas.DataLakeSasPermissions permissions, public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } set { } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestHeaders { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestQueryParameters { get { throw null; } set { } } public string Resource { get { throw null; } set { } } public System.DateTimeOffset StartsOn { get { throw null; } set { } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -1391,6 +1408,7 @@ public sealed partial class DataLakeSasQueryParameters : Azure.Storage.Sas.SasQu { internal DataLakeSasQueryParameters() { } public static new Azure.Storage.Sas.DataLakeSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.netstandard2.0.cs b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.netstandard2.0.cs index a9b7bb0b5329..9a6006128f42 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.netstandard2.0.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.netstandard2.0.cs @@ -40,6 +40,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class DataLakeDirectoryClient : Azure.Storage.Files.DataLake.DataLakePathClient @@ -481,8 +482,12 @@ public DataLakeServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedK public virtual Azure.AsyncPageable GetFileSystemsAsync(Azure.Storage.Files.DataLake.Models.FileSystemTraits traits, string prefix, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response GetProperties(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetPropertiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Files.DataLake.Models.DataLakeGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Files.DataLake.Models.DataLakeGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Files.DataLake.Models.DataLakeServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Files.DataLake.Models.DataLakeServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response UndeleteFileSystem(string deletedFileSystemName, string deleteFileSystemVersion, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -757,6 +762,13 @@ public DataLakeGetPathsOptions() { } public string StartFrom { get { throw null; } set { } } public bool UserPrincipalName { get { throw null; } set { } } } + public partial class DataLakeGetUserDelegationKeyOptions + { + public DataLakeGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class DataLakeLease { internal DataLakeLease() { } @@ -838,7 +850,9 @@ public static partial class DataLakeModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.DataLake.Models.PathProperties PathProperties(System.DateTimeOffset lastModified, System.DateTimeOffset creationTime, System.Collections.Generic.IDictionary metadata, System.DateTimeOffset copyCompletionTime, string copyStatusDescription, string copyId, string copyProgress, System.Uri copySource, Azure.Storage.Files.DataLake.Models.CopyStatus copyStatus, bool isIncrementalCopy, Azure.Storage.Files.DataLake.Models.DataLakeLeaseDuration leaseDuration, Azure.Storage.Files.DataLake.Models.DataLakeLeaseState leaseState, Azure.Storage.Files.DataLake.Models.DataLakeLeaseStatus leaseStatus, long contentLength, string contentType, Azure.ETag eTag, byte[] contentHash, string contentEncoding, string contentDisposition, string contentLanguage, string cacheControl, string acceptRanges, bool isServerEncrypted, string encryptionKeySha256, string accessTier, string archiveStatus, System.DateTimeOffset accessTierChangeTime, bool isDirectory, string encryptionContext, string owner, string group, string permissions) { throw null; } public static Azure.Storage.Files.DataLake.Models.PathProperties PathProperties(System.DateTimeOffset lastModified, System.DateTimeOffset creationTime, System.Collections.Generic.IDictionary metadata, System.DateTimeOffset copyCompletionTime, string copyStatusDescription, string copyId, string copyProgress, System.Uri copySource, Azure.Storage.Files.DataLake.Models.CopyStatus copyStatus, bool isIncrementalCopy, Azure.Storage.Files.DataLake.Models.DataLakeLeaseDuration leaseDuration, Azure.Storage.Files.DataLake.Models.DataLakeLeaseState leaseState, Azure.Storage.Files.DataLake.Models.DataLakeLeaseStatus leaseStatus, long contentLength, string contentType, Azure.ETag eTag, byte[] contentHash, string contentEncoding, string contentDisposition, string contentLanguage, string cacheControl, string acceptRanges, bool isServerEncrypted, string encryptionKeySha256, string accessTier, string archiveStatus, System.DateTimeOffset accessTierChangeTime, bool isDirectory, string encryptionContext, string owner, string group, string permissions, System.Collections.Generic.IList accessControlList) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.DataLake.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStart, System.DateTimeOffset signedExpiry, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Files.DataLake.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStart = default(System.DateTimeOffset), System.DateTimeOffset signedExpiry = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } public partial class DataLakeOpenReadOptions { @@ -1280,6 +1294,7 @@ public enum RolePermissions public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -1350,6 +1365,8 @@ public DataLakeSasBuilder(Azure.Storage.Sas.DataLakeSasPermissions permissions, public string Permissions { get { throw null; } } public string PreauthorizedAgentObjectId { get { throw null; } set { } } public Azure.Storage.Sas.SasProtocol Protocol { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestHeaders { get { throw null; } set { } } + public System.Collections.Generic.Dictionary RequestQueryParameters { get { throw null; } set { } } public string Resource { get { throw null; } set { } } public System.DateTimeOffset StartsOn { get { throw null; } set { } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -1389,6 +1406,7 @@ public sealed partial class DataLakeSasQueryParameters : Azure.Storage.Sas.SasQu { internal DataLakeSasQueryParameters() { } public static new Azure.Storage.Sas.DataLakeSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/assets.json b/sdk/storage/Azure.Storage.Files.DataLake/assets.json index ed441b664ea1..371a87215c01 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/assets.json +++ b/sdk/storage/Azure.Storage.Files.DataLake/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Files.DataLake", - "Tag": "net/storage/Azure.Storage.Files.DataLake_8e49cc5c23" + "Tag": "net/storage/Azure.Storage.Files.DataLake_e87e5c6594" } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/samples/Azure.Storage.Files.DataLake.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Files.DataLake/samples/Azure.Storage.Files.DataLake.Samples.Tests.csproj index c230f2ed8fa2..eecbe0543fe8 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/samples/Azure.Storage.Files.DataLake.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Files.DataLake/samples/Azure.Storage.Files.DataLake.Samples.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Azure.Storage.Files.DataLake.csproj b/sdk/storage/Azure.Storage.Files.DataLake/src/Azure.Storage.Files.DataLake.csproj index 42f452963e06..b1ea7b1fe932 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/Azure.Storage.Files.DataLake.csproj +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Azure.Storage.Files.DataLake.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.Files.DataLake client library - 12.25.0-beta.2 + 12.26.0-beta.1 12.24.0 DataLakeSDK;$(DefineConstants) @@ -42,6 +42,7 @@ + @@ -81,6 +82,10 @@ + + + + diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs index 93ba1a58aeb2..fd5ffa254614 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs @@ -176,7 +176,12 @@ public enum ServiceVersion /// /// The 2026-02-06 service version. /// - V2026_02_06 = 29 + V2026_02_06 = 29, + + /// + /// The 2026-04-06 service version. + /// + V2026_04_06 = 30 #pragma warning restore CA1707 // Identifiers should not contain underscores } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs index d70442bfb67c..b5d4e0296e6a 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs @@ -3248,7 +3248,7 @@ public override Uri GenerateSasUri(DataLakeSasBuilder builder, out string string /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -3279,7 +3279,7 @@ public override Uri GenerateUserDelegationSasUri(DataLakeSasPermissions permissi /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -3314,7 +3314,7 @@ public override Uri GenerateUserDelegationSasUri(DataLakeSasPermissions permissi /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -3340,7 +3340,7 @@ public override Uri GenerateUserDelegationSasUri(DataLakeSasBuilder builder, Use /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs index b1eaa5b423fc..662ef4aea2fd 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs @@ -16,6 +16,7 @@ using Azure.Storage.Common; using Azure.Storage.Files.DataLake.Models; using Azure.Storage.Sas; +using Azure.Storage.Shared; using Metadata = System.Collections.Generic.IDictionary; namespace Azure.Storage.Files.DataLake @@ -2390,13 +2391,39 @@ internal virtual async Task AppendInternal( using (ClientConfiguration.Pipeline.BeginLoggingScope(nameof(DataLakeFileClient))) { // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + if (content != null && + validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64) + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content.WithNoDispose().WithProgress(progressHandler); + content = validationOptions.PrecalculatedChecksum.IsEmpty + ? new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64) + : new StructuredMessagePrecalculatedCrcWrapperStream( + content, + validationOptions.PrecalculatedChecksum.Span); + contentLength = content.Length - content.Position; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content?.WithNoDispose().WithProgress(progressHandler); + } - content = content?.WithNoDispose().WithProgress(progressHandler); ClientConfiguration.Pipeline.LogMethodEnter( nameof(DataLakeFileClient), message: @@ -2431,6 +2458,8 @@ internal virtual async Task AppendInternal( encryptionKey: ClientConfiguration.CustomerProvidedKey?.EncryptionKey, encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, leaseId: leaseId, leaseAction: leaseAction, leaseDuration: leaseDurationLong, @@ -2450,6 +2479,8 @@ internal virtual async Task AppendInternal( encryptionKey: ClientConfiguration.CustomerProvidedKey?.EncryptionKey, encryptionKeySha256: ClientConfiguration.CustomerProvidedKey?.EncryptionKeyHash, encryptionAlgorithm: ClientConfiguration.CustomerProvidedKey?.EncryptionAlgorithm == null ? null : EncryptionAlgorithmTypeInternal.AES256, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, leaseId: leaseId, leaseAction: leaseAction, leaseDuration: leaseDurationLong, diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs index b6b66320e097..42dd5c9fd0d3 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs @@ -3468,7 +3468,7 @@ public virtual Uri GenerateSasUri( /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -3500,7 +3500,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeFileSystemSasPermissions /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -3531,7 +3531,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeFileSystemSasPermissions /// /// /// Required. A returned from - /// . + /// . /// /// /// A on successfully deleting. @@ -3561,7 +3561,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeSasBuilder builder, Mode /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs index 62a7cbd231bc..355f1d8a2140 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs @@ -4265,7 +4265,7 @@ public virtual Uri GenerateSasUri(DataLakeSasBuilder builder, out string stringT /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4296,7 +4296,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeSasPermissions permissio /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -4330,7 +4330,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeSasPermissions permissio /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4357,7 +4357,7 @@ public virtual Uri GenerateUserDelegationSasUri(DataLakeSasBuilder builder, User /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs index 38e9a64cb608..aa2507f0517d 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs @@ -9,10 +9,13 @@ using Azure.Core; using Azure.Core.Pipeline; using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; using Azure.Storage.Common; using Azure.Storage.Files.DataLake.Models; using Azure.Storage.Sas; using Metadata = System.Collections.Generic.IDictionary; +using PublicAccessType = Azure.Storage.Files.DataLake.Models.PublicAccessType; +using UserDelegationKey = Azure.Storage.Files.DataLake.Models.UserDelegationKey; namespace Azure.Storage.Files.DataLake { @@ -451,7 +454,120 @@ public virtual DataLakeFileSystemClient GetFileSystemClient(string fileSystemNam #region Get User Delegation Key /// - /// The operation retrieves a + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the use delegation key. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-datalake")] + public virtual Response GetUserDelegationKey( + DataLakeGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(DataLakeServiceClient)}.{nameof(GetUserDelegationKey)}"); + + try + { + scope.Start(); + + BlobGetUserDelegationKeyOptions blobOptions = MapOptions(options); + + Response response = _blobServiceClient.GetUserDelegationKey( + blobOptions, + cancellationToken); + + return Response.FromValue( + new UserDelegationKey(response.Value), + response.GetRawResponse()); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + finally + { + scope.Dispose(); + } + } + + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the use delegation key. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-datalake")] + public virtual async Task> GetUserDelegationKeyAsync( + DataLakeGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(DataLakeServiceClient)}.{nameof(GetUserDelegationKey)}"); + + try + { + scope.Start(); + + BlobGetUserDelegationKeyOptions blobOptions = MapOptions(options); + + Response response = await _blobServiceClient.GetUserDelegationKeyAsync( + blobOptions, + cancellationToken) + .ConfigureAwait(false); + + return Response.FromValue( + new UserDelegationKey(response.Value), + response.GetRawResponse()); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + finally + { + scope.Dispose(); + } + } + + /// + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -482,10 +598,13 @@ public virtual DataLakeFileSystemClient GetFileSystemClient(string fileSystemNam /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-datalake")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual Response GetUserDelegationKey( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(DataLakeServiceClient)}.{nameof(GetUserDelegationKey)}"); @@ -514,7 +633,7 @@ public virtual Response GetUserDelegationKey( } /// - /// The operation retrieves a + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -545,10 +664,13 @@ public virtual Response GetUserDelegationKey( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-datalake")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual async Task> GetUserDelegationKeyAsync( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(DataLakeServiceClient)}.{nameof(GetUserDelegationKey)}"); @@ -577,6 +699,16 @@ public virtual async Task> GetUserDelegationKeyAsync } } + private static BlobGetUserDelegationKeyOptions MapOptions(DataLakeGetUserDelegationKeyOptions options) + { + if (options == null) + return null; + return new BlobGetUserDelegationKeyOptions(expiresOn: options.ExpiresOn) + { + StartsOn = options.StartsOn, + DelegatedUserTenantId = options.DelegatedUserTenantId + }; + } #endregion Get User Delegation Key #region Get File Systems diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeGetUserDelegationKeyOptions.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeGetUserDelegationKeyOptions.cs new file mode 100644 index 000000000000..d41c4c5dc591 --- /dev/null +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeGetUserDelegationKeyOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Storage.Files.DataLake.Models +{ + /// + /// Parameters for Get User Delegation Key. + /// + public class DataLakeGetUserDelegationKeyOptions + { + /// + /// Constructor for DataLakeGetUserDelegationKeyOptions. + /// + public DataLakeGetUserDelegationKeyOptions(DateTimeOffset expiresOn) + { + ExpiresOn = expiresOn; + } + + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + public DateTimeOffset ExpiresOn { get; set; } + + /// + /// Optional. Start time for the key's validity, with null indicating an + /// immediate start. The time should be specified in UTC. + /// + /// Note: If you set the start time to the current time, failures + /// might occur intermittently for the first few minutes. This is due to different + /// machines having slightly different current times (known as clock skew). + /// + public DateTimeOffset? StartsOn { get; set; } + + /// + /// Optional. The delegated user tenant id in Azure AD. + /// + public string DelegatedUserTenantId { get; set; } + } +} diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeModelFactory.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeModelFactory.cs index bf3d2158214f..dd6730a17237 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeModelFactory.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeModelFactory.cs @@ -911,8 +911,33 @@ public static PathProperties PathProperties( #region UserDelegationKey /// - /// Creates a new instance for mocking. + /// Creates a new instance for mocking. /// + public static UserDelegationKey UserDelegationKey( + string signedObjectId = default, + string signedTenantId = default, + DateTimeOffset signedStart = default, + DateTimeOffset signedExpiry = default, + string signedService = default, + string signedVersion = default, + string signedDelegatedUserTenantId = default, + string value = default) + => new UserDelegationKey() + { + SignedObjectId = signedObjectId, + SignedTenantId = signedTenantId, + SignedStartsOn = signedStart, + SignedExpiresOn = signedExpiry, + SignedService = signedService, + SignedVersion = signedVersion, + Value = value, + SignedDelegatedUserTenantId = signedDelegatedUserTenantId + }; + + /// + /// Creates a new instance for mocking. + /// + [EditorBrowsable(EditorBrowsableState.Never)] public static UserDelegationKey UserDelegationKey( string signedObjectId, string signedTenantId, diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/UserDelegationKey.cs index a3f761e2362e..543d1eb732c6 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/UserDelegationKey.cs @@ -42,6 +42,11 @@ public class UserDelegationKey /// public string SignedVersion { get; internal set; } + /// + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// + public string SignedDelegatedUserTenantId { get; internal set; } + /// /// The key as a base64 string /// @@ -62,6 +67,7 @@ internal UserDelegationKey(Blobs.Models.UserDelegationKey blobUserDelegationKey) SignedService = blobUserDelegationKey.SignedService; SignedVersion = blobUserDelegationKey.SignedVersion; Value = blobUserDelegationKey.Value; + SignedDelegatedUserTenantId = blobUserDelegationKey.SignedDelegatedUserTenantId; } } } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasBuilder.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasBuilder.cs index 205c06c472d8..52a406962e9c 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasBuilder.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Security.Cryptography; using System.Text; +using System.Threading; using Azure.Core; using Azure.Storage.Files.DataLake; using Azure.Storage.Files.DataLake.Models; @@ -205,6 +206,18 @@ public class DataLakeSasBuilder /// public string DelegatedUserObjectId { get; set; } + /// + /// Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + /// include these headers and values in the request. + /// + public Dictionary RequestHeaders { get; set; } + + /// + /// Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS must + /// include these query parameters and values in the request. + /// + public Dictionary RequestQueryParameters { get; set; } + /// /// Optional. Required when is set to d to indicate the /// depth of the directory specified in the canonicalizedresource field of the @@ -446,7 +459,7 @@ private string ToStringToSign(StorageSharedKeyCredential sharedKeyCredential) /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -463,7 +476,7 @@ public DataLakeSasQueryParameters ToSasQueryParameters(UserDelegationKey userDel /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -511,7 +524,10 @@ public DataLakeSasQueryParameters ToSasQueryParameters(UserDelegationKey userDel correlationId: CorrelationId, directoryDepth: _directoryDepth, encryptionScope: EncryptionScope, - delegatedUserObjectId: DelegatedUserObjectId); + delegatedUserObjectId: DelegatedUserObjectId, + keyDelegatedUserTenantId: userDelegationKey.SignedDelegatedUserTenantId, + requestHeaders: SasExtensions.ConvertRequestDictToKeyList(RequestHeaders), + requestQueryParameters: SasExtensions.ConvertRequestDictToKeyList(RequestQueryParameters)); return p; } @@ -521,6 +537,8 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun string expiryTime = SasExtensions.FormatTimesForSasSigning(ExpiresOn); string signedStart = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedStartsOn); string signedExpiry = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedExpiresOn); + string canonicalizedSignedRequestHeaders = SasExtensions.FormatRequestHeadersForSasSigning(RequestHeaders); + string canonicalizedSignedRequestQueryParameters = SasExtensions.FormatRequestQueryParametersForSasSigning(RequestQueryParameters); // See http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx return string.Join("\n", @@ -537,7 +555,7 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun PreauthorizedAgentObjectId, AgentObjectId, CorrelationId, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + userDelegationKey.SignedDelegatedUserTenantId, DelegatedUserObjectId, IPRange.ToString(), SasExtensions.ToProtocolString(Protocol), @@ -545,6 +563,8 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun Resource, null, // snapshot EncryptionScope, + canonicalizedSignedRequestHeaders, + canonicalizedSignedRequestQueryParameters, CacheControl, ContentDisposition, ContentEncoding, diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasQueryParameters.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasQueryParameters.cs index a576df8bc2ba..c79a190472c8 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasQueryParameters.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Sas/DataLakeSasQueryParameters.cs @@ -53,6 +53,11 @@ public sealed class DataLakeSasQueryParameters : SasQueryParameters /// public string KeyVersion => KeyProperties?.Version; + /// + /// Gets the delegated user tenant id. + /// + public string KeyDelegatedUserTenantId => KeyProperties?.DelegatedUserTenantId; + /// /// Gets empty shared access signature query parameters. /// @@ -94,7 +99,10 @@ internal DataLakeSasQueryParameters( string correlationId = default, int? directoryDepth = default, string encryptionScope = default, - string delegatedUserObjectId = default) + string delegatedUserObjectId = default, + string keyDelegatedUserTenantId = default, + List requestHeaders = default, + List requestQueryParameters = default) : base( version: version, services: services, @@ -117,7 +125,9 @@ internal DataLakeSasQueryParameters( correlationId: correlationId, directoryDepth: directoryDepth, encryptionScope: encryptionScope, - delegatedUserObjectId: delegatedUserObjectId) + delegatedUserObjectId: delegatedUserObjectId, + requestHeaders: requestHeaders, + requestQueryParameter: requestQueryParameters) { KeyProperties = new UserDelegationKeyProperties { @@ -126,7 +136,8 @@ internal DataLakeSasQueryParameters( StartsOn = keyStart, ExpiresOn = keyExpiry, Service = keyService, - Version = keyVersion + Version = keyVersion, + DelegatedUserTenantId = keyDelegatedUserTenantId }; } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/autorest.md b/sdk/storage/Azure.Storage.Files.DataLake/src/autorest.md index a506bb082486..4f02fbc7aaf0 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/autorest.md +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/autorest.md @@ -23,7 +23,7 @@ directive: if (property.includes('/{filesystem}/{path}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/FileSystem") && false == param['$ref'].endsWith("#/parameters/Path"))}); - } + } else if (property.includes('/{filesystem}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/FileSystem"))}); @@ -127,7 +127,7 @@ directive: } $[newName] = $[oldName]; delete $[oldName]; - } + } else if (property.includes('/{filesystem}')) { var oldName = property; diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/Azure.Storage.Files.DataLake.Tests.csproj b/sdk/storage/Azure.Storage.Files.DataLake/tests/Azure.Storage.Files.DataLake.Tests.csproj index 341d5703e03f..560c51b70376 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/Azure.Storage.Files.DataLake.Tests.csproj +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/Azure.Storage.Files.DataLake.Tests.csproj @@ -6,6 +6,9 @@ Microsoft Azure.Storage.Files.DataLake client library tests false + + DataLakeSDK + diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeClientTestFixtureAttribute.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeClientTestFixtureAttribute.cs index 0528143231e2..924c589a9e6e 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeClientTestFixtureAttribute.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeClientTestFixtureAttribute.cs @@ -38,6 +38,7 @@ public DataLakeClientTestFixtureAttribute() DataLakeClientOptions.ServiceVersion.V2025_07_05, DataLakeClientOptions.ServiceVersion.V2025_11_05, DataLakeClientOptions.ServiceVersion.V2026_02_06, + DataLakeClientOptions.ServiceVersion.V2026_04_06, StorageVersionExtensions.LatestVersion, StorageVersionExtensions.MaxVersion) { diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeFileClientTransferValidationTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeFileClientTransferValidationTests.cs index 4bdefdbf756c..5067f98517bd 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeFileClientTransferValidationTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeFileClientTransferValidationTests.cs @@ -34,7 +34,10 @@ protected override async Task> Get StorageChecksumAlgorithm uploadAlgorithm = StorageChecksumAlgorithm.None, StorageChecksumAlgorithm downloadAlgorithm = StorageChecksumAlgorithm.None) { - var disposingFileSystem = await ClientBuilder.GetNewFileSystem(service: service, fileSystemName: containerName); + var disposingFileSystem = await ClientBuilder.GetNewFileSystem( + service: service, + fileSystemName: containerName, + publicAccessType: PublicAccessType.None); disposingFileSystem.FileSystem.ClientConfiguration.TransferValidation.Upload.ChecksumAlgorithm = uploadAlgorithm; disposingFileSystem.FileSystem.ClientConfiguration.TransferValidation.Download.ChecksumAlgorithm = downloadAlgorithm; diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasBuilderTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasBuilderTests.cs index ad0fa090f6cb..6d20024dbead 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasBuilderTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasBuilderTests.cs @@ -208,9 +208,9 @@ public async Task DataLakeSasBuilderRawPermissions_2020_02_10(string permissions DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -251,9 +251,9 @@ public async Task DataLakeSasBuilder_DirectoryRawPermissions_Exists() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -292,9 +292,9 @@ public async Task DataLakeSasBuilder_DirectoryRawPermissions_List() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -340,9 +340,9 @@ public async Task DataLakeSasBuilder_PreauthorizedAgentObjectId() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -382,9 +382,9 @@ public async Task DataLakeSasBuilder_AgentObjectId() // Arrange DataLakeDirectoryClient directory = test.FileSystem.GetRootDirectoryClient(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); // Give UnknownGuid rights IList accessControlList = new List() @@ -433,9 +433,9 @@ public async Task DataLakeSasBuilder_AgentObjectId_Error() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -475,9 +475,9 @@ public async Task DataLakeSasBuilder_BothObjectId_Error() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -510,9 +510,9 @@ public async Task DataLakeSasBuilder_CorrelationId() DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); DataLakeFileClient file = await directory.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -597,9 +597,9 @@ public async Task DataLakeSasBuilder_DirectoryDepth_Exists() DataLakeDirectoryClient subdirectory3 = await subdirectory2.CreateSubDirectoryAsync(GetNewDirectoryName()); DataLakeFileClient file = await subdirectory3.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -641,9 +641,9 @@ public async Task DataLakeSasBuilder_DirectoryDepth(string directoryName) DataLakeDirectoryClient directory = test.FileSystem.GetDirectoryClient(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder { @@ -686,9 +686,9 @@ public async Task DataLakeSasBuilder_DirectoryDepth_CustomSas(string directoryNa DataLakeDirectoryClient directory = test.FileSystem.GetDirectoryClient(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); // Make a SAS with the DirectoryDepth/sdd DataLakeSasBuilder dataLakeSasBuilder = new(DataLakeSasPermissions.All, Recording.UtcNow.AddHours(1)) @@ -732,9 +732,9 @@ public async Task SasCredentialRequiresUriWithoutSasError_RedactedSasUri() DataLakeDirectoryClient directory = test.FileSystem.GetDirectoryClient(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); // Make a SAS with the DirectoryDepth/sdd DataLakeSasBuilder dataLakeSasBuilder = new(DataLakeSasPermissions.All, Recording.UtcNow.AddHours(1)) @@ -776,6 +776,8 @@ public void ToSasQueryParameters_FileIdentityTest() DataLakeSasBuilder dataLakeSasBuilder = BuildDataLakeSasBuilder( includePath: true, includeDelegatedObjectId: true, + includeRequestHeaders: true, + includeRequestQueryParameters: true, fileSystemName: containerName, path: fileName, constants); @@ -801,9 +803,12 @@ public void ToSasQueryParameters_FileIdentityTest() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.Blob, sasQueryParameters.Resource); Assert.AreEqual(_sasPermissions.ToPermissionsString(), sasQueryParameters.Permissions); Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestHeaders), sasQueryParameters.RequestHeaders); + Assert.AreEqual(SasExtensions.ConvertRequestDictToKeyList(constants.Sas.RequestQueryParameters), sasQueryParameters.RequestQueryParameters); Assert.AreEqual(signature, sasQueryParameters.Signature); AssertResponseHeaders(constants, sasQueryParameters); } @@ -811,6 +816,8 @@ public void ToSasQueryParameters_FileIdentityTest() private DataLakeSasBuilder BuildDataLakeSasBuilder( bool includePath, bool includeDelegatedObjectId, + bool includeRequestHeaders, + bool includeRequestQueryParameters, string fileSystemName, string path, TestConstants constants) @@ -837,6 +844,14 @@ private DataLakeSasBuilder BuildDataLakeSasBuilder( { builder.DelegatedUserObjectId = constants.Sas.DelegatedObjectId; } + if (includeRequestHeaders) + { + builder.RequestHeaders = constants.Sas.RequestHeaders; + } + if (includeRequestQueryParameters) + { + builder.RequestQueryParameters = constants.Sas.RequestQueryParameters; + } builder.SetPermissions(_sasPermissions); return builder; @@ -872,7 +887,7 @@ private string BuildIdentitySignature( null, null, null, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + constants.Sas.KeyDelegatedTenantId, constants.Sas.DelegatedObjectId, constants.Sas.IPRange.ToString(), SasExtensions.ToProtocolString(constants.Sas.Protocol), @@ -880,6 +895,8 @@ private string BuildIdentitySignature( resource, null, constants.Sas.EncryptionScope, + SasExtensions.FormatRequestHeadersForSasSigning(constants.Sas.RequestHeaders), + SasExtensions.FormatRequestQueryParametersForSasSigning(constants.Sas.RequestQueryParameters), constants.Sas.CacheControl, constants.Sas.ContentDisposition, constants.Sas.ContentEncoding, @@ -904,6 +921,7 @@ private static UserDelegationKey GetUserDelegationKey(TestConstants constants) SignedExpiresOn = constants.Sas.KeyExpiry, SignedService = constants.Sas.KeyService, SignedVersion = constants.Sas.KeyVersion, + SignedDelegatedUserTenantId = constants.Sas.KeyDelegatedTenantId, Value = constants.Sas.KeyValue }; } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasTests.cs index 861814b951a1..bed4cf3ad671 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeSasTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using Azure.Storage.Files.DataLake.Specialized; using Azure.Storage.Sas; using Azure.Storage.Test; +using Azure.Storage.Test.Shared; using NUnit.Framework; namespace Azure.Storage.Files.DataLake.Tests @@ -380,6 +382,177 @@ public async Task AccountSasPermissions_FileSystemToFile() await InvokeAccountSasFileSystemToFileTest(permissions: permissions); } + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_04_06)] + public async Task FileSystemIdentitySAS_DelegatedTenantId() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder dataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + DataLakeFileClient identitySasFile = InstrumentClient(new DataLakeFileClient(dataLakeUriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act + Response response = await identitySasFile.GetPropertiesAsync(); + AssertSasUserDelegationKey(identitySasFile.Uri, userDelegationKey.Value); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_04_06)] + public async Task FileSystemIdentitySAS_DelegatedTenantId_Fail() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + // We are deliberately not passing in DelegatedUserObjectId to cause an auth failure + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder dataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + DataLakeFileClient identitySasFile = InstrumentClient(new DataLakeFileClient(dataLakeUriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act & Assert + await TestHelper.AssertExpectedExceptionAsync( + identitySasFile.GetPropertiesAsync(), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_04_06)] + public async Task FileSystemIdentitySAS_DelegatedTenantId_Roundtrip() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder originalDataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + DataLakeUriBuilder roundtripDataLakeUriBuilder = new DataLakeUriBuilder(originalDataLakeUriBuilder.ToUri()); + + Assert.AreEqual(originalDataLakeUriBuilder.ToUri(), roundtripDataLakeUriBuilder.ToUri()); + Assert.AreEqual(originalDataLakeUriBuilder.Sas.ToString(), roundtripDataLakeUriBuilder.Sas.ToString()); + } + [RecordedTest] [LiveOnly] // Cannot record Entra ID token [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_02_06)] @@ -394,9 +567,9 @@ public async Task FileSystemIdentitySAS_DelegatedObjectId() DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); await file.CreateAsync(); + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -444,9 +617,9 @@ public async Task FileSystemIdentitySAS_DelegatedObjectId_Fail() DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); await file.CreateAsync(); + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -478,6 +651,193 @@ await TestHelper.AssertExpectedExceptionAsync( identitySasFile.GetPropertiesAsync(), e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_02_06)] + public async Task FileSystemIdentitySAS_RequestHeadersAndQueryParameters() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder dataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + CustomRequestHeadersAndQueryParametersPolicy customRequestPolicy = new CustomRequestHeadersAndQueryParametersPolicy(); + // Send the request headers based on 'requestHeaders' Dictionary + foreach (var header in requestHeaders) + { + if (header.Key != null) + { + customRequestPolicy.AddRequestHeader(header.Key, header.Value); + } + } + + // Send the query parameters based on 'requestQueryParameters' Dictionary + foreach (var param in requestQueryParameters) + { + if (param.Key != null) + { + customRequestPolicy.AddQueryParameter(param.Key, param.Value); + } + } + + DataLakeClientOptions datalakeClientOptions = GetOptions(); + datalakeClientOptions.AddPolicy(customRequestPolicy, HttpPipelinePosition.PerCall); + + DataLakeFileClient identitySasFile = InstrumentClient(new DataLakeFileClient(dataLakeUriBuilder.ToUri(), TestEnvironment.Credential, datalakeClientOptions)); + + // Act + Response response = await identitySasFile.GetPropertiesAsync(); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_02_06)] + public async Task FileSystemIdentitySAS_RequestHeadersAndQueryParameters_Fail() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder dataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + // Deliberately do not send the request header and query parameter to cause an auth failure + + DataLakeClientOptions datalakeClientOptions = GetOptions(); + DataLakeFileClient identitySasFile = InstrumentClient(new DataLakeFileClient(dataLakeUriBuilder.ToUri(), TestEnvironment.Credential, datalakeClientOptions)); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + identitySasFile.GetPropertiesAsync(), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = DataLakeClientOptions.ServiceVersion.V2026_02_06)] + public async Task FileSystemIdentitySAS_RequestHeadersAndQueryParameters_Roundtrip() + { + DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + string fileSystemName = GetNewFileSystemName(); + string fileName = GetNewFileName(); + await using DisposingFileSystem test = await GetNewFileSystem(fileSystemName: fileSystemName, service: oauthService); + + // Arrange + DataLakeFileClient file = InstrumentClient(test.FileSystem.GetFileClient(fileName)); + await file.CreateAsync(); + + DataLakeGetUserDelegationKeyOptions options = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( + options: options); + + Dictionary requestHeaders = new Dictionary() + { + { "foo$", "bar!" }, + { "company", "msft" }, + { "city", "redmond,atlanta,reston" } + }; + + Dictionary requestQueryParameters = new Dictionary() + { + { "hello$", "world!" }, + { "abra", "cadabra" }, + { "firstName", "john,Tim"} + }; + + DataLakeSasBuilder dataLakeSasBuilder = new DataLakeSasBuilder(DataLakeFileSystemSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + FileSystemName = test.Container.Name, + RequestHeaders = requestHeaders, + RequestQueryParameters = requestQueryParameters + }; + + DataLakeSasQueryParameters dataLakeSasQueryParameters = dataLakeSasBuilder.ToSasQueryParameters(userDelegationKey.Value, oauthService.AccountName); + + DataLakeUriBuilder originalDataLakeUriBuilder = new DataLakeUriBuilder(file.Uri) + { + Sas = dataLakeSasQueryParameters + }; + + // Act + DataLakeUriBuilder roundtripDataLakeUriBuilder = new DataLakeUriBuilder(originalDataLakeUriBuilder.ToUri()); + + Assert.AreEqual(originalDataLakeUriBuilder.ToUri(), roundtripDataLakeUriBuilder.ToUri()); + Assert.AreEqual(originalDataLakeUriBuilder.Sas.ToString(), roundtripDataLakeUriBuilder.Sas.ToString()); + } #endregion DataLakeFileSystemClient #region DataLakeDirectoryClient diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs index 5538736af0f8..f9b6fa0d27c9 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs @@ -454,6 +454,7 @@ public void AssertSasUserDelegationKey(Uri uri, UserDelegationKey key) Assert.AreEqual(key.SignedService, sas.KeyService); Assert.AreEqual(key.SignedStartsOn, sas.KeyStartsOn); Assert.AreEqual(key.SignedTenantId, sas.KeyTenantId); + Assert.AreEqual(key.SignedDelegatedUserTenantId, sas.KeyDelegatedUserTenantId); //Assert.AreEqual(key.SignedVersion, sas.Version); } }; diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestEnvironment.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestEnvironment.cs index b23d821344bf..4d2662b92fa9 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestEnvironment.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestEnvironment.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using Azure.Core.TestFramework; using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; +using Azure.Storage.Files.DataLake.Models; using Azure.Storage.Sas; using Azure.Storage.Test; using Azure.Storage.Test.Shared; @@ -46,7 +48,9 @@ private async Task DoesOAuthWorkAsync() await blobClient.CreateIfNotExistsAsync(); await blobClient.GetPropertiesAsync(); - var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync(startsOn: null, expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + BlobGetUserDelegationKeyOptions getUserDelegationKeyOptions = new BlobGetUserDelegationKeyOptions(expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync( + options: getUserDelegationKeyOptions); var sasBuilder = new BlobSasBuilder(BlobSasPermissions.All, DateTimeOffset.UtcNow.AddHours(1)) { BlobContainerName = containerName, @@ -83,7 +87,9 @@ private async Task DoesOAuthWorkAsync() await fileClient.AppendAsync(new MemoryStream(new byte[] { 1 }), 0); await fileClient.GetAccessControlAsync(); - var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync(startsOn: null, expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: DateTimeOffset.UtcNow.AddHours(1)); + var userDelegationKey = await serviceClient.GetUserDelegationKeyAsync( + options: getUserDelegationKeyOptions); var sasBuilder = new DataLakeSasBuilder(DataLakeSasPermissions.All, DateTimeOffset.UtcNow.AddHours(1)) { FileSystemName = fileSystemName, diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs index 478213a8b19f..c65c7e20df00 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs @@ -912,9 +912,9 @@ public async Task CreateAsync_EncryptionScopeIdentitySas() { // Arrange await using DisposingFileSystem test = await GetNewFileSystem(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); string directoryName = GetNewDirectoryName(); @@ -1762,9 +1762,9 @@ public async Task GetAccessControlAsync_FileSystemIdentitySAS() // Arrange DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeDirectoryClient identitySasDirectory = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_FileSystem( @@ -1823,9 +1823,9 @@ public async Task GetAccessControlAsync_PathIdentitySAS() // Arrange DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeDirectoryClient identitySasDirectory = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_Path( @@ -2259,9 +2259,9 @@ public async Task SetAccessControlRecursiveAsync_WithProgressMonitoring_WithFail // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -2328,9 +2328,9 @@ public async Task SetAccessControlRecursiveAsync_ContinueOnFailure() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -2409,9 +2409,9 @@ public async Task SetAccessControlRecursiveAsync_ContinueOnFailure_Batches_StopA // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -2515,9 +2515,9 @@ public async Task SetAccessControlRecursiveAsync_BatchFailures_BatchSize() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -2606,9 +2606,9 @@ public async Task SetAccessControlRecursiveAsync_ContinueOnFailure_RetrieveBatch // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -2963,9 +2963,9 @@ public async Task UpdateAccessControlRecursiveAsync_WithProgressMonitoring_WithF // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3041,9 +3041,9 @@ public async Task UpdateAccessControlRecursiveAsync_ContinueOnFailure() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3117,9 +3117,9 @@ public async Task UpdateAccessControlRecursiveAsync_ContinueOnFailure_Batches_St // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3223,9 +3223,9 @@ public async Task UpdateAccessControlRecursiveAsync_BatchFailures_BatchSize() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3314,9 +3314,9 @@ public async Task UpdateAccessControlRecursiveAsync_ContinueOnFailure_RetrieveBa // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3673,9 +3673,9 @@ public async Task RemoveAccessControlRecursiveAsync_WithProgressMonitoring_WithF // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3740,9 +3740,9 @@ public async Task RemoveAccessControlRecursiveAsync_ContinueOnFailure() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3821,9 +3821,9 @@ public async Task RemoveAccessControlRecursiveAsync_ContinueOnFailure_Batches_St // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -3926,9 +3926,9 @@ public async Task RemoveAccessControlRecursiveAsync_BatchFailures_BatchSize() // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -4017,9 +4017,9 @@ public async Task RemoveAccessControlRecursiveAsync_ContinueOnFailure_RetrieveBa // Create a User Delegation SAS that delegates an owner when creating files DataLakeServiceClient oauthService = GetServiceClient_OAuth(); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeUriBuilder dataLakeUriBuilderOwner1 = new DataLakeUriBuilder(directory.Uri) { Sas = GetNewDataLakeSasCredentialsOwner(fileSystemName, subowner, userDelegationKey, test.FileSystem.AccountName) @@ -4351,9 +4351,9 @@ public async Task GetPropertiesAsync_FileSystemIdentitySAS() // Arrange DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeDirectoryClient identitySasDirectory = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_FileSystem( @@ -4454,9 +4454,9 @@ public async Task GetPropertiesAsync_PathIdentitySAS() // Arrange DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(directoryName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeDirectoryClient identitySasDirectory = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_Path( @@ -6267,9 +6267,9 @@ public async Task GetSetTags_DirectoryIdentitySas() await using DisposingFileSystem test = await GetNewFileSystem(oauthService); DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(GetNewDirectoryName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri sasUri = directory.GenerateUserDelegationSasUri(DataLakeSasPermissions.All, Recording.UtcNow.AddHours(1), userDelegationKey); directory = InstrumentClient(new DataLakeDirectoryClient(sasUri, GetOptions())); @@ -6294,9 +6294,9 @@ public async Task GetSetTags_FileSystemIdentitySas() await using DisposingFileSystem test = await GetNewFileSystem(oauthService); DataLakeDirectoryClient directory = await test.FileSystem.CreateDirectoryAsync(GetNewDirectoryName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri fileSystemSasUri = test.FileSystem.GenerateUserDelegationSasUri(DataLakeFileSystemSasPermissions.All, Recording.UtcNow.AddHours(1), userDelegationKey); DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(fileSystemSasUri); @@ -6919,9 +6919,9 @@ public async Task GenerateUserDelegationSas_RequiredParameters() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6971,9 +6971,9 @@ public async Task GenerateUserDelegationSas_Builder() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7013,9 +7013,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7082,9 +7082,9 @@ public async Task GenerateUserDelegationSas_BuilderNullFileSystemName() IsDirectory = true, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7132,9 +7132,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongFileSystemName() IsDirectory = true }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7168,9 +7168,9 @@ public async Task GenerateUserDelegationSas_BuilderNullPath() IsDirectory = true, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7218,9 +7218,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongPathName() IsDirectory = true, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7254,9 +7254,9 @@ public async Task GenerateUserDelegationSas_BuilderIsDirectoryNull() IsDirectory = null, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7303,9 +7303,9 @@ public async Task GenerateUserDelegationSas_BuilderIsDirectoryError() IsDirectory = false, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs index 5470294be3db..7af09b0fde7a 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs @@ -1667,9 +1667,9 @@ public async Task GetAccessControlAsync_FileSystemIdentitySAS() // Arrange DataLakeFileClient file = await directory.CreateFileAsync(fileName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeFileClient identitySasFile = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_FileSystem( @@ -1735,9 +1735,9 @@ public async Task GetAccessControlAsync_PathIdentitySAS() // Arrange DataLakeFileClient file = await directory.CreateFileAsync(fileName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeFileClient identitySasFile = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_Path( @@ -2220,9 +2220,9 @@ public async Task GetPropertiesAsync_FileSystemIdentitySAS() // Arrange DataLakeFileClient file = await directory.CreateFileAsync(fileName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeFileClient identitySasFile = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_FileSystem( @@ -2282,9 +2282,9 @@ public async Task GetPropertiesAsync_PathIdentitySAS() // Arrange DataLakeFileClient file = await directory.CreateFileAsync(fileName); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); DataLakeFileClient identitySasFile = InstrumentClient( GetServiceClient_DataLakeServiceIdentitySas_Path( @@ -5798,9 +5798,9 @@ public async Task GetSetTags_FileIdentitySas() await using DisposingFileSystem test = await GetNewFileSystem(oauthService); DataLakeFileClient file = await test.FileSystem.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri sasUri = file.GenerateUserDelegationSasUri(DataLakeSasPermissions.All, Recording.UtcNow.AddHours(1), userDelegationKey); file = InstrumentClient(new DataLakeFileClient(sasUri, GetOptions())); @@ -5825,9 +5825,9 @@ public async Task GetSetTags_FileSystemIdentitySas() await using DisposingFileSystem test = await GetNewFileSystem(oauthService); DataLakeFileClient file = await test.FileSystem.CreateFileAsync(GetNewFileName()); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKey = await oauthService.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri fileSystemSasUri = test.FileSystem.GenerateUserDelegationSasUri(DataLakeFileSystemSasPermissions.All, Recording.UtcNow.AddHours(1), userDelegationKey); DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(fileSystemSasUri); @@ -6762,9 +6762,9 @@ public async Task GenerateUserDelegationSas_RequiredParameters() dataLakeUriBuilder.ToUri(), GetOptions())); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6809,9 +6809,9 @@ public async Task GenerateUserDelegationSas_Builder() Path = path }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6848,9 +6848,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() dataLakeUriBuilder.ToUri(), GetOptions())); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6913,9 +6913,9 @@ public async Task GenerateUserDelegationSas_BuilderNullFileSystemName() Path = path }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6961,9 +6961,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongFileSystemName() Path = path, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -6996,9 +6996,9 @@ public async Task GenerateUserDelegationSas_BuilderNullFileName() Path = null }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7044,9 +7044,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongFileName() Path = GetNewFileName(), // different path }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7080,9 +7080,9 @@ public async Task GenerateUserDelegationSas_BuilderNullIsDirectory() IsDirectory = null }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -7132,9 +7132,9 @@ public async Task GenerateUserDelegationSas_BuilderIsDirectoryError() ExpiresOn = Recording.UtcNow.AddHours(+1) }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs index eb862023ea26..938b25092434 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs @@ -3427,9 +3427,9 @@ public async Task GenerateUserDelegationSas_RequiredParameters() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -3473,9 +3473,9 @@ public async Task GenerateUserDelegationSas_Builder() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -3513,9 +3513,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -3578,9 +3578,9 @@ public async Task GenerateUserDelegationSas_BuilderNullName() FileSystemName = null }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -3624,9 +3624,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongName() FileSystemName = GetNewFileSystemName(), // different filesytem name }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -3660,9 +3660,9 @@ public async Task GenerateUserDelegationSas_BuilderIncorrectlySetPath() Path = GetNewFileName() }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/PathClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/PathClientTests.cs index 4c529bb5dcfd..3d2fdaaa915f 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/PathClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/PathClientTests.cs @@ -447,9 +447,9 @@ public async Task GenerateUserDelegationSas_RequiredParameters() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -491,9 +491,9 @@ public async Task GenerateUserDelegationSas_Builder() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -524,9 +524,9 @@ public async Task GenerateUserDelegationSas_BuilderNull() GetOptions())); string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -586,9 +586,9 @@ public async Task GenerateUserDelegationSas_BuilderNullFileSystemName() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -628,9 +628,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongFileSystemName() Path = path, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -661,9 +661,9 @@ public async Task GenerateUserDelegationSas_BuilderNullPath() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -703,9 +703,9 @@ public async Task GenerateUserDelegationSas_BuilderWrongPath() Path = GetNewFileName(), // different path }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -737,9 +737,9 @@ public async Task GenerateUserDelegationSas_BuilderNullIsDirectory() }; string stringToSign = null; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act @@ -781,9 +781,9 @@ public async Task GenerateUserDelegationSas_BuilderIsDirectoryError() IsDirectory = true, }; + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await GetServiceClient_OAuth().GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; // Act diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs index 0af575bc3c97..8d80255c93be 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs @@ -306,7 +306,9 @@ public async Task GetUserDelegationKey() DataLakeServiceClient service = GetServiceClient_OAuth(); // Act - Response response = await service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)); + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response response = await service.GetUserDelegationKeyAsync( + options: getUserDelegationKeyOptions); // Assert Assert.IsNotNull(response.Value); @@ -319,8 +321,9 @@ public async Task GetUserDelegationKey_Error() DataLakeServiceClient service = DataLakeClientBuilder.GetServiceClient_Hns(); // Act + DataLakeGetUserDelegationKeyOptions getUserDelegationKeyOptions = new DataLakeGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)), + service.GetUserDelegationKeyAsync(options: getUserDelegationKeyOptions), e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } diff --git a/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md b/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md index ea1b590136ca..99f332f7cb81 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md @@ -1,14 +1,15 @@ # Release History -## 12.25.0-beta.2 (Unreleased) +## 12.26.0-beta.1 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed +- Added support for service version 2026-04-06. +- Added support for Content Validation via Structured Message +- Added cross-tenant support for Principal-Bound User Delegation SAS. +- Added support for Files Provisioned V2 Guardrails ### Other Changes +- Changed the default concurrency upload count from 5 to Math.Clamp(Environment.ProcessorCount * 2, 8, 32). This controls the maximum number of concurrent tasks that will be used during large uploads, and this change should result in higher throughput for these operations by default in most environments. This can be reverted by enabling "Azure.Storage.UseLegacyDefaultConcurrency" in the AppContext switch or "AZURE_STORAGE_USE_LEGACY_DEFAULT_CONCURRENCY" in the environment variable. ## 12.25.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net8.0.cs b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net8.0.cs index 5ffcba5b23c7..a4a26386b1f1 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net8.0.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net8.0.cs @@ -159,6 +159,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class ShareDirectoryClient @@ -452,8 +453,12 @@ public ShareServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyC public virtual Azure.Storage.Files.Shares.ShareClient GetShareClient(string shareName) { throw null; } public virtual Azure.Pageable GetShares(Azure.Storage.Files.Shares.Models.ShareTraits traits = Azure.Storage.Files.Shares.Models.ShareTraits.None, Azure.Storage.Files.Shares.Models.ShareStates states = Azure.Storage.Files.Shares.Models.ShareStates.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.AsyncPageable GetSharesAsync(Azure.Storage.Files.Shares.Models.ShareTraits traits = Azure.Storage.Files.Shares.Models.ShareTraits.None, Azure.Storage.Files.Shares.Models.ShareStates states = Azure.Storage.Files.Shares.Models.ShareStates.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Files.Shares.Models.ShareGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Files.Shares.Models.ShareGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Files.Shares.Models.ShareServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Files.Shares.Models.ShareServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response UndeleteShare(string deletedShareName, string deletedShareVersion, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -809,7 +814,10 @@ public ShareDirectorySetHttpHeadersOptions() { } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FeatureVersionMismatch { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileLockConflict { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedBandwidthDowngradeNotAllowed { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedBandwidthInvalid { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedIopsDowngradeNotAllowed { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedIopsInvalid { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedStorageInvalid { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InsufficientAccountPermissions { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InternalError { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InvalidAuthenticationInfo { get { throw null; } } @@ -854,6 +862,10 @@ public ShareDirectorySetHttpHeadersOptions() { } public static Azure.Storage.Files.Shares.Models.ShareErrorCode ShareSnapshotNotFound { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode ShareSnapshotOperationNotSupported { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode SharingViolation { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesCountExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedBandwidthExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedCapacityExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedIopsExceedsAccountLimit { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedHeader { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedHttpVerb { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedQueryParameter { get { throw null; } } @@ -946,6 +958,7 @@ public partial class ShareFileDownloadInfo : System.IDisposable { internal ShareFileDownloadInfo() { } public System.IO.Stream Content { get { throw null; } } + public byte[] ContentCrc { get { throw null; } } public byte[] ContentHash { get { throw null; } } public long ContentLength { get { throw null; } } public string ContentType { get { throw null; } } @@ -1201,6 +1214,13 @@ public ShareFileUploadRangeOptions() { } public byte[] TransactionalContentHash { get { throw null; } set { } } public Azure.Storage.UploadTransferValidationOptions TransferValidation { get { throw null; } set { } } } + public partial class ShareGetUserDelegationKeyOptions + { + public ShareGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class ShareInfo { internal ShareInfo() { } @@ -1282,7 +1302,9 @@ public static partial class ShareModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.Shares.Models.StorageClosedHandlesSegment StorageClosedHandlesSegment(string marker, int numberOfHandlesClosed) { throw null; } public static Azure.Storage.Files.Shares.Models.StorageClosedHandlesSegment StorageClosedHandlesSegment(string marker, int numberOfHandlesClosed, int numberOfHandlesFailedToClose) { throw null; } - public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } public partial class ShareNfsSettings { @@ -1464,6 +1486,7 @@ internal StorageClosedHandlesSegment() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -1588,6 +1611,7 @@ public enum ShareSasPermissions public sealed partial class ShareSasQueryParameters : Azure.Storage.Sas.SasQueryParameters { internal ShareSasQueryParameters() { } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.netstandard2.0.cs b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.netstandard2.0.cs index d0ac6d3de486..c94f84f4b52d 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.netstandard2.0.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.netstandard2.0.cs @@ -159,6 +159,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class ShareDirectoryClient @@ -452,8 +453,12 @@ public ShareServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyC public virtual Azure.Storage.Files.Shares.ShareClient GetShareClient(string shareName) { throw null; } public virtual Azure.Pageable GetShares(Azure.Storage.Files.Shares.Models.ShareTraits traits = Azure.Storage.Files.Shares.Models.ShareTraits.None, Azure.Storage.Files.Shares.Models.ShareStates states = Azure.Storage.Files.Shares.Models.ShareStates.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.AsyncPageable GetSharesAsync(Azure.Storage.Files.Shares.Models.ShareTraits traits = Azure.Storage.Files.Shares.Models.ShareTraits.None, Azure.Storage.Files.Shares.Models.ShareStates states = Azure.Storage.Files.Shares.Models.ShareStates.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Files.Shares.Models.ShareGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Files.Shares.Models.ShareGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Files.Shares.Models.ShareServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Files.Shares.Models.ShareServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response UndeleteShare(string deletedShareName, string deletedShareVersion, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -809,7 +814,10 @@ public ShareDirectorySetHttpHeadersOptions() { } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FeatureVersionMismatch { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileLockConflict { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedBandwidthDowngradeNotAllowed { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedBandwidthInvalid { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedIopsDowngradeNotAllowed { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedIopsInvalid { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode FileShareProvisionedStorageInvalid { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InsufficientAccountPermissions { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InternalError { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode InvalidAuthenticationInfo { get { throw null; } } @@ -854,6 +862,10 @@ public ShareDirectorySetHttpHeadersOptions() { } public static Azure.Storage.Files.Shares.Models.ShareErrorCode ShareSnapshotNotFound { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode ShareSnapshotOperationNotSupported { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode SharingViolation { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesCountExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedBandwidthExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedCapacityExceedsAccountLimit { get { throw null; } } + public static Azure.Storage.Files.Shares.Models.ShareErrorCode TotalSharesProvisionedIopsExceedsAccountLimit { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedHeader { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedHttpVerb { get { throw null; } } public static Azure.Storage.Files.Shares.Models.ShareErrorCode UnsupportedQueryParameter { get { throw null; } } @@ -946,6 +958,7 @@ public partial class ShareFileDownloadInfo : System.IDisposable { internal ShareFileDownloadInfo() { } public System.IO.Stream Content { get { throw null; } } + public byte[] ContentCrc { get { throw null; } } public byte[] ContentHash { get { throw null; } } public long ContentLength { get { throw null; } } public string ContentType { get { throw null; } } @@ -1200,6 +1213,13 @@ public ShareFileUploadRangeOptions() { } public byte[] TransactionalContentHash { get { throw null; } set { } } public Azure.Storage.UploadTransferValidationOptions TransferValidation { get { throw null; } set { } } } + public partial class ShareGetUserDelegationKeyOptions + { + public ShareGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class ShareInfo { internal ShareInfo() { } @@ -1281,7 +1301,9 @@ public static partial class ShareModelFactory [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public static Azure.Storage.Files.Shares.Models.StorageClosedHandlesSegment StorageClosedHandlesSegment(string marker, int numberOfHandlesClosed) { throw null; } public static Azure.Storage.Files.Shares.Models.StorageClosedHandlesSegment StorageClosedHandlesSegment(string marker, int numberOfHandlesClosed, int numberOfHandlesFailedToClose) { throw null; } - public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Files.Shares.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } public partial class ShareNfsSettings { @@ -1463,6 +1485,7 @@ internal StorageClosedHandlesSegment() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -1587,6 +1610,7 @@ public enum ShareSasPermissions public sealed partial class ShareSasQueryParameters : Azure.Storage.Sas.SasQueryParameters { internal ShareSasQueryParameters() { } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/assets.json b/sdk/storage/Azure.Storage.Files.Shares/assets.json index ea7dbce47013..152929f4f8df 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/assets.json +++ b/sdk/storage/Azure.Storage.Files.Shares/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Files.Shares", - "Tag": "net/storage/Azure.Storage.Files.Shares_91d454765e" + "Tag": "net/storage/Azure.Storage.Files.Shares_8cc433b62e" } diff --git a/sdk/storage/Azure.Storage.Files.Shares/samples/Azure.Storage.Files.Shares.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Files.Shares/samples/Azure.Storage.Files.Shares.Samples.Tests.csproj index 0bcec423c144..d1efeca0c2da 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/samples/Azure.Storage.Files.Shares.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Files.Shares/samples/Azure.Storage.Files.Shares.Samples.Tests.csproj @@ -16,6 +16,7 @@ + PreserveNewest diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Azure.Storage.Files.Shares.csproj b/sdk/storage/Azure.Storage.Files.Shares/src/Azure.Storage.Files.Shares.csproj index e14c757e5cf1..67e87cd7101e 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Azure.Storage.Files.Shares.csproj +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Azure.Storage.Files.Shares.csproj @@ -1,11 +1,11 @@ - + $(RequiredTargetFrameworks) true Microsoft Azure.Storage.Files.Shares client library - 12.25.0-beta.2 + 12.26.0-beta.1 12.24.0 FileSDK;$(DefineConstants) @@ -42,6 +42,7 @@ + @@ -85,6 +86,11 @@ + + + + + diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/DirectoryRestClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/DirectoryRestClient.cs index fb35a54387f7..501cdd666424 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/DirectoryRestClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/DirectoryRestClient.cs @@ -33,7 +33,7 @@ internal partial class DirectoryRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, share, directory or file that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// If true, the trailing dot will not be trimmed from the target URI. /// Valid value is backup. /// If true, the trailing dot will not be trimmed from the source URI. diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/FileRestClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/FileRestClient.cs index 42b19dc9fe6f..8eb691c6f2e5 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/FileRestClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/FileRestClient.cs @@ -34,7 +34,7 @@ internal partial class FileRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, share, directory or file that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// Only update is supported: - Update: Writes the bytes downloaded from the source url into the specified range. The default value is "update". /// If true, the trailing dot will not be trimmed from the target URI. /// Valid value is backup. diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.Serialization.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.Serialization.cs index 64f644f529db..2c6fe9a7a61f 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.Serialization.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.Serialization.cs @@ -25,6 +25,12 @@ void IXmlSerializable.Write(XmlWriter writer, string nameHint) writer.WriteStartElement("Expiry"); writer.WriteValue(Expiry); writer.WriteEndElement(); + if (Common.Optional.IsDefined(DelegatedUserTid)) + { + writer.WriteStartElement("DelegatedUserTid"); + writer.WriteValue(DelegatedUserTid); + writer.WriteEndElement(); + } writer.WriteEndElement(); } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.cs index 12c2e558b1d0..025a0bcca511 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/KeyInfo.cs @@ -26,15 +26,19 @@ public KeyInfo(string expiry) /// Initializes a new instance of . /// The date-time the key is active in ISO 8601 UTC time. /// The date-time the key expires in ISO 8601 UTC time. - internal KeyInfo(string start, string expiry) + /// The delegated user tenant id in Azure AD. + internal KeyInfo(string start, string expiry, string delegatedUserTid) { Start = start; Expiry = expiry; + DelegatedUserTid = delegatedUserTid; } /// The date-time the key is active in ISO 8601 UTC time. public string Start { get; set; } /// The date-time the key expires in ISO 8601 UTC time. public string Expiry { get; } + /// The delegated user tenant id in Azure AD. + public string DelegatedUserTid { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/ShareErrorCode.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/ShareErrorCode.cs index a8c59d06ec91..c1f2d8e27521 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/ShareErrorCode.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/ShareErrorCode.cs @@ -92,6 +92,13 @@ public ShareErrorCode(string value) private const string AuthorizationResourceTypeMismatchValue = "AuthorizationResourceTypeMismatch"; private const string FeatureVersionMismatchValue = "FeatureVersionMismatch"; private const string ShareSnapshotNotFoundValue = "ShareSnapshotNotFound"; + private const string FileShareProvisionedIopsInvalidValue = "FileShareProvisionedIopsInvalid"; + private const string FileShareProvisionedBandwidthInvalidValue = "FileShareProvisionedBandwidthInvalid"; + private const string FileShareProvisionedStorageInvalidValue = "FileShareProvisionedStorageInvalid"; + private const string TotalSharesProvisionedCapacityExceedsAccountLimitValue = "TotalSharesProvisionedCapacityExceedsAccountLimit"; + private const string TotalSharesProvisionedIopsExceedsAccountLimitValue = "TotalSharesProvisionedIopsExceedsAccountLimit"; + private const string TotalSharesProvisionedBandwidthExceedsAccountLimitValue = "TotalSharesProvisionedBandwidthExceedsAccountLimit"; + private const string TotalSharesCountExceedsAccountLimitValue = "TotalSharesCountExceedsAccountLimit"; /// AccountAlreadyExists. public static ShareErrorCode AccountAlreadyExists { get; } = new ShareErrorCode(AccountAlreadyExistsValue); @@ -233,6 +240,20 @@ public ShareErrorCode(string value) public static ShareErrorCode FeatureVersionMismatch { get; } = new ShareErrorCode(FeatureVersionMismatchValue); /// ShareSnapshotNotFound. public static ShareErrorCode ShareSnapshotNotFound { get; } = new ShareErrorCode(ShareSnapshotNotFoundValue); + /// FileShareProvisionedIopsInvalid. + public static ShareErrorCode FileShareProvisionedIopsInvalid { get; } = new ShareErrorCode(FileShareProvisionedIopsInvalidValue); + /// FileShareProvisionedBandwidthInvalid. + public static ShareErrorCode FileShareProvisionedBandwidthInvalid { get; } = new ShareErrorCode(FileShareProvisionedBandwidthInvalidValue); + /// FileShareProvisionedStorageInvalid. + public static ShareErrorCode FileShareProvisionedStorageInvalid { get; } = new ShareErrorCode(FileShareProvisionedStorageInvalidValue); + /// TotalSharesProvisionedCapacityExceedsAccountLimit. + public static ShareErrorCode TotalSharesProvisionedCapacityExceedsAccountLimit { get; } = new ShareErrorCode(TotalSharesProvisionedCapacityExceedsAccountLimitValue); + /// TotalSharesProvisionedIopsExceedsAccountLimit. + public static ShareErrorCode TotalSharesProvisionedIopsExceedsAccountLimit { get; } = new ShareErrorCode(TotalSharesProvisionedIopsExceedsAccountLimitValue); + /// TotalSharesProvisionedBandwidthExceedsAccountLimit. + public static ShareErrorCode TotalSharesProvisionedBandwidthExceedsAccountLimit { get; } = new ShareErrorCode(TotalSharesProvisionedBandwidthExceedsAccountLimitValue); + /// TotalSharesCountExceedsAccountLimit. + public static ShareErrorCode TotalSharesCountExceedsAccountLimit { get; } = new ShareErrorCode(TotalSharesCountExceedsAccountLimitValue); /// Determines if two values are the same. public static bool operator ==(ShareErrorCode left, ShareErrorCode right) => left.Equals(right); /// Determines if two values are not the same. diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.Serialization.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.Serialization.cs index de60fc768eb9..d199b3343996 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.Serialization.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.Serialization.cs @@ -21,6 +21,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) DateTimeOffset signedExpiresOn = default; string signedService = default; string signedVersion = default; + string signedDelegatedUserTenantId = default; string value = default; if (element.Element("SignedOid") is XElement signedOidElement) { @@ -46,6 +47,10 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) { signedVersion = (string)signedVersionElement; } + if (element.Element("SignedDelegatedUserTid") is XElement signedDelegatedUserTidElement) + { + signedDelegatedUserTenantId = (string)signedDelegatedUserTidElement; + } if (element.Element("Value") is XElement valueElement) { value = (string)valueElement; @@ -57,6 +62,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) signedExpiresOn, signedService, signedVersion, + signedDelegatedUserTenantId, value); } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.cs index 64d8739370f6..fcbec259879f 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/Models/UserDelegationKey.cs @@ -38,5 +38,26 @@ internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTim SignedVersion = signedVersion; Value = value; } + + /// Initializes a new instance of . + /// The Azure Active Directory object ID in GUID format. + /// The Azure Active Directory tenant ID in GUID format. + /// The date-time the key is active. + /// The date-time the key expires. + /// Abbreviation of the Azure Storage service that accepts the key. + /// The service version that created the key. + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// The key as a base64 string. + internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTimeOffset signedStartsOn, DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string signedDelegatedUserTenantId, string value) + { + SignedObjectId = signedObjectId; + SignedTenantId = signedTenantId; + SignedStartsOn = signedStartsOn; + SignedExpiresOn = signedExpiresOn; + SignedService = signedService; + SignedVersion = signedVersion; + SignedDelegatedUserTenantId = signedDelegatedUserTenantId; + Value = value; + } } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ServiceRestClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ServiceRestClient.cs index d0ac78e2b587..5b3b2a462c47 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ServiceRestClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ServiceRestClient.cs @@ -31,7 +31,7 @@ internal partial class ServiceRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, share, directory or file that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// Valid value is backup. /// , , or is null. public ServiceRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version, ShareTokenIntent? fileRequestIntent = null) diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ShareRestClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ShareRestClient.cs index 40e7a3b767d1..30b156002fe5 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ShareRestClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Generated/ShareRestClient.cs @@ -32,7 +32,7 @@ internal partial class ShareRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, share, directory or file that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// Valid value is backup. /// , , or is null. public ShareRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version, ShareTokenIntent? fileRequestIntent = null) diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareFileDownloadInfo.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareFileDownloadInfo.cs index 0165af94435a..4037cbdfd875 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareFileDownloadInfo.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareFileDownloadInfo.cs @@ -38,6 +38,12 @@ public partial class ShareFileDownloadInfo : IDisposable, IDownloadedContent public byte[] ContentHash { get; internal set; } #pragma warning restore CA1819 // Properties should not return arrays + /// + /// When requested using , this value contains the CRC for the download blob range. + /// This value may only become populated once the network stream is fully consumed. + /// + public byte[] ContentCrc { get; internal set; } + /// /// Details returned when downloading a file /// diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareGetUserDelegationKeyOptions.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareGetUserDelegationKeyOptions.cs new file mode 100644 index 000000000000..5d6c267dfb4e --- /dev/null +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareGetUserDelegationKeyOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Storage.Files.Shares.Models +{ + /// + /// Parameters for Get User Delegation Key. + /// + public class ShareGetUserDelegationKeyOptions + { + /// + /// Constructor for ShareGetUserDelegationKeyOptions. + /// + public ShareGetUserDelegationKeyOptions(DateTimeOffset expiresOn) + { + ExpiresOn = expiresOn; + } + + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + public DateTimeOffset ExpiresOn { get; set; } + + /// + /// Optional. Start time for the key's validity, with null indicating an + /// immediate start. The time should be specified in UTC. + /// + /// Note: If you set the start time to the current time, failures + /// might occur intermittently for the first few minutes. This is due to different + /// machines having slightly different current times (known as clock skew). + /// + public DateTimeOffset? StartsOn { get; set; } + + /// + /// Optional. The delegated user tenant id in Azure AD. + /// + public string DelegatedUserTenantId { get; set; } + } +} diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareModelFactory.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareModelFactory.cs index f983b242718e..b60eeda905a1 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareModelFactory.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareModelFactory.cs @@ -681,7 +681,34 @@ public static UserDelegationKey UserDelegationKey( DateTimeOffset signedExpiresOn = default, string signedService = default, string signedVersion = default, + string signedDelegatedUserTenantId = default, string value = default) + { + return new UserDelegationKey() + { + SignedObjectId = signedObjectId, + SignedTenantId = signedTenantId, + SignedStartsOn = signedStartsOn, + SignedExpiresOn = signedExpiresOn, + SignedService = signedService, + SignedVersion = signedVersion, + SignedDelegatedUserTenantId = signedDelegatedUserTenantId, + Value = value + }; + } + + /// + /// Creates a new UserDelegationKey instance for mocking. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static UserDelegationKey UserDelegationKey( + string signedObjectId, + string signedTenantId, + DateTimeOffset signedStartsOn, + DateTimeOffset signedExpiresOn, + string signedService, + string signedVersion, + string value) { return new UserDelegationKey() { diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Models/UserDelegationKey.cs index db91b3323e2b..64aa2954666c 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Models/UserDelegationKey.cs @@ -45,6 +45,12 @@ public partial class UserDelegationKey /// public string SignedVersion { get; internal set; } + /// + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// + [CodeGenMember("SignedDelegatedUserTid")] + public string SignedDelegatedUserTenantId { get; internal set; } + /// /// The key as a base64 string. /// diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasBuilder.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasBuilder.cs index 6321303a7f38..352cab7fa368 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text; +using System.Threading; using Azure.Core; using Azure.Storage.Files.Shares; using Azure.Storage.Files.Shares.Models; @@ -367,7 +368,7 @@ private string ToStringToSign(StorageSharedKeyCredential sharedKeyCredential) /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -384,7 +385,7 @@ public ShareSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelega /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -427,7 +428,8 @@ public ShareSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelega contentEncoding: ContentEncoding, contentLanguage: ContentLanguage, contentType: ContentType, - delegatedUserObjectId: DelegatedUserObjectId); + delegatedUserObjectId: DelegatedUserObjectId, + keyDelegatedUserTenantId: userDelegationKey.SignedDelegatedUserTenantId); return p; } @@ -450,7 +452,7 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun signedExpiry, userDelegationKey.SignedService, userDelegationKey.SignedVersion, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + userDelegationKey.SignedDelegatedUserTenantId, DelegatedUserObjectId, IPRange.ToString(), SasExtensions.ToProtocolString(Protocol), diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasQueryParameters.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasQueryParameters.cs index 812525e8ee32..e7ed392a2137 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasQueryParameters.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Sas/ShareSasQueryParameters.cs @@ -51,6 +51,11 @@ public sealed class ShareSasQueryParameters : SasQueryParameters /// public string KeyVersion => KeyProperties?.Version; + /// + /// Gets the delegated user tenant id. + /// + public string KeyDelegatedUserTenantId => KeyProperties?.DelegatedUserTenantId; + internal ShareSasQueryParameters() : base() { } @@ -81,7 +86,8 @@ internal ShareSasQueryParameters( string contentEncoding = default, string contentLanguage = default, string contentType = default, - string delegatedUserObjectId = default) + string delegatedUserObjectId = default, + string keyDelegatedUserTenantId = default) : base( version, services, @@ -113,7 +119,8 @@ internal ShareSasQueryParameters( StartsOn = keyStart, ExpiresOn = keyExpiry, Service = keyService, - Version = keyVersion + Version = keyVersion, + DelegatedUserTenantId = keyDelegatedUserTenantId }; } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs index cd5ec1f9fc62..8a38fc26a0d5 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs @@ -4245,7 +4245,7 @@ public virtual Uri GenerateSasUri(ShareSasBuilder builder, out string stringToSi /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4278,7 +4278,7 @@ public virtual Uri GenerateUserDelegationSasUri(ShareSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -4310,7 +4310,7 @@ public virtual Uri GenerateUserDelegationSasUri(ShareSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -4338,7 +4338,7 @@ public virtual Uri GenerateUserDelegationSasUri(ShareSasBuilder builder, UserDel /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs index 66448cccba61..46fce964a3b9 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs @@ -173,7 +173,12 @@ public enum ServiceVersion /// /// The 2026-02-06 service version. /// - V2026_02_06 = 29 + V2026_02_06 = 29, + + /// + /// The 2026-04-06 service version. + /// + V2026_04_06 = 30 #pragma warning restore CA1707 // Identifiers should not contain underscores } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareErrors.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareErrors.cs index 421103988280..2c8e550be2d0 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareErrors.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareErrors.cs @@ -17,7 +17,6 @@ public static InvalidOperationException FileOrShareMissing( string fileClient, string shareClient) => new InvalidOperationException($"{leaseClient} requires either a {fileClient} or {shareClient}"); - public static void AssertAlgorithmSupport(StorageChecksumAlgorithm? algorithm) { StorageChecksumAlgorithm resolved = (algorithm ?? StorageChecksumAlgorithm.None).ResolveAuto(); @@ -25,9 +24,8 @@ public static void AssertAlgorithmSupport(StorageChecksumAlgorithm? algorithm) { case StorageChecksumAlgorithm.None: case StorageChecksumAlgorithm.MD5: - return; case StorageChecksumAlgorithm.StorageCrc64: - throw new ArgumentException("Azure File Shares do not support CRC-64."); + return; default: throw new ArgumentException($"{nameof(StorageChecksumAlgorithm)} does not support value {Enum.GetName(typeof(StorageChecksumAlgorithm), resolved) ?? ((int)resolved).ToString(CultureInfo.InvariantCulture)}."); } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs index 1dd0cd4d48c9..b341bd8929b6 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs @@ -2577,51 +2577,70 @@ private async Task> DownloadInternal( // Wrap the response Content in a RetriableStream so we // can return it before it's finished downloading, but still // allow retrying if it fails. - initialResponse.Value.Content = RetriableStream.Create( - stream, - startOffset => - { - (Response Response, Stream ContentStream) = StartDownloadAsync( - range, - validationOptions, - conditions, - startOffset, - async, - cancellationToken) - .EnsureCompleted(); - if (etag != Response.GetRawResponse().Headers.ETag) - { - throw new ShareFileModifiedException( - "File has been modified concurrently", - Uri, etag, Response.GetRawResponse().Headers.ETag.GetValueOrDefault(), range); - } - return ContentStream; - }, - async startOffset => + async ValueTask> Factory(long offset, bool async, CancellationToken cancellationToken) + { + (Response response, Stream contentStream) = await StartDownloadAsync( + range, + validationOptions, + conditions, + offset, + async, + cancellationToken).ConfigureAwait(false); + if (etag != response.GetRawResponse().Headers.ETag) { - (Response Response, Stream ContentStream) = await StartDownloadAsync( - range, - validationOptions, - conditions, - startOffset, - async, - cancellationToken) - .ConfigureAwait(false); - if (etag != Response.GetRawResponse().Headers.ETag) + throw new ShareFileModifiedException( + "File has been modified concurrently", + Uri, etag, response.GetRawResponse().Headers.ETag.GetValueOrDefault(), range); + } + return response; + } + async ValueTask<(Stream DecodingStream, StructuredMessageDecodingStream.RawDecodedData DecodedData)> StructuredMessageFactory( + long offset, bool async, CancellationToken cancellationToken) + { + Response result = await Factory(offset, async, cancellationToken).ConfigureAwait(false); + return StructuredMessageDecodingStream.WrapStream(result.Value.Content, result.Value.ContentLength); + } + + if (initialResponse.GetRawResponse().Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) + { + (Stream decodingStream, StructuredMessageDecodingStream.RawDecodedData decodedData) = StructuredMessageDecodingStream.WrapStream( + initialResponse.Value.Content, initialResponse.Value.ContentLength); + initialResponse.Value.Content = new StructuredMessageDecodingRetriableStream( + decodingStream, + decodedData, + StructuredMessage.Flags.StorageCrc64, + startOffset => StructuredMessageFactory(startOffset, async: false, cancellationToken) + .EnsureCompleted(), + async startOffset => await StructuredMessageFactory(startOffset, async: true, cancellationToken) + .ConfigureAwait(false), + decodedData => { - throw new ShareFileModifiedException( - "File has been modified concurrently", - Uri, etag, Response.GetRawResponse().Headers.ETag.GetValueOrDefault(), range); - } - return ContentStream; - }, - ClientConfiguration.Pipeline.ResponseClassifier, - Constants.MaxReliabilityRetries); + initialResponse.Value.ContentCrc = new byte[StructuredMessage.Crc64Length]; + decodedData.Crc.WriteCrc64(initialResponse.Value.ContentCrc); + }, + ClientConfiguration.Pipeline.ResponseClassifier, + Constants.MaxReliabilityRetries); + } + else + { + initialResponse.Value.Content = RetriableStream.Create( + initialResponse.Value.Content, + startOffset => Factory(startOffset, async: false, cancellationToken) + .EnsureCompleted().Value.Content, + async startOffset => (await Factory(startOffset, async: true, cancellationToken) + .ConfigureAwait(false)).Value.Content, + ClientConfiguration.Pipeline.ResponseClassifier, + Constants.MaxReliabilityRetries); + } // buffer response stream and ensure it matches the transactional hash if any // Storage will not return a hash for payload >4MB, so this buffer is capped similarly // hashing is opt-in, so this buffer is part of that opt-in - if (validationOptions != default && validationOptions.ChecksumAlgorithm != StorageChecksumAlgorithm.None && validationOptions.AutoValidateChecksum) + if (validationOptions != default && + validationOptions.ChecksumAlgorithm != StorageChecksumAlgorithm.None && + validationOptions.AutoValidateChecksum && + // structured message decoding does the validation for us + !initialResponse.GetRawResponse().Headers.Contains(Constants.StructuredMessage.StructuredMessageHeader)) { // safe-buffer; transactional hash download limit well below maxInt var readDestStream = new MemoryStream((int)initialResponse.Value.ContentLength); @@ -2704,8 +2723,6 @@ await ContentHasher.AssertResponseHashMatchInternal( bool async = true, CancellationToken cancellationToken = default) { - ShareErrors.AssertAlgorithmSupport(transferValidationOverride?.ChecksumAlgorithm); - // calculation gets illegible with null coalesce; just pre-initialize var pageRange = range; pageRange = new HttpRange( @@ -2715,13 +2732,27 @@ await ContentHasher.AssertResponseHashMatchInternal( (long?)null); ClientConfiguration.Pipeline.LogTrace($"Download {Uri} with range: {pageRange}"); - ResponseWithHeaders response; + bool? rangeGetContentMD5 = null; + string structuredBodyType = null; + switch (transferValidationOverride?.ChecksumAlgorithm.ResolveAuto()) + { + case StorageChecksumAlgorithm.MD5: + rangeGetContentMD5 = true; + break; + case StorageChecksumAlgorithm.StorageCrc64: + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + break; + default: + break; + } + ResponseWithHeaders response; if (async) { response = await FileRestClient.DownloadAsync( range: pageRange == default ? null : pageRange.ToString(), - rangeGetContentMD5: transferValidationOverride?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.MD5 ? true : null, + rangeGetContentMD5: rangeGetContentMD5, + structuredBodyType: structuredBodyType, shareFileRequestConditions: conditions, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -2730,7 +2761,8 @@ await ContentHasher.AssertResponseHashMatchInternal( { response = FileRestClient.Download( range: pageRange == default ? null : pageRange.ToString(), - rangeGetContentMD5: transferValidationOverride?.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.MD5 ? true : null, + rangeGetContentMD5: rangeGetContentMD5, + structuredBodyType: structuredBodyType, shareFileRequestConditions: conditions, cancellationToken: cancellationToken); } @@ -4886,7 +4918,6 @@ internal async Task> UploadRangeInternal( CancellationToken cancellationToken) { UploadTransferValidationOptions validationOptions = transferValidationOverride ?? ClientConfiguration.TransferValidation.Upload; - ShareErrors.AssertAlgorithmSupport(validationOptions?.ChecksumAlgorithm); using (ClientConfiguration.Pipeline.BeginLoggingScope(nameof(ShareFileClient))) { @@ -4902,14 +4933,38 @@ internal async Task> UploadRangeInternal( scope.Start(); Errors.VerifyStreamPosition(content, nameof(content)); - // compute hash BEFORE attaching progress handler - ContentHasher.GetHashResult hashResult = await ContentHasher.GetHashOrDefaultInternal( - content, - validationOptions, - async, - cancellationToken).ConfigureAwait(false); - - content = content.WithNoDispose().WithProgress(progressHandler); + ContentHasher.GetHashResult hashResult = null; + long contentLength = (content?.Length - content?.Position) ?? 0; + long? structuredContentLength = default; + string structuredBodyType = null; + if (validationOptions != null && + validationOptions.ChecksumAlgorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64) + { + // report progress in terms of caller bytes, not encoded bytes + structuredContentLength = contentLength; + contentLength = (content?.Length - content?.Position) ?? 0; + structuredBodyType = Constants.StructuredMessage.CrcStructuredMessage; + content = content.WithNoDispose().WithProgress(progressHandler); + content = validationOptions.PrecalculatedChecksum.IsEmpty + ? new StructuredMessageEncodingStream( + content, + Constants.StructuredMessage.DefaultSegmentContentLength, + StructuredMessage.Flags.StorageCrc64) + : new StructuredMessagePrecalculatedCrcWrapperStream( + content, + validationOptions.PrecalculatedChecksum.Span); + contentLength = (content?.Length - content?.Position) ?? 0; + } + else + { + // compute hash BEFORE attaching progress handler + hashResult = await ContentHasher.GetHashOrDefaultInternal( + content, + validationOptions, + async, + cancellationToken).ConfigureAwait(false); + content = content.WithNoDispose().WithProgress(progressHandler); + } ResponseWithHeaders response; @@ -4922,6 +4977,8 @@ internal async Task> UploadRangeInternal( fileLastWrittenMode: fileLastWrittenMode, optionalbody: content, contentMD5: hashResult?.MD5AsArray, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, shareFileRequestConditions: conditions, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -4935,6 +4992,8 @@ internal async Task> UploadRangeInternal( fileLastWrittenMode: fileLastWrittenMode, optionalbody: content, contentMD5: hashResult?.MD5AsArray, + structuredBodyType: structuredBodyType, + structuredContentLength: structuredContentLength, shareFileRequestConditions: conditions, cancellationToken: cancellationToken); } @@ -7795,7 +7854,7 @@ public virtual Uri GenerateSasUri(ShareSasBuilder builder, out string stringToSi /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -7827,7 +7886,7 @@ public Uri GenerateUserDelegationSasUri(ShareFileSasPermissions permissions, Dat /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -7861,7 +7920,7 @@ public Uri GenerateUserDelegationSasUri(ShareFileSasPermissions permissions, Dat /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -7887,7 +7946,7 @@ public Uri GenerateUserDelegationSasUri(ShareSasBuilder builder, UserDelegationK /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs index c9ba7ed0f840..5ede38385be7 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs @@ -916,7 +916,7 @@ public virtual Response CreateShare( enablePaidBursting: options?.EnablePaidBursting, paidBurstingMaxIops: options?.PaidBurstingMaxIops, paidBurstingMaxBandwidthMibps: options?.PaidBurstingMaxBandwidthMibps, - provisionedMaxIops: options?.PaidBurstingMaxIops, + provisionedMaxIops: options?.ProvisionedMaxIops, provisionedMaxBandwidthMibps: options?.ProvisionedMaxBandwidthMibps, //enableDirectoryLease: options?.EnableDirectoryLease, async: false, @@ -973,7 +973,7 @@ public virtual async Task> CreateShareAsync( enablePaidBursting: options?.EnablePaidBursting, paidBurstingMaxIops: options?.PaidBurstingMaxIops, paidBurstingMaxBandwidthMibps: options?.PaidBurstingMaxBandwidthMibps, - provisionedMaxIops: options?.PaidBurstingMaxIops, + provisionedMaxIops: options?.ProvisionedMaxIops, provisionedMaxBandwidthMibps: options?.ProvisionedMaxBandwidthMibps, //enableDirectoryLease: options?.EnableDirectoryLease, async: true, @@ -1436,7 +1436,83 @@ private async Task> UndeleteShareInternal( #region GetUserDelegationKey /// - /// The operation retrieves a + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-shares")] + public virtual Response GetUserDelegationKey( + ShareGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + false, // async + cancellationToken) + .EnsureCompleted(); + } + + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-shares")] + public virtual async Task> GetUserDelegationKeyAsync( + ShareGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return await GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + true, // async + cancellationToken) + .ConfigureAwait(false); + } + + /// + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -1467,19 +1543,23 @@ private async Task> UndeleteShareInternal( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-shares")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual Response GetUserDelegationKey( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => GetUserDelegationKeyInternal( startsOn, expiresOn, + default, false, // async cancellationToken) .EnsureCompleted(); /// - /// The operation retrieves a + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -1510,13 +1590,17 @@ public virtual Response GetUserDelegationKey( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-files-shares")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual async Task> GetUserDelegationKeyAsync( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => await GetUserDelegationKeyInternal( startsOn, expiresOn, + default, true, // async cancellationToken) .ConfigureAwait(false); @@ -1538,6 +1622,9 @@ await GetUserDelegationKeyInternal( /// Expiration of the key's validity. The time should be specified /// in UTC. /// + /// + /// The delegated user tenant id in Azure AD. + /// /// /// Optional to propagate /// notifications that the operation should be cancelled. @@ -1556,6 +1643,7 @@ await GetUserDelegationKeyInternal( private async Task> GetUserDelegationKeyInternal( DateTimeOffset? startsOn, DateTimeOffset expiresOn, + string delegatedUserTenantId, bool async, CancellationToken cancellationToken) { @@ -1584,7 +1672,8 @@ private async Task> GetUserDelegationKeyInternal( KeyInfo keyInfo = new KeyInfo(expiresOn.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture)) { - Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture) + Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture), + DelegatedUserTid = delegatedUserTenantId }; ResponseWithHeaders response; diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareUriBuilder.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareUriBuilder.cs index c60edc43386c..88a29ec10349 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareUriBuilder.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareUriBuilder.cs @@ -232,7 +232,7 @@ public ShareUriBuilder(Uri uri) if (paramsMap.ContainsKey(Constants.Sas.Parameters.Version)) { - Sas = SasQueryParametersInternals.Create(paramsMap); + Sas = new ShareSasQueryParameters(paramsMap); } Query = paramsMap.ToString(); diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/autorest.md b/sdk/storage/Azure.Storage.Files.Shares/src/autorest.md index b87e9177af73..7fca09049fb4 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/autorest.md +++ b/sdk/storage/Azure.Storage.Files.Shares/src/autorest.md @@ -4,7 +4,7 @@ Run `dotnet build /t:GenerateCode` to generate code. ``` yaml input-file: - - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/b6472ffd34d5d4a155101b41b4eb1f356abff600/specification/storage/data-plane/Microsoft.FileStorage/stable/2026-02-06/file.json + - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/42b84487c693d3f30939b81ded12b26e931a619b/specification/storage/data-plane/Microsoft.FileStorage/stable/2026-04-06/file.json generation1-convenience-client: true # https://github.com/Azure/autorest/issues/4075 skip-semantics-validation: true @@ -25,7 +25,7 @@ directive: if (property.includes('/{shareName}/{directory}/{fileName}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/ShareName") && false == param['$ref'].endsWith("#/parameters/DirectoryPath") && false == param['$ref'].endsWith("#/parameters/FilePath"))}); - } + } else if (property.includes('/{shareName}/{directory}')) { $[property]["parameters"] = $[property]["parameters"].filter(function(param) { return (typeof param['$ref'] === "undefined") || (false == param['$ref'].endsWith("#/parameters/ShareName") && false == param['$ref'].endsWith("#/parameters/DirectoryPath"))}); @@ -46,7 +46,7 @@ directive: $.Metrics.type = "object"; ``` -### Times aren't required +### Times aren't required ``` yaml directive: - from: swagger-document diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/Azure.Storage.Files.Shares.Tests.csproj b/sdk/storage/Azure.Storage.Files.Shares/tests/Azure.Storage.Files.Shares.Tests.csproj index 3a6d542efdba..3337bc1790a9 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/Azure.Storage.Files.Shares.Tests.csproj +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/Azure.Storage.Files.Shares.Tests.csproj @@ -18,6 +18,7 @@ + PreserveNewest diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/FileSasBuilderTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/FileSasBuilderTests.cs index 94c9404960c6..1401d783d57f 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/FileSasBuilderTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/FileSasBuilderTests.cs @@ -317,6 +317,7 @@ public void ShareSasBuilder_ToSasQuerParameters_IdentitySas() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.File, sasQueryParameters.Resource); Assert.AreEqual(constants.Sas.CacheControl, sasQueryParameters.CacheControl); Assert.AreEqual(constants.Sas.ContentDisposition, sasQueryParameters.ContentDisposition); @@ -416,7 +417,7 @@ private string BuildUserDelegationSasSignature( SasExtensions.FormatTimesForSasSigning(constants.Sas.KeyExpiry), constants.Sas.KeyService, constants.Sas.KeyVersion, - null, + constants.Sas.KeyDelegatedTenantId, constants.Sas.DelegatedObjectId, constants.Sas.IPRange.ToString(), SasExtensions.ToProtocolString(constants.Sas.Protocol), @@ -445,6 +446,7 @@ private static UserDelegationKey GetUserDelegationKey(TestConstants constants) SignedExpiresOn = constants.Sas.KeyExpiry, SignedService = constants.Sas.KeyService, SignedVersion = constants.Sas.KeyVersion, + SignedDelegatedUserTenantId = constants.Sas.KeyDelegatedTenantId, Value = constants.Sas.KeyValue }; } diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs index af453474a428..958f71251768 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs @@ -698,7 +698,11 @@ public async Task GetUserDelegationKey() DateTimeOffset expiryTime = Recording.UtcNow.AddHours(1); // Act - Response response = await service.GetUserDelegationKeyAsync(startTime, expiryTime); + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions(expiresOn: expiryTime) + { + StartsOn = startTime, + }; + Response response = await service.GetUserDelegationKeyAsync(options); // Assert Assert.IsNotNull(response.Value); @@ -712,8 +716,9 @@ public async Task GetUserDelegationKey_Error() ShareServiceClient service = SharesClientBuilder.GetServiceClient_SharedKey(); // Act + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)), + service.GetUserDelegationKeyAsync(options: options), e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } @@ -725,14 +730,15 @@ public async Task GetUserDelegationKey_ArgumentException() ShareServiceClient service = GetServiceClient_OAuth(); // Act - await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync( - startsOn: null, + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions( // ensure the time used is not UTC, as DateTimeOffset.Now could actually be UTC based on OS settings // Use a custom time zone so we aren't dependent on OS having specific standard time zone. expiresOn: TimeZoneInfo.ConvertTime( Recording.Now.AddHours(1), - TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))), + TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))); + await TestHelper.AssertExpectedExceptionAsync( + service.GetUserDelegationKeyAsync( + options: options), e => Assert.AreEqual("expiresOn must be UTC", e.Message)); ; } diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTestFixtureAttribute.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTestFixtureAttribute.cs index 698356c67d1a..7edae595ea3e 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTestFixtureAttribute.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTestFixtureAttribute.cs @@ -42,6 +42,7 @@ public ShareClientTestFixtureAttribute(params object[] additionalParameters) ShareClientOptions.ServiceVersion.V2025_07_05, ShareClientOptions.ServiceVersion.V2025_11_05, ShareClientOptions.ServiceVersion.V2026_02_06, + ShareClientOptions.ServiceVersion.V2026_04_06, StorageVersionExtensions.LatestVersion, StorageVersionExtensions.MaxVersion }, diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareFileClientTransferValidationTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareFileClientTransferValidationTests.cs index 87acd04844e3..2ea094c82c92 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareFileClientTransferValidationTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareFileClientTransferValidationTests.cs @@ -65,10 +65,6 @@ protected override async Task GetResourceClientAsync( private void AssertSupportsHashAlgorithm(StorageChecksumAlgorithm algorithm) { - if (algorithm.ResolveAuto() == StorageChecksumAlgorithm.StorageCrc64) - { - TestHelper.AssertInconclusiveRecordingFriendly(Recording.Mode, "Azure File Share does not support CRC64."); - } } protected override async Task UploadPartitionAsync(ShareFileClient client, Stream source, UploadTransferValidationOptions transferValidation) @@ -148,7 +144,7 @@ protected override async Task SetupDataAsync(ShareFileClient client, Stream data public override void TestAutoResolve() { Assert.AreEqual( - StorageChecksumAlgorithm.MD5, + StorageChecksumAlgorithm.StorageCrc64, TransferValidationOptionsExtensions.ResolveAuto(StorageChecksumAlgorithm.Auto)); } @@ -362,5 +358,40 @@ public override void TestAutoResolve() // // Assert // // Assertion was in the pipeline //} + + public async Task StructuredMessagePopulatesCrcDownloadStreaming() + { + await using DisposingShare disposingContainer = await ClientBuilder.GetTestShareAsync(); + + const int dataLength = Constants.KB; + byte[] data = GetRandomBuffer(dataLength); + byte[] dataCrc = new byte[8]; + StorageCrc64Calculator.ComputeSlicedSafe(data, 0L).WriteCrc64(dataCrc); + + ShareFileClient file = disposingContainer.Container.GetRootDirectoryClient().GetFileClient(GetNewResourceName()); + await file.CreateAsync(data.Length); + await file.UploadAsync(new MemoryStream(data)); + + Response response = await file.DownloadAsync(new ShareFileDownloadOptions() + { + TransferValidation = new DownloadTransferValidationOptions + { + ChecksumAlgorithm = StorageChecksumAlgorithm.StorageCrc64 + } + }); + + // crc is not present until response stream is consumed + Assert.That(response.Value.ContentCrc, Is.Null); + + byte[] downloadedData; + using (MemoryStream ms = new()) + { + await response.Value.Content.CopyToAsync(ms); + downloadedData = ms.ToArray(); + } + + Assert.That(response.Value.ContentCrc, Is.EqualTo(dataCrc)); + Assert.That(downloadedData, Is.EqualTo(data)); + } } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareSasTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareSasTests.cs index a7322a26d9ad..7e68f53cee44 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareSasTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareSasTests.cs @@ -856,9 +856,13 @@ public async Task ShareClient_GetUserDelegationSAS() await using DisposingShare test = await GetTestShareAsync(service); ShareClient share = test.Share; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri sasUri = share.GenerateUserDelegationSasUri( ShareSasPermissions.All, @@ -885,9 +889,13 @@ public async Task ShareClient_GetUserDelegationSAS_Builder() await using DisposingShare test = await GetTestShareAsync(service); ShareClient share = test.Share; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); ShareSasBuilder builder = new ShareSasBuilder( permissions: ShareSasPermissions.All, @@ -914,9 +922,13 @@ public async Task FileClient_GetUserDelegationSAS() await using DisposingFile test = await SharesClientBuilder.GetTestFileAsync(service); ShareFileClient file = test.File; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); Uri sasUri = file.GenerateUserDelegationSasUri( ShareFileSasPermissions.All, @@ -943,9 +955,13 @@ public async Task FileClient_GetUserDelegationSAS_Builder() await using DisposingFile test = await SharesClientBuilder.GetTestFileAsync(service); ShareFileClient file = test.File; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); ShareSasBuilder builder = new ShareSasBuilder( permissions: ShareFileSasPermissions.All, @@ -966,6 +982,166 @@ public async Task FileClient_GetUserDelegationSAS_Builder() Assert.IsNotNull(stringToSign); } + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2026_04_06)] + public async Task ShareClient_UserDelegationSas_DelegatedTenantId() + { + // Arrange + ShareServiceClient service = GetServiceClient_OAuth(); + await using DisposingShare test = await GetTestShareAsync(service); + ShareClient share = test.Share; + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + ShareSasBuilder sasBuilder = new ShareSasBuilder(permissions: ShareSasPermissions.All, expiresOn: Recording.UtcNow.AddHours(1)) + { + ShareName = share.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + ShareSasQueryParameters sasQueryParameters = sasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName); + + ShareUriBuilder shareUriBuilder = new ShareUriBuilder(share.Uri) + { + Sas = sasQueryParameters + }; + + ShareClientOptions clientOptions = GetOptions(); + clientOptions.ShareTokenIntent = ShareTokenIntent.Backup; + ShareClient sasShare = InstrumentClient(new ShareClient(shareUriBuilder.ToUri(), TestEnvironment.Credential, clientOptions)); + + // Act + Response response = await sasShare.CreateDirectoryAsync(GetNewDirectoryName()); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2026_04_06)] + public async Task ShareClient_UserDelegationSas_DelegatedTenantId_Fail() + { + // Arrange + ShareServiceClient service = GetServiceClient_OAuth(); + await using DisposingShare test = await GetTestShareAsync(service); + ShareClient share = test.Share; + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + ShareSasBuilder sasBuilder = new ShareSasBuilder(permissions: ShareSasPermissions.Read, expiresOn: Recording.UtcNow.AddHours(1)) + { + ShareName = share.Name, + // We are deliberately not passing in DelegatedUserObjectId to cause an auth failure + }; + + ShareSasQueryParameters sasQueryParameters = sasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName); + + ShareUriBuilder shareUriBuilder = new ShareUriBuilder(share.Uri) + { + Sas = sasQueryParameters + }; + + ShareClient sasShare = InstrumentClient(new ShareClient(shareUriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act & Assert + await TestHelper.AssertExpectedExceptionAsync( + sasShare.CreateDirectoryAsync(GetNewDirectoryName()), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] + [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2026_04_06)] + public async Task ShareClient_UserDelegationSas_DelegatedTenantId_Roundtrip() + { + // Arrange + ShareServiceClient service = GetServiceClient_OAuth(); + await using DisposingShare test = await GetTestShareAsync(service); + ShareClient share = test.Share; + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + ShareGetUserDelegationKeyOptions options = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + ShareSasBuilder sasBuilder = new ShareSasBuilder(permissions: ShareSasPermissions.Read, expiresOn: Recording.UtcNow.AddHours(1)) + { + ShareName = share.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + ShareSasQueryParameters sasQueryParameters = sasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName); + + ShareUriBuilder originalShareUriBuilder = new ShareUriBuilder(share.Uri) + { + Sas = sasQueryParameters + }; + + ShareUriBuilder roundtripShareUriBuilder = new ShareUriBuilder(originalShareUriBuilder.ToUri()); + + Assert.AreEqual(originalShareUriBuilder.ToUri(), roundtripShareUriBuilder.ToUri()); + Assert.AreEqual(originalShareUriBuilder.Sas.ToString(), roundtripShareUriBuilder.Sas.ToString()); + } + [RecordedTest] [LiveOnly] // Cannot record Entra ID token [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2026_02_06)] @@ -976,9 +1152,13 @@ public async Task ShareClient_UserDelegationSas_DelegatedObjectId() await using DisposingShare test = await GetTestShareAsync(service); ShareClient share = test.Share; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -1024,9 +1204,13 @@ public async Task ShareClient_UserDelegationSas_DelegatedObjectId_Fail() await using DisposingShare test = await GetTestShareAsync(service); ShareClient share = test.Share; + ShareGetUserDelegationKeyOptions getUserDelegationKeyOptions = new ShareGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + StartsOn = Recording.UtcNow.AddHours(-1) + }; + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: Recording.UtcNow.AddHours(-1), - expiresOn: Recording.UtcNow.AddHours(1)); + options: getUserDelegationKeyOptions); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; diff --git a/sdk/storage/Azure.Storage.Queues/CHANGELOG.md b/sdk/storage/Azure.Storage.Queues/CHANGELOG.md index b70b7199ecbc..24d5e87978cd 100644 --- a/sdk/storage/Azure.Storage.Queues/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Queues/CHANGELOG.md @@ -1,14 +1,10 @@ # Release History -## 12.25.0-beta.2 (Unreleased) +## 12.26.0-beta.1 (Unreleased) ### Features Added - -### Breaking Changes - -### Bugs Fixed - -### Other Changes +- Added support for service version 2026-04-06. +- Added cross-tenant support for Principal-Bound User Delegation SAS. ## 12.25.0-beta.1 (2025-11-17) diff --git a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net8.0.cs b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net8.0.cs index 1fb1a2c4e92d..cb62ebb4d669 100644 --- a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net8.0.cs +++ b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net8.0.cs @@ -118,6 +118,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class QueueMessageDecodingFailedEventArgs : Azure.SyncAsyncEventArgs @@ -161,8 +162,12 @@ public QueueServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyC public virtual Azure.AsyncPageable GetQueuesAsync(Azure.Storage.Queues.Models.QueueTraits traits = Azure.Storage.Queues.Models.QueueTraits.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } @@ -328,6 +333,13 @@ public enum QueueGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class QueueGetUserDelegationKeyOptions + { + public QueueGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class QueueItem { internal QueueItem() { } @@ -405,7 +417,9 @@ public static partial class QueuesModelFactory public static Azure.Storage.Queues.Models.QueueServiceStatistics QueueServiceStatistics(Azure.Storage.Queues.Models.QueueGeoReplication geoReplication = null) { throw null; } public static Azure.Storage.Queues.Models.SendReceipt SendReceipt(string messageId, System.DateTimeOffset insertionTime, System.DateTimeOffset expirationTime, string popReceipt, System.DateTimeOffset timeNextVisible) { throw null; } public static Azure.Storage.Queues.Models.UpdateReceipt UpdateReceipt(string popReceipt, System.DateTimeOffset nextVisibleOn) { throw null; } - public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } [System.FlagsAttribute] public enum QueueTraits @@ -431,6 +445,7 @@ internal UpdateReceipt() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -521,6 +536,7 @@ public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQuery { internal QueueSasQueryParameters() { } public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.0.cs b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.0.cs index 3c5b9571c958..551a1bf8667a 100644 --- a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.0.cs +++ b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.0.cs @@ -118,6 +118,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class QueueMessageDecodingFailedEventArgs : Azure.SyncAsyncEventArgs @@ -161,8 +162,12 @@ public QueueServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyC public virtual Azure.AsyncPageable GetQueuesAsync(Azure.Storage.Queues.Models.QueueTraits traits = Azure.Storage.Queues.Models.QueueTraits.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } @@ -328,6 +333,13 @@ public enum QueueGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class QueueGetUserDelegationKeyOptions + { + public QueueGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class QueueItem { internal QueueItem() { } @@ -405,7 +417,9 @@ public static partial class QueuesModelFactory public static Azure.Storage.Queues.Models.QueueServiceStatistics QueueServiceStatistics(Azure.Storage.Queues.Models.QueueGeoReplication geoReplication = null) { throw null; } public static Azure.Storage.Queues.Models.SendReceipt SendReceipt(string messageId, System.DateTimeOffset insertionTime, System.DateTimeOffset expirationTime, string popReceipt, System.DateTimeOffset timeNextVisible) { throw null; } public static Azure.Storage.Queues.Models.UpdateReceipt UpdateReceipt(string popReceipt, System.DateTimeOffset nextVisibleOn) { throw null; } - public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } [System.FlagsAttribute] public enum QueueTraits @@ -431,6 +445,7 @@ internal UpdateReceipt() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -521,6 +536,7 @@ public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQuery { internal QueueSasQueryParameters() { } public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.1.cs b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.1.cs index 3c5b9571c958..551a1bf8667a 100644 --- a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.1.cs +++ b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.netstandard2.1.cs @@ -118,6 +118,7 @@ public enum ServiceVersion V2025_07_05 = 27, V2025_11_05 = 28, V2026_02_06 = 29, + V2026_04_06 = 30, } } public partial class QueueMessageDecodingFailedEventArgs : Azure.SyncAsyncEventArgs @@ -161,8 +162,12 @@ public QueueServiceClient(System.Uri serviceUri, Azure.Storage.StorageSharedKeyC public virtual Azure.AsyncPageable GetQueuesAsync(Azure.Storage.Queues.Models.QueueTraits traits = Azure.Storage.Queues.Models.QueueTraits.None, string prefix = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual Azure.Response GetStatistics(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task> GetStatisticsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Response GetUserDelegationKey(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual Azure.Response GetUserDelegationKey(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(Azure.Storage.Queues.Models.QueueGetUserDelegationKeyOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task> GetUserDelegationKeyAsync(System.DateTimeOffset? startsOn, System.DateTimeOffset expiresOn, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response SetProperties(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task SetPropertiesAsync(Azure.Storage.Queues.Models.QueueServiceProperties properties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } @@ -328,6 +333,13 @@ public enum QueueGeoReplicationStatus Bootstrap = 1, Unavailable = 2, } + public partial class QueueGetUserDelegationKeyOptions + { + public QueueGetUserDelegationKeyOptions(System.DateTimeOffset expiresOn) { } + public string DelegatedUserTenantId { get { throw null; } set { } } + public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } + public System.DateTimeOffset? StartsOn { get { throw null; } set { } } + } public partial class QueueItem { internal QueueItem() { } @@ -405,7 +417,9 @@ public static partial class QueuesModelFactory public static Azure.Storage.Queues.Models.QueueServiceStatistics QueueServiceStatistics(Azure.Storage.Queues.Models.QueueGeoReplication geoReplication = null) { throw null; } public static Azure.Storage.Queues.Models.SendReceipt SendReceipt(string messageId, System.DateTimeOffset insertionTime, System.DateTimeOffset expirationTime, string popReceipt, System.DateTimeOffset timeNextVisible) { throw null; } public static Azure.Storage.Queues.Models.UpdateReceipt UpdateReceipt(string popReceipt, System.DateTimeOffset nextVisibleOn) { throw null; } - public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string value = null) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId, string signedTenantId, System.DateTimeOffset signedStartsOn, System.DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) { throw null; } + public static Azure.Storage.Queues.Models.UserDelegationKey UserDelegationKey(string signedObjectId = null, string signedTenantId = null, System.DateTimeOffset signedStartsOn = default(System.DateTimeOffset), System.DateTimeOffset signedExpiresOn = default(System.DateTimeOffset), string signedService = null, string signedVersion = null, string signedDelegatedUserTenantId = null, string value = null) { throw null; } } [System.FlagsAttribute] public enum QueueTraits @@ -431,6 +445,7 @@ internal UpdateReceipt() { } public partial class UserDelegationKey { internal UserDelegationKey() { } + public string SignedDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset SignedExpiresOn { get { throw null; } } public string SignedObjectId { get { throw null; } } public string SignedService { get { throw null; } } @@ -521,6 +536,7 @@ public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQuery { internal QueueSasQueryParameters() { } public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public string KeyDelegatedUserTenantId { get { throw null; } } public System.DateTimeOffset KeyExpiresOn { get { throw null; } } public string KeyObjectId { get { throw null; } } public string KeyService { get { throw null; } } diff --git a/sdk/storage/Azure.Storage.Queues/samples/Azure.Storage.Queues.Samples.Tests.csproj b/sdk/storage/Azure.Storage.Queues/samples/Azure.Storage.Queues.Samples.Tests.csproj index f9ed70da2e75..12794e190f4e 100644 --- a/sdk/storage/Azure.Storage.Queues/samples/Azure.Storage.Queues.Samples.Tests.csproj +++ b/sdk/storage/Azure.Storage.Queues/samples/Azure.Storage.Queues.Samples.Tests.csproj @@ -16,6 +16,7 @@ + PreserveNewest diff --git a/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj b/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj index 3dc5ad536d0d..127c6f914cda 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj +++ b/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.Queues client library - 12.25.0-beta.2 + 12.26.0-beta.1 12.24.0 QueueSDK;$(DefineConstants) diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs index 2be78151d054..a596c804a4e4 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs @@ -27,7 +27,7 @@ internal partial class MessageIdRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, queue or message that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public MessageIdRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/MessagesRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/MessagesRestClient.cs index 880709e94889..59d9c0107c49 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/MessagesRestClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/MessagesRestClient.cs @@ -29,7 +29,7 @@ internal partial class MessagesRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, queue or message that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public MessagesRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.Serialization.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.Serialization.cs index 1f0486da2510..20b89e3c39e6 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.Serialization.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.Serialization.cs @@ -25,6 +25,12 @@ void IXmlSerializable.Write(XmlWriter writer, string nameHint) writer.WriteStartElement("Expiry"); writer.WriteValue(Expiry); writer.WriteEndElement(); + if (Common.Optional.IsDefined(DelegatedUserTid)) + { + writer.WriteStartElement("DelegatedUserTid"); + writer.WriteValue(DelegatedUserTid); + writer.WriteEndElement(); + } writer.WriteEndElement(); } } diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.cs index 32040e89ef20..c6a241893d3b 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.cs @@ -26,15 +26,19 @@ public KeyInfo(string expiry) /// Initializes a new instance of . /// The date-time the key is active in ISO 8601 UTC time. /// The date-time the key expires in ISO 8601 UTC time. - internal KeyInfo(string start, string expiry) + /// The delegated user tenant id in Azure AD. + internal KeyInfo(string start, string expiry, string delegatedUserTid) { Start = start; Expiry = expiry; + DelegatedUserTid = delegatedUserTid; } /// The date-time the key is active in ISO 8601 UTC time. public string Start { get; set; } /// The date-time the key expires in ISO 8601 UTC time. public string Expiry { get; } + /// The delegated user tenant id in Azure AD. + public string DelegatedUserTid { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.Serialization.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.Serialization.cs index 07b6f6633730..b9a2dfd74feb 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.Serialization.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.Serialization.cs @@ -21,6 +21,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) DateTimeOffset signedExpiresOn = default; string signedService = default; string signedVersion = default; + string signedDelegatedUserTenantId = default; string value = default; if (element.Element("SignedOid") is XElement signedOidElement) { @@ -46,6 +47,10 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) { signedVersion = (string)signedVersionElement; } + if (element.Element("SignedDelegatedUserTid") is XElement signedDelegatedUserTidElement) + { + signedDelegatedUserTenantId = (string)signedDelegatedUserTidElement; + } if (element.Element("Value") is XElement valueElement) { value = (string)valueElement; @@ -57,6 +62,7 @@ internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) signedExpiresOn, signedService, signedVersion, + signedDelegatedUserTenantId, value); } } diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.cs index dc341a1f1483..10fad26bbad1 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.cs @@ -38,5 +38,26 @@ internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTim SignedVersion = signedVersion; Value = value; } + + /// Initializes a new instance of . + /// The Azure Active Directory object ID in GUID format. + /// The Azure Active Directory tenant ID in GUID format. + /// The date-time the key is active. + /// The date-time the key expires. + /// Abbreviation of the Azure Storage service that accepts the key. + /// The service version that created the key. + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// The key as a base64 string. + internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTimeOffset signedStartsOn, DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string signedDelegatedUserTenantId, string value) + { + SignedObjectId = signedObjectId; + SignedTenantId = signedTenantId; + SignedStartsOn = signedStartsOn; + SignedExpiresOn = signedExpiresOn; + SignedService = signedService; + SignedVersion = signedVersion; + SignedDelegatedUserTenantId = signedDelegatedUserTenantId; + Value = value; + } } } diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/QueueRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/QueueRestClient.cs index 74f77dafeb91..8b2c120cf1c1 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/QueueRestClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/QueueRestClient.cs @@ -29,7 +29,7 @@ internal partial class QueueRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, queue or message that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public QueueRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs index 4002fe504c2b..dc16f3f45f7d 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs @@ -30,7 +30,7 @@ internal partial class ServiceRestClient /// The handler for diagnostic messaging in the client. /// The HTTP pipeline for sending and receiving REST requests and responses. /// The URL of the service account, queue or message that is the target of the desired operation. - /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". + /// Specifies the version of the operation to use for this request. /// , , or is null. public ServiceRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/QueueGetUserDelegationKeyOptions.cs b/sdk/storage/Azure.Storage.Queues/src/Models/QueueGetUserDelegationKeyOptions.cs new file mode 100644 index 000000000000..67f9694b5a6a --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Models/QueueGetUserDelegationKeyOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Storage.Queues.Models +{ + /// + /// Parameters for Get User Delegation Key. + /// + public class QueueGetUserDelegationKeyOptions + { + /// + /// Constructor for QueueGetUserDelegationKeyOptions. + /// + public QueueGetUserDelegationKeyOptions(DateTimeOffset expiresOn) + { + ExpiresOn = expiresOn; + } + + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + public DateTimeOffset ExpiresOn { get; set; } + + /// + /// Optional. Start time for the key's validity, with null indicating an + /// immediate start. The time should be specified in UTC. + /// + /// Note: If you set the start time to the current time, failures + /// might occur intermittently for the first few minutes. This is due to different + /// machines having slightly different current times (known as clock skew). + /// + public DateTimeOffset? StartsOn { get; set; } + + /// + /// Optional. The delegated user tenant id in Azure AD. + /// + public string DelegatedUserTenantId { get; set; } + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/QueuesModelFactory.cs b/sdk/storage/Azure.Storage.Queues/src/Models/QueuesModelFactory.cs index c326116636f7..4080683bf20a 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Models/QueuesModelFactory.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Models/QueuesModelFactory.cs @@ -217,7 +217,34 @@ public static UserDelegationKey UserDelegationKey( DateTimeOffset signedExpiresOn = default, string signedService = default, string signedVersion = default, + string signedDelegatedUserTenantId = default, string value = default) + { + return new UserDelegationKey() + { + SignedObjectId = signedObjectId, + SignedTenantId = signedTenantId, + SignedStartsOn = signedStartsOn, + SignedExpiresOn = signedExpiresOn, + SignedService = signedService, + SignedVersion = signedVersion, + SignedDelegatedUserTenantId = signedDelegatedUserTenantId, + Value = value + }; + } + + /// + /// Creates a new UserDelegationKey instance for mocking. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static UserDelegationKey UserDelegationKey( + string signedObjectId, + string signedTenantId, + DateTimeOffset signedStartsOn, + DateTimeOffset signedExpiresOn, + string signedService, + string signedVersion, + string value) { return new UserDelegationKey() { diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs index 56579552a153..7cb2ae9bddd3 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs @@ -45,6 +45,12 @@ public partial class UserDelegationKey /// public string SignedVersion { get; internal set; } + /// + /// The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + /// + [CodeGenMember("SignedDelegatedUserTid")] + public string SignedDelegatedUserTenantId { get; internal set; } + /// /// The key as a base64 string. /// diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs index b65bf20d6a40..10e30ff98e31 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs @@ -3289,7 +3289,7 @@ public virtual Uri GenerateSasUri( /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -3322,7 +3322,7 @@ public virtual Uri GenerateUserDelegationSasUri(QueueSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. @@ -3354,7 +3354,7 @@ public virtual Uri GenerateUserDelegationSasUri(QueueSasPermissions permissions, /// /// /// Required. A returned from - /// . + /// . /// /// /// A containing the SAS Uri. @@ -3382,7 +3382,7 @@ public virtual Uri GenerateUserDelegationSasUri(QueueSasBuilder builder, UserDel /// /// /// Required. A returned from - /// . + /// . /// /// /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the SAS Uri. diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs b/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs index bf3515b2ad8d..7f94d159d0fb 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs @@ -179,7 +179,12 @@ public enum ServiceVersion /// /// The 2026-02-06 service version. /// - V2026_02_06 = 29 + V2026_02_06 = 29, + + /// + /// The 2026-04-06 service version. + /// + V2026_04_06 = 30 #pragma warning restore CA1707 // Identifiers should not contain underscores } diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs index 07d04d77c70c..3e825ec2af2e 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs @@ -967,7 +967,83 @@ await GetQueueClient(queueName) #region GetUserDelegationKey /// - /// The operation retrieves a + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual Response GetUserDelegationKey( + QueueGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + false, // async + cancellationToken) + .EnsureCompleted(); + } + + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// Optional parameters. + /// + /// + /// Optional to propagate + /// notifications that the operation should be cancelled. + /// + /// + /// A describing + /// the service replication statistics. + /// + /// + /// A will be thrown if + /// a failure occurs. + /// If multiple failures occur, an will be thrown, + /// containing each failure instance. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual async Task> GetUserDelegationKeyAsync( + QueueGetUserDelegationKeyOptions options, + CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(options, nameof(options)); + + return await GetUserDelegationKeyInternal( + options.StartsOn, + options.ExpiresOn, + options.DelegatedUserTenantId, + true, // async + cancellationToken) + .ConfigureAwait(false); + } + + /// + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -998,19 +1074,23 @@ await GetQueueClient(queueName) /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual Response GetUserDelegationKey( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => GetUserDelegationKeyInternal( startsOn, expiresOn, + default, false, // async cancellationToken) .EnsureCompleted(); /// - /// The operation retrieves a + /// The operation retrieves a /// key that can be used to delegate Active Directory authorization to /// shared access signatures created with . /// @@ -1041,13 +1121,17 @@ public virtual Response GetUserDelegationKey( /// containing each failure instance. /// [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. public virtual async Task> GetUserDelegationKeyAsync( +#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called 'cancellationToken' or a RequestContext parameter called 'context'. DateTimeOffset? startsOn, DateTimeOffset expiresOn, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken) => await GetUserDelegationKeyInternal( startsOn, expiresOn, + default, true, // async cancellationToken) .ConfigureAwait(false); @@ -1069,6 +1153,9 @@ await GetUserDelegationKeyInternal( /// Expiration of the key's validity. The time should be specified /// in UTC. /// + /// + /// The delegated user tenant id in Azure AD. + /// /// /// Optional to propagate /// notifications that the operation should be cancelled. @@ -1087,6 +1174,7 @@ await GetUserDelegationKeyInternal( private async Task> GetUserDelegationKeyInternal( DateTimeOffset? startsOn, DateTimeOffset expiresOn, + string delegatedUserTenantId, bool async, CancellationToken cancellationToken) { @@ -1112,7 +1200,8 @@ private async Task> GetUserDelegationKeyInternal( KeyInfo keyInfo = new KeyInfo(expiresOn.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture)) { - Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture) + Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture), + DelegatedUserTid = delegatedUserTenantId }; ResponseWithHeaders response; diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueUriBuilder.cs b/sdk/storage/Azure.Storage.Queues/src/QueueUriBuilder.cs index 34c8c341e955..5821549318f2 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueUriBuilder.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueUriBuilder.cs @@ -211,7 +211,7 @@ public QueueUriBuilder(Uri uri) var paramsMap = new UriQueryParamsCollection(uri.Query); if (paramsMap.ContainsKey(Constants.Sas.Parameters.Version)) { - Sas = SasQueryParametersInternals.Create(paramsMap); + Sas = new QueueSasQueryParameters(paramsMap); } Query = paramsMap.ToString(); } diff --git a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs index 0f23b86924eb..46a7cbe004e9 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Net.Mime; using System.Text; +using System.Threading; using Azure.Core; using Azure.Storage.Queues; using Azure.Storage.Queues.Models; @@ -290,7 +291,7 @@ public SasQueryParameters ToSasQueryParameters(StorageSharedKeyCredential shared /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -307,7 +308,7 @@ public QueueSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelega /// /// /// A returned from - /// . + /// . /// /// The name of the storage account. /// @@ -345,6 +346,7 @@ public QueueSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelega keyService: userDelegationKey.SignedService, keyVersion: userDelegationKey.SignedVersion, delegatedUserObjectId: DelegatedUserObjectId, + keyDelegatedUserTenantId: userDelegationKey.SignedDelegatedUserTenantId, signature: signature); return p; } @@ -368,7 +370,7 @@ private string ToStringToSign(UserDelegationKey userDelegationKey, string accoun signedExpiry, userDelegationKey.SignedService, userDelegationKey.SignedVersion, - null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + userDelegationKey.SignedDelegatedUserTenantId, DelegatedUserObjectId, IPRange.ToString(), SasExtensions.ToProtocolString(Protocol), diff --git a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs index 44da5ec9d9a7..fd5343f9d2f6 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs @@ -51,6 +51,11 @@ public sealed class QueueSasQueryParameters : SasQueryParameters /// public string KeyVersion => KeyProperties?.Version; + /// + /// Gets the delegated user tenant id. + /// + public string KeyDelegatedUserTenantId => KeyProperties?.DelegatedUserTenantId; + /// /// Gets empty shared access signature query parameters. /// @@ -82,7 +87,8 @@ internal QueueSasQueryParameters( DateTimeOffset keyExpiry = default, string keyService = default, string keyVersion = default, - string delegatedUserObjectId = default) + string delegatedUserObjectId = default, + string keyDelegatedUserTenantId = default) : base( version, services, @@ -114,7 +120,8 @@ internal QueueSasQueryParameters( StartsOn = keyStart, ExpiresOn = keyExpiry, Service = keyService, - Version = keyVersion + Version = keyVersion, + DelegatedUserTenantId = keyDelegatedUserTenantId }; } diff --git a/sdk/storage/Azure.Storage.Queues/src/autorest.md b/sdk/storage/Azure.Storage.Queues/src/autorest.md index 71844d7a0e77..bfc0c82e259b 100644 --- a/sdk/storage/Azure.Storage.Queues/src/autorest.md +++ b/sdk/storage/Azure.Storage.Queues/src/autorest.md @@ -4,7 +4,7 @@ Run `dotnet build /t:GenerateCode` to generate code. ``` yaml input-file: - - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/b6472ffd34d5d4a155101b41b4eb1f356abff600/specification/storage/data-plane/Microsoft.QueueStorage/stable/2026-02-06/queue.json + - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/42b84487c693d3f30939b81ded12b26e931a619b/specification/storage/data-plane/Microsoft.QueueStorage/stable/2026-04-06/queue.json generation1-convenience-client: true # https://github.com/Azure/autorest/issues/4075 skip-semantics-validation: true diff --git a/sdk/storage/Azure.Storage.Queues/tests/Azure.Storage.Queues.Tests.csproj b/sdk/storage/Azure.Storage.Queues/tests/Azure.Storage.Queues.Tests.csproj index 019c6084d49c..046372d0d46d 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/Azure.Storage.Queues.Tests.csproj +++ b/sdk/storage/Azure.Storage.Queues/tests/Azure.Storage.Queues.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs index bbb8e93f0e3d..48df24a7cd16 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs @@ -41,6 +41,7 @@ public QueueClientTestFixtureAttribute(params object[] additionalParameters) QueueClientOptions.ServiceVersion.V2025_07_05, QueueClientOptions.ServiceVersion.V2025_11_05, QueueClientOptions.ServiceVersion.V2026_02_06, + QueueClientOptions.ServiceVersion.V2026_04_06, StorageVersionExtensions.LatestVersion, StorageVersionExtensions.MaxVersion }, diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs index 0560d1620e88..491f4cf2a8f5 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs @@ -52,6 +52,7 @@ public void QueueSasBuilder_ToSasQueryParameters_IdentitySas() Assert.AreEqual(constants.Sas.KeyExpiry, sasQueryParameters.KeyExpiresOn); Assert.AreEqual(constants.Sas.KeyService, sasQueryParameters.KeyService); Assert.AreEqual(constants.Sas.KeyVersion, sasQueryParameters.KeyVersion); + Assert.AreEqual(constants.Sas.KeyDelegatedTenantId, sasQueryParameters.KeyDelegatedUserTenantId); Assert.AreEqual(Constants.Sas.Resource.Queue, sasQueryParameters.Resource); Assert.AreEqual(Permissions, sasQueryParameters.Permissions); Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); @@ -358,7 +359,7 @@ private string BuildUserDelegationSignature(TestConstants constants, string queu SasExtensions.FormatTimesForSasSigning(constants.Sas.KeyExpiry), constants.Sas.KeyService, constants.Sas.KeyVersion, - null, + constants.Sas.KeyDelegatedTenantId, constants.Sas.DelegatedObjectId, constants.Sas.IPRange.ToString(), SasExtensions.ToProtocolString(SasProtocol.Https), @@ -382,6 +383,7 @@ private static UserDelegationKey GetUserDelegationKey(TestConstants constants) SignedExpiresOn = constants.Sas.KeyExpiry, SignedService = constants.Sas.KeyService, SignedVersion = constants.Sas.KeyVersion, + SignedDelegatedUserTenantId = constants.Sas.KeyDelegatedTenantId, Value = constants.Sas.KeyValue }; } diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs index 2a1528c5cd28..90d343a40acf 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs @@ -132,9 +132,9 @@ public async Task SendMessageAsync_UserDelegationSAS() QueueServiceClient service = GetServiceClient_OAuth(); await using DisposingQueue test = await GetTestQueueAsync(service); + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; @@ -161,9 +161,9 @@ public async Task SendMessageAsync_UserDelegationSAS_Builder() QueueServiceClient service = GetServiceClient_OAuth(); await using DisposingQueue test = await GetTestQueueAsync(service); + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; @@ -186,6 +186,161 @@ public async Task SendMessageAsync_UserDelegationSAS_Builder() Assert.NotNull(response.Value); } + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_04_06)] + public async Task QueueClient_UserDelegationSAS_DelegatedTenantId() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + QueueSasBuilder queueSasBuilder = new QueueSasBuilder(QueueSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + QueueName = test.Queue.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName, out string stringToSign); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(test.Queue.Uri) + { + Sas = sasQueryParameters + }; + + QueueClient identityQueueClient = InstrumentClient(new QueueClient(uriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act + Response response = await identityQueueClient.GetPropertiesAsync(); + + // Assert + Assert.IsNotNull(response.GetRawResponse().Headers.RequestId); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_04_06)] + public async Task QueueClient_UserDelegationSAS_DelegatedTenantId_Failed() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + QueueSasBuilder queueSasBuilder = new QueueSasBuilder(QueueSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + QueueName = test.Queue.Name, + // We are deliberately not passing in DelegatedUserObjectId to cause an auth failure + }; + + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName, out string stringToSign); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(test.Queue.Uri) + { + Sas = sasQueryParameters + }; + + QueueClient identityQueueClient = InstrumentClient(new QueueClient(uriBuilder.ToUri(), TestEnvironment.Credential, GetOptions())); + + // Act & Assert + await TestHelper.AssertExpectedExceptionAsync( + identityQueueClient.GetPropertiesAsync(), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [LiveOnly] + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_04_06)] + public async Task QueueClient_UserDelegationSAS_DelegatedTenantId_Roundtrip() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + // We need to get the tenant ID from the token credential used to authenticate the request + TokenCredential tokenCredential = TestEnvironment.Credential; + AccessToken accessToken = await tokenCredential.GetTokenAsync( + new TokenRequestContext(Scopes), + CancellationToken.None); + + JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken.Token); + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.TenantId, out object tenantId); + + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)) + { + DelegatedUserTenantId = tenantId?.ToString() + }; + + Response userDelegationKey = await service.GetUserDelegationKeyAsync( + options: options); + + Assert.IsNotNull(userDelegationKey.Value); + Assert.AreEqual(options.DelegatedUserTenantId, userDelegationKey.Value.SignedDelegatedUserTenantId); + + jwtSecurityToken.Payload.TryGetValue(Constants.Sas.ObjectId, out object objectId); + + QueueSasBuilder queueSasBuilder = new QueueSasBuilder(QueueSasPermissions.Read, Recording.UtcNow.AddHours(1)) + { + QueueName = test.Queue.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(userDelegationKey.Value, service.AccountName, out string stringToSign); + + QueueUriBuilder originalUriBuilder = new QueueUriBuilder(test.Queue.Uri) + { + Sas = sasQueryParameters + }; + + QueueUriBuilder roundtripUriBuilder = new QueueUriBuilder(originalUriBuilder.ToUri()); + + Assert.AreEqual(originalUriBuilder.ToUri(), roundtripUriBuilder.ToUri()); + Assert.AreEqual(originalUriBuilder.Sas.ToString(), roundtripUriBuilder.Sas.ToString()); + } + [RecordedTest] [LiveOnly] // Cannot record Entra ID token [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] @@ -195,9 +350,9 @@ public async Task SendMessageAsync_UserDelegationSAS_DelegatedObjectId() QueueServiceClient service = GetServiceClient_OAuth(); await using DisposingQueue test = await GetTestQueueAsync(service); + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; @@ -244,9 +399,9 @@ public async Task SendMessageAsync_UserDelegationSAS_DelegatedObjectId_Fail() QueueServiceClient service = GetServiceClient_OAuth(); await using DisposingQueue test = await GetTestQueueAsync(service); + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( - startsOn: null, - expiresOn: Recording.UtcNow.AddHours(1)); + options: options); // We need to get the object ID from the token credential used to authenticate the request TokenCredential tokenCredential = TestEnvironment.Credential; diff --git a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs index bedab12900e4..f3039e5e01cc 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs @@ -308,7 +308,9 @@ public async Task GetUserDelegatioKey() QueueServiceClient service = GetServiceClient_OAuth(); // Act - Response response = await service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)); + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); + Response response = await service.GetUserDelegationKeyAsync( + options: options); // Assert Assert.IsNotNull(response.Value); @@ -322,8 +324,9 @@ public async Task GetUserDelegationKey_Error() QueueServiceClient service = GetServiceClient_SharedKey(); // Act + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions(expiresOn: Recording.UtcNow.AddHours(1)); await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)), + service.GetUserDelegationKeyAsync(options: options), e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); } @@ -335,14 +338,15 @@ public async Task GetUserDelegationKey_ArgumentException() QueueServiceClient service = GetServiceClient_OAuth(); // Act - await TestHelper.AssertExpectedExceptionAsync( - service.GetUserDelegationKeyAsync( - startsOn: null, + QueueGetUserDelegationKeyOptions options = new QueueGetUserDelegationKeyOptions( // ensure the time used is not UTC, as DateTimeOffset.Now could actually be UTC based on OS settings // Use a custom time zone so we aren't dependent on OS having specific standard time zone. expiresOn: TimeZoneInfo.ConvertTime( Recording.Now.AddHours(1), - TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))), + TimeZoneInfo.CreateCustomTimeZone("Storage Test Custom Time Zone", TimeSpan.FromHours(-3), "CTZ", "CTZ"))); + await TestHelper.AssertExpectedExceptionAsync( + service.GetUserDelegationKeyAsync( + options: options), e => Assert.AreEqual("expiresOn must be UTC", e.Message)); ; }