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 a97864d06566..bef69e9d56b4 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj +++ b/sdk/storage/Azure.Storage.Blobs/src/Azure.Storage.Blobs.csproj @@ -106,5 +106,6 @@ + diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs index 8eb5c079a4b5..775051b352a0 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs @@ -1611,12 +1611,12 @@ private async Task> GetUserDelegationKeyInternal( if (startsOn.HasValue && startsOn.Value.Offset != TimeSpan.Zero) { - throw BlobErrors.InvalidDateTimeUtc(nameof(startsOn)); + throw Errors.InvalidDateTimeUtc(nameof(startsOn)); } if (expiresOn.Offset != TimeSpan.Zero) { - throw BlobErrors.InvalidDateTimeUtc(nameof(expiresOn)); + throw Errors.InvalidDateTimeUtc(nameof(expiresOn)); } KeyInfo keyInfo = new KeyInfo(expiresOn.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture)) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs index 10445deb8b22..9c36d485a7ed 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Sas/BlobSasBuilder.cs @@ -463,7 +463,7 @@ public BlobSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelegat stringToSign = ToStringToSign(userDelegationKey, accountName); - string signature = ComputeHMACSHA256(userDelegationKey.Value, stringToSign); + string signature = SasExtensions.ComputeHMACSHA256(userDelegationKey.Value, stringToSign); BlobSasQueryParameters p = new BlobSasQueryParameters( version: Version, @@ -546,22 +546,6 @@ private static string GetCanonicalName(string account, string containerName, str ? $"/blob/{account}/{containerName}/{blobName.Replace("\\", "/")}" : $"/blob/{account}/{containerName}"; - /// - /// ComputeHMACSHA256 generates a base-64 hash signature string for an - /// HTTP request or for a SAS. - /// - /// - /// A used to sign with a key - /// representing AD credentials. - /// - /// The message to sign. - /// The signed message. - private static string ComputeHMACSHA256(string userDelegationKeyValue, string message) => - Convert.ToBase64String( - new HMACSHA256( - Convert.FromBase64String(userDelegationKeyValue)) - .ComputeHash(Encoding.UTF8.GetBytes(message))); - /// /// Ensure the 's properties are in a /// consistent state. diff --git a/sdk/storage/Azure.Storage.Blobs/src/Shared/BlobErrors.cs b/sdk/storage/Azure.Storage.Blobs/src/Shared/BlobErrors.cs index 873958b211bd..788602804d52 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Shared/BlobErrors.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Shared/BlobErrors.cs @@ -20,9 +20,6 @@ public static InvalidOperationException BlobOrContainerMissing(string leaseClien string blobContainerClient) => new InvalidOperationException($"{leaseClient} requires either a {blobBaseClient} or {blobContainerClient}"); - public static ArgumentException InvalidDateTimeUtc(string dateTime) => - new ArgumentException($"{dateTime} must be UTC"); - internal static void VerifyHttpsCustomerProvidedKey(Uri uri, CustomerProvidedKey? customerProvidedKey) { if (customerProvidedKey.HasValue && !string.Equals(uri.Scheme, Constants.Https, StringComparison.OrdinalIgnoreCase)) diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs index 57c15c5fbd8c..4360d5164585 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs @@ -25,6 +25,7 @@ internal static class Constants /// Gets the default service version to use when building shared access /// signatures. /// + // TODO fix this public const string DefaultSasVersion = "2026-02-06"; /// @@ -435,6 +436,8 @@ internal static class Queue public const string UriSubDomain = "queue"; public const string QueueTraitsMetadata = "metadata"; + + public const string Name = "Queue"; } /// @@ -631,6 +634,7 @@ internal static class Resource public const string File = "f"; public const string Share = "s"; public const string Directory = "d"; + public const string Queue = "q"; } internal static class AccountServices 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 a3e4330d9a58..d112219c9a5f 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Errors.Clients.cs @@ -35,6 +35,9 @@ public static ArgumentOutOfRangeException MustBeGreaterThanValueOrEqualToOtherVa long value1) => new ArgumentOutOfRangeException(paramName, $"Value must be greater than {value0} or equal to {value1}"); + public static ArgumentException InvalidDateTimeUtc(string dateTime) => + new ArgumentException($"{dateTime} must be UTC"); + public static ArgumentException StreamMustBeReadable(string paramName) => new ArgumentException("Stream must be readable", paramName); diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs index 417cc5ffb11c..4f883ec03970 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/SasExtensions.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Net; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Text; namespace Azure.Storage.Sas @@ -238,5 +239,21 @@ internal static string ValidateAndSanitizeRawPermissions(string permissions, return stringBuilder.ToString(); } + + /// + /// ComputeHMACSHA256 generates a base-64 hash signature string for an + /// HTTP request or for a SAS. + /// + /// + /// A UserDelegationKey.Value used to sign with a key + /// representing AD credentials. + /// + /// The message to sign. + /// The signed message. + internal static string ComputeHMACSHA256(string userDelegationKeyValue, string message) => + Convert.ToBase64String( + new HMACSHA256( + Convert.FromBase64String(userDelegationKeyValue)) + .ComputeHash(Encoding.UTF8.GetBytes(message))); } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Sas/SasQueryParametersExtensions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs similarity index 98% rename from sdk/storage/Azure.Storage.Blobs/src/Sas/SasQueryParametersExtensions.cs rename to sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs index abbbcd1260b1..0321d5c1ec6f 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Sas/SasQueryParametersExtensions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/SasQueryParametersExtensions.cs @@ -26,6 +26,8 @@ internal static void ParseKeyProperties( BlobSasQueryParameters #elif DataLakeSDK DataLakeSasQueryParameters +#elif QueueSDK + QueueSasQueryParameters #endif parameters, IDictionary values) 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 dad53a252550..b0e4071e98af 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 @@ -36,7 +36,7 @@ - + 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 434ab6611cf1..7f820302788c 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 @@ -42,6 +42,12 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } public virtual Azure.Response> GetAccessPolicy(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task>> GetAccessPolicyAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal virtual Azure.Storage.Queues.QueueServiceClient GetParentQueueServiceClientCore() { throw null; } @@ -161,6 +167,8 @@ 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 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; } } @@ -425,6 +433,17 @@ internal UpdateReceipt() { } public System.DateTimeOffset NextVisibleOn { get { throw null; } } public string PopReceipt { get { throw null; } } } + public partial class UserDelegationKey + { + internal UserDelegationKey() { } + public System.DateTimeOffset SignedExpiresOn { get { throw null; } } + public string SignedObjectId { get { throw null; } } + public string SignedService { get { throw null; } } + public System.DateTimeOffset SignedStartsOn { get { throw null; } } + public string SignedTenantId { get { throw null; } } + public string SignedVersion { get { throw null; } } + public string Value { get { throw null; } } + } } namespace Azure.Storage.Queues.Specialized { @@ -469,6 +488,7 @@ public partial class QueueSasBuilder public QueueSasBuilder() { } public QueueSasBuilder(Azure.Storage.Sas.QueueAccountSasPermissions permissions, System.DateTimeOffset expiresOn) { } public QueueSasBuilder(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { } + public string DelegatedUserObjectId { get { throw null; } set { } } public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } public string Identifier { get { throw null; } set { } } public Azure.Storage.Sas.SasIPRange IPRange { get { throw null; } set { } } @@ -486,6 +506,8 @@ public void SetPermissions(Azure.Storage.Sas.QueueAccountSasPermissions permissi public void SetPermissions(Azure.Storage.Sas.QueueSasPermissions permissions) { } public void SetPermissions(string rawPermissions) { } public void SetPermissions(string rawPermissions, bool normalize = false) { } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName) { throw null; } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName, out string stringToSign) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential, out string stringToSign) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -500,6 +522,18 @@ public enum QueueSasPermissions Update = 4, Process = 8, } + public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQueryParameters + { + internal QueueSasQueryParameters() { } + public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public System.DateTimeOffset KeyExpiresOn { get { throw null; } } + public string KeyObjectId { get { throw null; } } + public string KeyService { get { throw null; } } + public System.DateTimeOffset KeyStartsOn { get { throw null; } } + public string KeyTenantId { get { throw null; } } + public string KeyVersion { get { throw null; } } + public override string ToString() { throw null; } + } } namespace Microsoft.Extensions.Azure { 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 434ab6611cf1..7f820302788c 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 @@ -42,6 +42,12 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } public virtual Azure.Response> GetAccessPolicy(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task>> GetAccessPolicyAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal virtual Azure.Storage.Queues.QueueServiceClient GetParentQueueServiceClientCore() { throw null; } @@ -161,6 +167,8 @@ 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 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; } } @@ -425,6 +433,17 @@ internal UpdateReceipt() { } public System.DateTimeOffset NextVisibleOn { get { throw null; } } public string PopReceipt { get { throw null; } } } + public partial class UserDelegationKey + { + internal UserDelegationKey() { } + public System.DateTimeOffset SignedExpiresOn { get { throw null; } } + public string SignedObjectId { get { throw null; } } + public string SignedService { get { throw null; } } + public System.DateTimeOffset SignedStartsOn { get { throw null; } } + public string SignedTenantId { get { throw null; } } + public string SignedVersion { get { throw null; } } + public string Value { get { throw null; } } + } } namespace Azure.Storage.Queues.Specialized { @@ -469,6 +488,7 @@ public partial class QueueSasBuilder public QueueSasBuilder() { } public QueueSasBuilder(Azure.Storage.Sas.QueueAccountSasPermissions permissions, System.DateTimeOffset expiresOn) { } public QueueSasBuilder(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { } + public string DelegatedUserObjectId { get { throw null; } set { } } public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } public string Identifier { get { throw null; } set { } } public Azure.Storage.Sas.SasIPRange IPRange { get { throw null; } set { } } @@ -486,6 +506,8 @@ public void SetPermissions(Azure.Storage.Sas.QueueAccountSasPermissions permissi public void SetPermissions(Azure.Storage.Sas.QueueSasPermissions permissions) { } public void SetPermissions(string rawPermissions) { } public void SetPermissions(string rawPermissions, bool normalize = false) { } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName) { throw null; } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName, out string stringToSign) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential, out string stringToSign) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -500,6 +522,18 @@ public enum QueueSasPermissions Update = 4, Process = 8, } + public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQueryParameters + { + internal QueueSasQueryParameters() { } + public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public System.DateTimeOffset KeyExpiresOn { get { throw null; } } + public string KeyObjectId { get { throw null; } } + public string KeyService { get { throw null; } } + public System.DateTimeOffset KeyStartsOn { get { throw null; } } + public string KeyTenantId { get { throw null; } } + public string KeyVersion { get { throw null; } } + public override string ToString() { throw null; } + } } namespace Microsoft.Extensions.Azure { 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 434ab6611cf1..7f820302788c 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 @@ -42,6 +42,12 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Uri GenerateSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasBuilder builder, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GenerateUserDelegationSasUri(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn, Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, out string stringToSign) { throw null; } public virtual Azure.Response> GetAccessPolicy(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task>> GetAccessPolicyAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal virtual Azure.Storage.Queues.QueueServiceClient GetParentQueueServiceClientCore() { throw null; } @@ -161,6 +167,8 @@ 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 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; } } @@ -425,6 +433,17 @@ internal UpdateReceipt() { } public System.DateTimeOffset NextVisibleOn { get { throw null; } } public string PopReceipt { get { throw null; } } } + public partial class UserDelegationKey + { + internal UserDelegationKey() { } + public System.DateTimeOffset SignedExpiresOn { get { throw null; } } + public string SignedObjectId { get { throw null; } } + public string SignedService { get { throw null; } } + public System.DateTimeOffset SignedStartsOn { get { throw null; } } + public string SignedTenantId { get { throw null; } } + public string SignedVersion { get { throw null; } } + public string Value { get { throw null; } } + } } namespace Azure.Storage.Queues.Specialized { @@ -469,6 +488,7 @@ public partial class QueueSasBuilder public QueueSasBuilder() { } public QueueSasBuilder(Azure.Storage.Sas.QueueAccountSasPermissions permissions, System.DateTimeOffset expiresOn) { } public QueueSasBuilder(Azure.Storage.Sas.QueueSasPermissions permissions, System.DateTimeOffset expiresOn) { } + public string DelegatedUserObjectId { get { throw null; } set { } } public System.DateTimeOffset ExpiresOn { get { throw null; } set { } } public string Identifier { get { throw null; } set { } } public Azure.Storage.Sas.SasIPRange IPRange { get { throw null; } set { } } @@ -486,6 +506,8 @@ public void SetPermissions(Azure.Storage.Sas.QueueAccountSasPermissions permissi public void SetPermissions(Azure.Storage.Sas.QueueSasPermissions permissions) { } public void SetPermissions(string rawPermissions) { } public void SetPermissions(string rawPermissions, bool normalize = false) { } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName) { throw null; } + public Azure.Storage.Sas.QueueSasQueryParameters ToSasQueryParameters(Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey, string accountName, out string stringToSign) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential) { throw null; } public Azure.Storage.Sas.SasQueryParameters ToSasQueryParameters(Azure.Storage.StorageSharedKeyCredential sharedKeyCredential, out string stringToSign) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -500,6 +522,18 @@ public enum QueueSasPermissions Update = 4, Process = 8, } + public sealed partial class QueueSasQueryParameters : Azure.Storage.Sas.SasQueryParameters + { + internal QueueSasQueryParameters() { } + public static new Azure.Storage.Sas.QueueSasQueryParameters Empty { get { throw null; } } + public System.DateTimeOffset KeyExpiresOn { get { throw null; } } + public string KeyObjectId { get { throw null; } } + public string KeyService { get { throw null; } } + public System.DateTimeOffset KeyStartsOn { get { throw null; } } + public string KeyTenantId { get { throw null; } } + public string KeyVersion { get { throw null; } } + public override string ToString() { throw null; } + } } namespace Microsoft.Extensions.Azure { diff --git a/sdk/storage/Azure.Storage.Queues/assets.json b/sdk/storage/Azure.Storage.Queues/assets.json index 83952061b930..2c2b66269895 100644 --- a/sdk/storage/Azure.Storage.Queues/assets.json +++ b/sdk/storage/Azure.Storage.Queues/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Queues", - "Tag": "net/storage/Azure.Storage.Queues_975e82f8f8" + "Tag": "net/storage/Azure.Storage.Queues_1b869deccf" } 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 b5e934bc8b90..6851dc2eda87 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj +++ b/sdk/storage/Azure.Storage.Queues/src/Azure.Storage.Queues.csproj @@ -73,5 +73,7 @@ + + diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/MessageIdRestClient.cs index 905db9baf0d3..2be78151d054 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 "2018-03-28". + /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". /// , , 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 e25d809efc91..880709e94889 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 "2018-03-28". + /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". /// , , 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 new file mode 100644 index 000000000000..20b89e3c39e6 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.Serialization.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System.Xml; +using Azure.Core; +using Azure.Storage.Common; + +namespace Azure.Storage.Queues.Models +{ + internal partial class KeyInfo : IXmlSerializable + { + void IXmlSerializable.Write(XmlWriter writer, string nameHint) + { + writer.WriteStartElement(nameHint ?? "KeyInfo"); + if (Common.Optional.IsDefined(Start)) + { + writer.WriteStartElement("Start"); + writer.WriteValue(Start); + writer.WriteEndElement(); + } + 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 new file mode 100644 index 000000000000..1637826f094c --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/KeyInfo.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using Azure.Storage.Common; + +namespace Azure.Storage.Queues.Models +{ + /// Key information. + internal partial class KeyInfo + { + /// Initializes a new instance of . + /// The date-time the key expires in ISO 8601 UTC time. + /// is null. + public KeyInfo(string expiry) + { + Argument.AssertNotNull(expiry, nameof(expiry)); + + Expiry = 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. + /// Optional. String, The delegated user tenant ID in Entra ID. + 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; } + /// Optional. String, The delegated user tenant ID in Entra ID. + 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 new file mode 100644 index 000000000000..07b6f6633730 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.Serialization.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using System.Xml.Linq; +using Azure.Core; + +namespace Azure.Storage.Queues.Models +{ + public partial class UserDelegationKey + { + internal static UserDelegationKey DeserializeUserDelegationKey(XElement element) + { + string signedObjectId = default; + string signedTenantId = default; + DateTimeOffset signedStartsOn = default; + DateTimeOffset signedExpiresOn = default; + string signedService = default; + string signedVersion = default; + string value = default; + if (element.Element("SignedOid") is XElement signedOidElement) + { + signedObjectId = (string)signedOidElement; + } + if (element.Element("SignedTid") is XElement signedTidElement) + { + signedTenantId = (string)signedTidElement; + } + if (element.Element("SignedStart") is XElement signedStartElement) + { + signedStartsOn = signedStartElement.GetDateTimeOffsetValue("O"); + } + if (element.Element("SignedExpiry") is XElement signedExpiryElement) + { + signedExpiresOn = signedExpiryElement.GetDateTimeOffsetValue("O"); + } + if (element.Element("SignedService") is XElement signedServiceElement) + { + signedService = (string)signedServiceElement; + } + if (element.Element("SignedVersion") is XElement signedVersionElement) + { + signedVersion = (string)signedVersionElement; + } + if (element.Element("Value") is XElement valueElement) + { + value = (string)valueElement; + } + return new UserDelegationKey( + signedObjectId, + signedTenantId, + signedStartsOn, + signedExpiresOn, + signedService, + signedVersion, + 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 new file mode 100644 index 000000000000..dc341a1f1483 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/Models/UserDelegationKey.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using Azure.Storage.Common; + +namespace Azure.Storage.Queues.Models +{ + /// A user delegation key. + public partial class UserDelegationKey + { + /// 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. + internal UserDelegationKey(string signedObjectId, string signedTenantId, DateTimeOffset signedStartsOn, DateTimeOffset signedExpiresOn, string signedService, string signedVersion, string value) + { + Argument.AssertNotNull(signedObjectId, nameof(signedObjectId)); + Argument.AssertNotNull(signedTenantId, nameof(signedTenantId)); + Argument.AssertNotNull(signedService, nameof(signedService)); + Argument.AssertNotNull(signedVersion, nameof(signedVersion)); + Argument.AssertNotNull(value, nameof(value)); + + SignedObjectId = signedObjectId; + SignedTenantId = signedTenantId; + SignedStartsOn = signedStartsOn; + SignedExpiresOn = signedExpiresOn; + SignedService = signedService; + SignedVersion = signedVersion; + 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 9fc3133267dd..74f77dafeb91 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 "2018-03-28". + /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". /// , , or is null. public QueueRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceGetUserDelegationKeyHeaders.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceGetUserDelegationKeyHeaders.cs new file mode 100644 index 000000000000..c18a4a5fe94d --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceGetUserDelegationKeyHeaders.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Azure.Core; + +namespace Azure.Storage.Queues +{ + internal partial class ServiceGetUserDelegationKeyHeaders + { + private readonly Response _response; + public ServiceGetUserDelegationKeyHeaders(Response response) + { + _response = response; + } + /// Indicates the version of the Blob service used to execute the request. This header is returned for requests made against version 2009-09-19 and above. + public string Version => _response.Headers.TryGetValue("x-ms-version", out string value) ? value : null; + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs b/sdk/storage/Azure.Storage.Queues/src/Generated/ServiceRestClient.cs index 773878c5be81..4002fe504c2b 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 "2018-03-28". + /// Specifies the version of the operation to use for this request. The default value is "2026-02-06". /// , , or is null. public ServiceRestClient(ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, string url, string version) { @@ -252,6 +252,94 @@ public ResponseWithHeaders } } + internal HttpMessage CreateGetUserDelegationKeyRequest(KeyInfo keyInfo, int? timeout) + { + var message = _pipeline.CreateMessage(); + var request = message.Request; + request.Method = RequestMethod.Post; + var uri = new RawRequestUriBuilder(); + uri.AppendRaw(_url, false); + uri.AppendPath("/", false); + uri.AppendQuery("restype", "service", true); + uri.AppendQuery("comp", "userdelegationkey", true); + if (timeout != null) + { + uri.AppendQuery("timeout", timeout.Value, true); + } + request.Uri = uri; + request.Headers.Add("x-ms-version", _version); + request.Headers.Add("Accept", "application/xml"); + request.Headers.Add("Content-Type", "application/xml"); + var content = new XmlWriterContent(); + content.XmlWriter.WriteObjectValue(keyInfo, "KeyInfo"); + request.Content = content; + return message; + } + + /// Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token authentication. + /// Key information. + /// The The timeout parameter is expressed in seconds. For more information, see <a href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting Timeouts for Queue Service Operations.</a>. + /// The cancellation token to use. + /// is null. + public async Task> GetUserDelegationKeyAsync(KeyInfo keyInfo, int? timeout = null, CancellationToken cancellationToken = default) + { + if (keyInfo == null) + { + throw new ArgumentNullException(nameof(keyInfo)); + } + + using var message = CreateGetUserDelegationKeyRequest(keyInfo, timeout); + await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); + var headers = new ServiceGetUserDelegationKeyHeaders(message.Response); + switch (message.Response.Status) + { + case 200: + { + UserDelegationKey value = default; + var document = XDocument.Load(message.Response.ContentStream, LoadOptions.PreserveWhitespace); + if (document.Element("UserDelegationKey") is XElement userDelegationKeyElement) + { + value = UserDelegationKey.DeserializeUserDelegationKey(userDelegationKeyElement); + } + return ResponseWithHeaders.FromValue(value, headers, message.Response); + } + default: + throw new RequestFailedException(message.Response); + } + } + + /// Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token authentication. + /// Key information. + /// The The timeout parameter is expressed in seconds. For more information, see <a href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting Timeouts for Queue Service Operations.</a>. + /// The cancellation token to use. + /// is null. + public ResponseWithHeaders GetUserDelegationKey(KeyInfo keyInfo, int? timeout = null, CancellationToken cancellationToken = default) + { + if (keyInfo == null) + { + throw new ArgumentNullException(nameof(keyInfo)); + } + + using var message = CreateGetUserDelegationKeyRequest(keyInfo, timeout); + _pipeline.Send(message, cancellationToken); + var headers = new ServiceGetUserDelegationKeyHeaders(message.Response); + switch (message.Response.Status) + { + case 200: + { + UserDelegationKey value = default; + var document = XDocument.Load(message.Response.ContentStream, LoadOptions.PreserveWhitespace); + if (document.Element("UserDelegationKey") is XElement userDelegationKeyElement) + { + value = UserDelegationKey.DeserializeUserDelegationKey(userDelegationKeyElement); + } + return ResponseWithHeaders.FromValue(value, headers, message.Response); + } + default: + throw new RequestFailedException(message.Response); + } + } + internal HttpMessage CreateListQueuesSegmentRequest(string prefix, string marker, int? maxresults, IEnumerable include, int? timeout) { var message = _pipeline.CreateMessage(); diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/Internal/KeyInfo.cs b/sdk/storage/Azure.Storage.Queues/src/Models/Internal/KeyInfo.cs new file mode 100644 index 000000000000..1d2c0b5992cc --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Models/Internal/KeyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Storage.Queues.Models +{ + internal partial class KeyInfo + { + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs b/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs new file mode 100644 index 000000000000..56579552a153 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Models/UserDelegationKey.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core; + +namespace Azure.Storage.Queues.Models +{ + /// + /// A user delegation key. + /// + public partial class UserDelegationKey + { + /// + /// The Azure Active Directory object ID in GUID format. + /// + [CodeGenMember("SignedOid")] + public string SignedObjectId { get; internal set; } + + /// + /// The Azure Active Directory tenant ID in GUID format. + /// + [CodeGenMember("SignedTid")] + public string SignedTenantId { get; internal set; } + + /// + /// The date-time the key expires. + /// + [CodeGenMember("SignedExpiry")] + public DateTimeOffset SignedExpiresOn { get; internal set; } + + /// + /// The date-time the key is active. + /// + [CodeGenMember("SignedStart")] + public DateTimeOffset SignedStartsOn { get; internal set; } + + /// + /// Abbreviation of the Azure Storage service that accepts the key. + /// + public string SignedService { get; internal set; } + + /// + /// The service version that created the key. + /// + public string SignedVersion { get; internal set; } + + /// + /// The key as a base64 string. + /// + public string Value { get; internal set; } + + /// + /// Constructor. + /// + internal UserDelegationKey() { } + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs index 5ede94c21ad2..b65bf20d6a40 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs @@ -3267,6 +3267,156 @@ public virtual Uri GenerateSasUri( } #endregion + #region GenerateUserDelegationSas + /// + /// The + /// returns a representing a Queue Service + /// Shared Access Signature (SAS) Uri based on the Client properties + /// and parameters passed. The SAS is signed by the user delegation key + /// that is passed in. + /// + /// For more information, see + /// + /// Creating an user delegation SAS. + /// + /// + /// Required. Specifies the list of permissions to be associated with the SAS. + /// See . + /// + /// + /// Required. Specifies the time at which the SAS becomes invalid. This field + /// must be omitted if it has been specified in an associated stored access policy. + /// + /// + /// Required. A returned from + /// . + /// + /// + /// A containing the SAS Uri. + /// + /// + /// A will be thrown if a failure occurs. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual Uri GenerateUserDelegationSasUri(QueueSasPermissions permissions, DateTimeOffset expiresOn, UserDelegationKey userDelegationKey) => + GenerateUserDelegationSasUri(permissions, expiresOn, userDelegationKey, out _); + + /// + /// The + /// returns a representing a Blob Container Service + /// Shared Access Signature (SAS) Uri based on the Client properties + /// and parameters passed. The SAS is signed by the user delegation key + /// that is passed in. + /// + /// For more information, see + /// + /// Creating an user delegation SAS. + /// + /// + /// Required. Specifies the list of permissions to be associated with the SAS. + /// See . + /// + /// + /// Required. Specifies the time at which the SAS becomes invalid. This field + /// must be omitted if it has been specified in an associated stored access policy. + /// + /// + /// 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. + /// + /// + /// A containing the SAS Uri. + /// + /// + /// A will be thrown if a failure occurs. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual Uri GenerateUserDelegationSasUri(QueueSasPermissions permissions, DateTimeOffset expiresOn, UserDelegationKey userDelegationKey, out string stringToSign) => + GenerateUserDelegationSasUri(new QueueSasBuilder(permissions, expiresOn) { QueueName = Name }, userDelegationKey, out stringToSign); + + /// + /// The + /// returns a representing a Queue Service + /// Shared Access Signature (SAS) Uri based on the Client properties + /// and builder passed. The SAS is signed by the user delegation key + /// that is passed in. + /// + /// For more information, see + /// + /// Creating an user delegation SAS. + /// + /// + /// Required. Used to generate a Shared Access Signature (SAS). + /// + /// + /// Required. A returned from + /// . + /// + /// + /// A containing the SAS Uri. + /// + /// + /// A will be thrown if a failure occurs. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual Uri GenerateUserDelegationSasUri(QueueSasBuilder builder, UserDelegationKey userDelegationKey) => + GenerateUserDelegationSasUri(builder, userDelegationKey, out _); + + /// + /// The + /// returns a representing a Queue Client Service + /// Shared Access Signature (SAS) Uri based on the Client properties + /// and builder passed. The SAS is signed by the user delegation key + /// that is passed in. + /// + /// For more information, see + /// + /// Creating an user delegation SAS. + /// + /// + /// Required. Used to generate a Shared Access Signature (SAS). + /// + /// + /// 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. + /// + /// + /// A containing the SAS Uri. + /// + /// + /// A will be thrown if a failure occurs. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-queues")] + public virtual Uri GenerateUserDelegationSasUri(QueueSasBuilder builder, UserDelegationKey userDelegationKey, out string stringToSign) + { + builder = builder ?? throw Errors.ArgumentNull(nameof(builder)); + userDelegationKey = userDelegationKey ?? throw Errors.ArgumentNull(nameof(userDelegationKey)); + + // Deep copy of builder so we don't modify the user's origial BlobSasBuilder. + builder = QueueSasBuilder.DeepCopy(builder); + + SetBuilderAndValidate(builder); + if (string.IsNullOrEmpty(AccountName)) + { + throw Errors.SasClientMissingData(nameof(AccountName)); + } + + QueueUriBuilder sasUri = new QueueUriBuilder(Uri) + { + Sas = builder.ToSasQueryParameters(userDelegationKey, AccountName, out stringToSign) + }; + return sasUri.ToUri(); + } + #endregion + #region Encoding private static BinaryData ToBinaryData(string input) { @@ -3343,6 +3493,21 @@ protected internal virtual QueueServiceClient GetParentQueueServiceClientCore() return _parentQueueServiceClient; } #endregion + + private void SetBuilderAndValidate(QueueSasBuilder builder) + { + // Assign builder's ContainerName if it is null. + builder.QueueName ??= Name; + + // Validate that builder is properly set + if (!builder.QueueName.Equals(Name, StringComparison.InvariantCulture)) + { + throw Errors.SasNamesNotMatching( + nameof(builder.QueueName), + nameof(QueueSasBuilder), + nameof(Name)); + } + } } } diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs index b64f23a7fb98..3718413aecf7 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -964,6 +965,191 @@ await GetQueueClient(queueName) .ConfigureAwait(false); #endregion DeleteQueue + #region GetUserDelegationKey + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// 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). + /// + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + /// + /// 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( + DateTimeOffset? startsOn, + DateTimeOffset expiresOn, + CancellationToken cancellationToken = default) => + GetUserDelegationKeyInternal( + startsOn, + expiresOn, + false, // async + cancellationToken) + .EnsureCompleted(); + + /// + /// The operation retrieves a + /// key that can be used to delegate Active Directory authorization to + /// shared access signatures created with . + /// + /// + /// 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). + /// + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + /// + /// 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( + DateTimeOffset? startsOn, + DateTimeOffset expiresOn, + CancellationToken cancellationToken = default) => + await GetUserDelegationKeyInternal( + startsOn, + expiresOn, + 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 . + /// + /// + /// 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). + /// + /// + /// Expiration of the key's validity. The time should be specified + /// in UTC. + /// + /// + /// 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. + /// + private async Task> GetUserDelegationKeyInternal( + DateTimeOffset? startsOn, + DateTimeOffset expiresOn, + bool async, + CancellationToken cancellationToken) + { + using (ClientConfiguration.Pipeline.BeginLoggingScope(nameof(QueueServiceClient))) + { + ClientConfiguration.Pipeline.LogMethodEnter(nameof(QueueServiceClient), message: $"{nameof(Uri)}: {Uri}"); + + DiagnosticScope scope = ClientConfiguration.ClientDiagnostics.CreateScope($"{nameof(QueueServiceClient)}.{nameof(GetUserDelegationKey)}"); + + try + { + scope.Start(); + + if (startsOn.HasValue && startsOn.Value.Offset != TimeSpan.Zero) + { + throw Errors.InvalidDateTimeUtc(nameof(startsOn)); + } + + if (expiresOn.Offset != TimeSpan.Zero) + { + throw Errors.InvalidDateTimeUtc(nameof(expiresOn)); + } + + KeyInfo keyInfo = new KeyInfo(expiresOn.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture)) + { + Start = startsOn?.ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture) + }; + + ResponseWithHeaders response; + + if (async) + { + response = await ServiceRestClient.GetUserDelegationKeyAsync( + keyInfo: keyInfo, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + else + { + response = ServiceRestClient.GetUserDelegationKey( + keyInfo: keyInfo, + cancellationToken: cancellationToken); + } + + return Response.FromValue( + response.Value, + response.GetRawResponse()); + } + catch (Exception ex) + { + ClientConfiguration.Pipeline.LogException(ex); + scope.Failed(ex); + throw; + } + finally + { + ClientConfiguration.Pipeline.LogMethodExit(nameof(QueueServiceClient)); + scope.Dispose(); + } + } + } + #endregion GetUserDelegationKey + #region GenerateSas /// /// The diff --git a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs index 6706bb0d028f..0ea5cc419879 100644 --- a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs +++ b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasBuilder.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Net.Mime; using System.Text; using Azure.Core; using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using static Azure.Storage.Constants.Sas; namespace Azure.Storage.Sas { @@ -89,6 +92,13 @@ public class QueueSasBuilder /// public string QueueName { get; set; } + /// + /// Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user would is authorized to + /// use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been + /// issued to the user specified in this value. + /// + public string DelegatedUserObjectId { get; set; } + /// /// Initializes a new instance of the /// class. @@ -273,6 +283,98 @@ public SasQueryParameters ToSasQueryParameters(StorageSharedKeyCredential shared return p; } + /// + /// Use an account's to sign this + /// shared access signature values to produce the proper SAS query + /// parameters for authenticating requests. + /// + /// + /// A returned from + /// . + /// + /// The name of the storage account. + /// + /// The used for authenticating requests. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + public QueueSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelegationKey, string accountName) + => ToSasQueryParameters(userDelegationKey, accountName, out _); + + /// + /// Use an account's to sign this + /// shared access signature values to produce the proper SAS query + /// parameters for authenticating requests. + /// + /// + /// A returned from + /// . + /// + /// The name of the storage account. + /// + /// + /// For debugging purposes only. This string will be overwritten with the string to sign that was used to generate the . + /// + /// The used for authenticating requests. + /// + [CallerShouldAudit("https://aka.ms/azsdk/callershouldaudit/storage-blobs")] + public QueueSasQueryParameters ToSasQueryParameters(UserDelegationKey userDelegationKey, string accountName, out string stringToSign) + { + userDelegationKey = userDelegationKey ?? throw Errors.ArgumentNull(nameof(userDelegationKey)); + + EnsureState(); + + stringToSign = ToStringToSign(userDelegationKey, accountName); + + string signature = SasExtensions.ComputeHMACSHA256(userDelegationKey.Value, stringToSign); + + QueueSasQueryParameters p = new QueueSasQueryParameters( + version: Version, + services: default, + resourceTypes: default, + protocol: Protocol, + startsOn: StartsOn, + expiresOn: ExpiresOn, + ipRange: IPRange, + identifier: null, + resource: Resource.Queue, + permissions: Permissions, + keyOid: userDelegationKey.SignedObjectId, + keyTid: userDelegationKey.SignedTenantId, + keyStart: userDelegationKey.SignedStartsOn, + keyExpiry: userDelegationKey.SignedExpiresOn, + keyService: userDelegationKey.SignedService, + keyVersion: userDelegationKey.SignedVersion, + delegatedUserObjectId: DelegatedUserObjectId, + signature: signature); + return p; + } + + private string ToStringToSign(UserDelegationKey userDelegationKey, string accountName) + { + string startTime = SasExtensions.FormatTimesForSasSigning(StartsOn); + string expiryTime = SasExtensions.FormatTimesForSasSigning(ExpiresOn); + string signedStart = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedStartsOn); + string signedExpiry = SasExtensions.FormatTimesForSasSigning(userDelegationKey.SignedExpiresOn); + + // See http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx + return string.Join("\n", + Permissions, + startTime, + expiryTime, + GetCanonicalName(accountName, QueueName ?? string.Empty), + userDelegationKey.SignedObjectId, + userDelegationKey.SignedTenantId, + signedStart, + signedExpiry, + userDelegationKey.SignedService, + userDelegationKey.SignedVersion, + null, // SignedKeyDelegatedUserTenantId, will be added in a future release. + DelegatedUserObjectId, + IPRange.ToString(), + SasExtensions.ToProtocolString(Protocol), + Version); + } + /// /// For debugging purposes only. /// Returns the string to sign that will be used to generate the signature for the SAS URL. diff --git a/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs new file mode 100644 index 000000000000..44da5ec9d9a7 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Sas/QueueSasQueryParameters.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Storage.Sas +{ + /// + /// A object represents the components + /// that make up an Azure Storage Shared Access Signature's query + /// parameters. You can construct a new instance using + /// . + /// + /// For more information, + /// + /// Create a service SAS. + /// + public sealed class QueueSasQueryParameters : SasQueryParameters + { + internal UserDelegationKeyProperties KeyProperties { get; set; } + + /// + /// Gets the Azure Active Directory object ID in GUID format. + /// + public string KeyObjectId => KeyProperties?.ObjectId; + + /// + /// Gets the Azure Active Directory tenant ID in GUID format + /// + public string KeyTenantId => KeyProperties?.TenantId; + + /// + /// Gets the time at which the key becomes valid. + /// + public DateTimeOffset KeyStartsOn => KeyProperties == null ? default : KeyProperties.StartsOn; + + /// + /// Gets the time at which the key becomes expires. + /// + public DateTimeOffset KeyExpiresOn => KeyProperties == null ? default : KeyProperties.ExpiresOn; + + /// + /// Gets the Storage service that accepts the key. + /// + public string KeyService => KeyProperties?.Service; + + /// + /// Gets the Storage service version that created the key. + /// + public string KeyVersion => KeyProperties?.Version; + + /// + /// Gets empty shared access signature query parameters. + /// + public static new QueueSasQueryParameters Empty => new QueueSasQueryParameters(); + + internal QueueSasQueryParameters() + : base() + { + } + + /// + /// Creates a new BlobSasQueryParameters instance. + /// + internal QueueSasQueryParameters( + string version, + AccountSasServices? services, + AccountSasResourceTypes? resourceTypes, + SasProtocol protocol, + DateTimeOffset startsOn, + DateTimeOffset expiresOn, + SasIPRange ipRange, + string identifier, + string resource, + string permissions, + string signature, + string keyOid = default, + string keyTid = default, + DateTimeOffset keyStart = default, + DateTimeOffset keyExpiry = default, + string keyService = default, + string keyVersion = default, + string delegatedUserObjectId = default) + : base( + version, + services, + resourceTypes, + protocol, + startsOn, + expiresOn, + ipRange, + identifier, + resource, + permissions, + signature, + cacheControl: null, + contentDisposition: null, + contentEncoding: null, + contentLanguage: null, + contentType: null, + authorizedAadObjectId: null, + unauthorizedAadObjectId: null, + correlationId: null, + directoryDepth: null, + encryptionScope: null, + delegatedUserObjectId) + { + KeyProperties = new UserDelegationKeyProperties + { + ObjectId = keyOid, + TenantId = keyTid, + StartsOn = keyStart, + ExpiresOn = keyExpiry, + Service = keyService, + Version = keyVersion + }; + } + + /// + /// Creates a new instance of the + /// type based on the supplied query parameters . + /// All SAS-related query parameters will be removed from + /// . + /// + /// URI query parameters + internal QueueSasQueryParameters( + IDictionary values) + : base(values) + { + this.ParseKeyProperties(values); + } + + /// + /// Convert the SAS query parameters into a URL encoded query string. + /// + /// + /// A URL encoded query string representing the SAS. + /// + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + KeyProperties.AppendProperties(sb); + AppendProperties(sb); + return sb.ToString(); + } + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/autorest.md b/sdk/storage/Azure.Storage.Queues/src/autorest.md index cf00b5b15086..d4424b8d23b9 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/596d8d2a8c1c50bd6ebe60036143f4c4787fc816/specification/storage/data-plane/Microsoft.QueueStorage/stable/2018-03-28/queue.json + - Q:\src\azure-rest-api-specs\specification\storage\data-plane\Microsoft.QueueStorage\stable\2026-02-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 e0a6fab3c753..019c6084d49c 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 @@ -17,6 +17,7 @@ + diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs index bbb8e93f0e3d..d98155ff8a3a 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTestFixtureAttribute.cs @@ -47,7 +47,7 @@ public QueueClientTestFixtureAttribute(params object[] additionalParameters) additionalParameters: additionalParameters) { RecordingServiceVersion = StorageVersionExtensions.MaxVersion; - LiveServiceVersions = new object[] { StorageVersionExtensions.LatestVersion, }; + LiveServiceVersions = new object[] { StorageVersionExtensions.MaxVersion }; } } } diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs index f5d5fb736aa6..0560d1620e88 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueSasBuilderTests.cs @@ -2,8 +2,11 @@ // Licensed under the MIT License. using System; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using Azure.Core.TestFramework; +using Azure.Storage.Queues.Models; using Azure.Storage.Queues.Tests; using Azure.Storage.Sas; using Azure.Storage.Test; @@ -21,6 +24,41 @@ public QueueSasBuilderTests(bool async, QueueClientOptions.ServiceVersion servic { } + [RecordedTest] + public void QueueSasBuilder_ToSasQueryParameters_IdentitySas() + { + // Arrange + var constants = TestConstants.Create(this); + string queueName = GetNewQueueName(); + QueueSasBuilder queueSasBuilder = BuildQueueSasBuilder(constants, queueName); + string signature = BuildUserDelegationSignature(constants, queueName); + string stringToSign = null; + + // Act + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(GetUserDelegationKey(constants), constants.Sas.Account, out stringToSign); + + // Assert + Assert.AreEqual(SasQueryParametersInternals.DefaultSasVersionInternal, sasQueryParameters.Version); + Assert.IsNull(sasQueryParameters.Services); + Assert.IsNull(sasQueryParameters.ResourceTypes); + Assert.AreEqual(constants.Sas.Protocol, sasQueryParameters.Protocol); + Assert.AreEqual(constants.Sas.StartTime, sasQueryParameters.StartsOn); + Assert.AreEqual(constants.Sas.ExpiryTime, sasQueryParameters.ExpiresOn); + Assert.AreEqual(constants.Sas.IPRange, sasQueryParameters.IPRange); + Assert.AreEqual(String.Empty, sasQueryParameters.Identifier); + Assert.AreEqual(constants.Sas.KeyObjectId, sasQueryParameters.KeyObjectId); + Assert.AreEqual(constants.Sas.KeyTenantId, sasQueryParameters.KeyTenantId); + Assert.AreEqual(constants.Sas.KeyStart, sasQueryParameters.KeyStartsOn); + 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.Resource.Queue, sasQueryParameters.Resource); + Assert.AreEqual(Permissions, sasQueryParameters.Permissions); + Assert.AreEqual(constants.Sas.DelegatedObjectId, sasQueryParameters.DelegatedUserObjectId); + Assert.AreEqual(signature, sasQueryParameters.Signature); + Assert.IsNotNull(stringToSign); + } + [RecordedTest] public void QueueSasBuilder_ToSasQueryParameters_VersionTest() { @@ -285,6 +323,7 @@ private QueueSasBuilder BuildQueueSasBuilder(TestConstants constants, string que IPRange = constants.Sas.IPRange, Identifier = constants.Sas.Identifier, QueueName = queueName, + DelegatedUserObjectId = constants.Sas.DelegatedObjectId, }; queueSasBuilder.SetPermissions(Permissions); @@ -305,5 +344,45 @@ private string BuildSignature(TestConstants constants, string queueName) return StorageSharedKeyCredentialInternals.ComputeSasSignature(constants.Sas.SharedKeyCredential, stringToSign); } + + private string BuildUserDelegationSignature(TestConstants constants, string queueName) + { + var stringToSign = string.Join("\n", + Permissions, + SasExtensions.FormatTimesForSasSigning(constants.Sas.StartTime), + SasExtensions.FormatTimesForSasSigning(constants.Sas.ExpiryTime), + "/queue/" + constants.Sas.Account + "/" + queueName, + constants.Sas.KeyObjectId, + constants.Sas.KeyTenantId, + SasExtensions.FormatTimesForSasSigning(constants.Sas.KeyStart), + SasExtensions.FormatTimesForSasSigning(constants.Sas.KeyExpiry), + constants.Sas.KeyService, + constants.Sas.KeyVersion, + null, + constants.Sas.DelegatedObjectId, + constants.Sas.IPRange.ToString(), + SasExtensions.ToProtocolString(SasProtocol.Https), + SasQueryParametersInternals.DefaultSasVersionInternal); + + return ComputeHMACSHA256(constants.Sas.KeyValue, stringToSign); + } + + private string ComputeHMACSHA256(string userDelegationKeyValue, string message) => + Convert.ToBase64String( + new HMACSHA256( + Convert.FromBase64String(userDelegationKeyValue)) + .ComputeHash(Encoding.UTF8.GetBytes(message))); + + private static UserDelegationKey GetUserDelegationKey(TestConstants constants) + => new UserDelegationKey + { + SignedObjectId = constants.Sas.KeyObjectId, + SignedTenantId = constants.Sas.KeyTenantId, + SignedStartsOn = constants.Sas.KeyStart, + SignedExpiresOn = constants.Sas.KeyExpiry, + SignedService = constants.Sas.KeyService, + SignedVersion = constants.Sas.KeyVersion, + 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 25859c0b01d8..4b5639546352 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueSasTests.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; +using Azure.Core; using Azure.Core.TestFramework; +using Azure.Storage.Queues.Models; using Azure.Storage.Queues.Specialized; using Azure.Storage.Queues.Tests; +using Azure.Storage.Sas; using Azure.Storage.Test; using Azure.Storage.Test.Shared; using NUnit.Framework; @@ -118,6 +123,132 @@ public async Task AccountSas_ServiceOrder(string services) await InvokeAccountSasTest(services: services); } + [RecordedTest] + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task SendMessageAsync_UserDelegationSAS() + { + // Arrange + string queueName = GetNewQueueName(); + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( + startsOn: null, + expiresOn: Recording.UtcNow.AddHours(1)); + + UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; + + Uri queueUri = test.Queue.GenerateUserDelegationSasUri(QueueSasPermissions.All, Recording.UtcNow.AddHours(1), userDelegationKey); + + QueueClient queueClient = InstrumentClient(new QueueClient(queueUri, GetOptions())); + + // Act + Response response = await queueClient.SendMessageAsync( + messageText: GetNewString(), + visibilityTimeout: new TimeSpan(0, 0, 1), + timeToLive: new TimeSpan(1, 0, 0)); + + // Assert + Assert.NotNull(response.Value); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task SendMessageAsync_UserDelegationSAS_DelegatedObjectId() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( + startsOn: null, + expiresOn: Recording.UtcNow.AddHours(1)); + + // We need to get the object 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.ObjectId, out object objectId); + + UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; + + QueueSasBuilder queueSasBuilder = new QueueSasBuilder(QueueSasPermissions.All, Recording.UtcNow.AddHours(1)) + { + QueueName = test.Queue.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(userDelegationKey, 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.SendMessageAsync( + messageText: GetNewString(), + visibilityTimeout: new TimeSpan(0, 0, 1), + timeToLive: new TimeSpan(1, 0, 0)); + + // Assert + Assert.NotNull(response.Value); + } + + [RecordedTest] + [LiveOnly] // Cannot record Entra ID token + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task SendMessageAsync_UserDelegationSAS_DelegatedObjectId_Fail() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + await using DisposingQueue test = await GetTestQueueAsync(service); + + Response userDelegationKeyResponse = await service.GetUserDelegationKeyAsync( + startsOn: null, + expiresOn: Recording.UtcNow.AddHours(1)); + + // We need to get the object 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.ObjectId, out object objectId); + + UserDelegationKey userDelegationKey = userDelegationKeyResponse.Value; + + QueueSasBuilder queueSasBuilder = new QueueSasBuilder(QueueSasPermissions.All, Recording.UtcNow.AddHours(1)) + { + QueueName = test.Queue.Name, + DelegatedUserObjectId = objectId?.ToString() + }; + + QueueSasQueryParameters sasQueryParameters = queueSasBuilder.ToSasQueryParameters(userDelegationKey, service.AccountName, out string stringToSign); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(test.Queue.Uri) + { + Sas = sasQueryParameters + }; + + QueueClient identityQueueClient = InstrumentClient(new QueueClient(uriBuilder.ToUri(), GetOptions())); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + identityQueueClient.SendMessageAsync( + messageText: GetNewString(), + visibilityTimeout: new TimeSpan(0, 0, 1), + timeToLive: new TimeSpan(1, 0, 0)), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + // Creating Client from GetStorageClient #region QueueServiceClient private async Task InvokeAccountServiceToQueueSasTest( diff --git a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs index fff33753243e..bedab12900e4 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs @@ -300,6 +300,53 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual("OutOfRangeInput", e.ErrorCode)); } + [RecordedTest] + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task GetUserDelegatioKey() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + + // Act + Response response = await service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)); + + // Assert + Assert.IsNotNull(response.Value); + } + + [RecordedTest] + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task GetUserDelegationKey_Error() + { + // Arrange + QueueServiceClient service = GetServiceClient_SharedKey(); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + service.GetUserDelegationKeyAsync(startsOn: null, expiresOn: Recording.UtcNow.AddHours(1)), + e => Assert.AreEqual("AuthenticationFailed", e.ErrorCode)); + } + + [RecordedTest] + [ServiceVersion(Min = QueueClientOptions.ServiceVersion.V2026_02_06)] + public async Task GetUserDelegationKey_ArgumentException() + { + // Arrange + QueueServiceClient service = GetServiceClient_OAuth(); + + // Act + 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"))), + e => Assert.AreEqual("expiresOn must be UTC", e.Message)); + ; + } + #region Secondary Storage [RecordedTest] public async Task GetQueuesAsync_SecondaryStorageFirstRetrySuccessful()