diff --git a/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md b/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md index cf89b9d4ceac..a11d978f97de 100644 --- a/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Blobs/CHANGELOG.md @@ -3,6 +3,7 @@ ## 12.19.0-beta.1 (Unreleased) ### Features Added +- Added support for BlobClientOptions.Audience ### Breaking Changes diff --git a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net6.0.cs b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net6.0.cs index 22ee2d383324..05b9a1ad1356 100644 --- a/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net6.0.cs +++ b/sdk/storage/Azure.Storage.Blobs/api/Azure.Storage.Blobs.net6.0.cs @@ -52,6 +52,7 @@ public BlobClient(System.Uri blobUri, Azure.Storage.StorageSharedKeyCredential c public partial class BlobClientOptions : Azure.Core.ClientOptions { public BlobClientOptions(Azure.Storage.Blobs.BlobClientOptions.ServiceVersion version = Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Blobs.Models.BlobAudience? Audience { get { throw null; } set { } } public Azure.Storage.Blobs.Models.CustomerProvidedKey? CustomerProvidedKey { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public string EncryptionScope { get { throw null; } set { } } @@ -347,6 +348,24 @@ internal BlobAppendInfo() { } public System.DateTimeOffset LastModified { get { throw null; } } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct BlobAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public BlobAudience(string value) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Blobs.Models.BlobAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience GetBlobServiceAccountAudience(string storageAccountName) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static bool operator ==(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public static implicit operator Azure.Storage.Blobs.Models.BlobAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public override string ToString() { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct BlobBlock : System.IEquatable { private readonly object _dummy; 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 22ee2d383324..05b9a1ad1356 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 @@ -52,6 +52,7 @@ public BlobClient(System.Uri blobUri, Azure.Storage.StorageSharedKeyCredential c public partial class BlobClientOptions : Azure.Core.ClientOptions { public BlobClientOptions(Azure.Storage.Blobs.BlobClientOptions.ServiceVersion version = Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Blobs.Models.BlobAudience? Audience { get { throw null; } set { } } public Azure.Storage.Blobs.Models.CustomerProvidedKey? CustomerProvidedKey { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public string EncryptionScope { get { throw null; } set { } } @@ -347,6 +348,24 @@ internal BlobAppendInfo() { } public System.DateTimeOffset LastModified { get { throw null; } } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct BlobAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public BlobAudience(string value) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Blobs.Models.BlobAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience GetBlobServiceAccountAudience(string storageAccountName) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static bool operator ==(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public static implicit operator Azure.Storage.Blobs.Models.BlobAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public override string ToString() { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct BlobBlock : System.IEquatable { private readonly object _dummy; 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 22ee2d383324..05b9a1ad1356 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 @@ -52,6 +52,7 @@ public BlobClient(System.Uri blobUri, Azure.Storage.StorageSharedKeyCredential c public partial class BlobClientOptions : Azure.Core.ClientOptions { public BlobClientOptions(Azure.Storage.Blobs.BlobClientOptions.ServiceVersion version = Azure.Storage.Blobs.BlobClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Blobs.Models.BlobAudience? Audience { get { throw null; } set { } } public Azure.Storage.Blobs.Models.CustomerProvidedKey? CustomerProvidedKey { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public string EncryptionScope { get { throw null; } set { } } @@ -347,6 +348,24 @@ internal BlobAppendInfo() { } public System.DateTimeOffset LastModified { get { throw null; } } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct BlobAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public BlobAudience(string value) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Blobs.Models.BlobAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + public static Azure.Storage.Blobs.Models.BlobAudience GetBlobServiceAccountAudience(string storageAccountName) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static bool operator ==(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public static implicit operator Azure.Storage.Blobs.Models.BlobAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Blobs.Models.BlobAudience left, Azure.Storage.Blobs.Models.BlobAudience right) { throw null; } + public override string ToString() { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct BlobBlock : System.IEquatable { private readonly object _dummy; diff --git a/sdk/storage/Azure.Storage.Blobs/assets.json b/sdk/storage/Azure.Storage.Blobs/assets.json index e892d850e8dd..b5bc5a285f2a 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_b46f524eaf" + "Tag": "net/storage/Azure.Storage.Blobs_c772765837" } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs index 758935081c5e..e0ddbb6044ee 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs @@ -330,12 +330,14 @@ public BlobBaseClient(Uri blobUri, AzureSasCredential credential, BlobClientOpti /// public BlobBaseClient(Uri blobUri, TokenCredential credential, BlobClientOptions options = default) : this( - blobUri, - credential.AsPolicy(options), - options, - storageSharedKeyCredential: null, - sasCredential: null, - tokenCredential: credential) + blobUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? BlobAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + storageSharedKeyCredential: null, + sasCredential: null, + tokenCredential: credential) { Errors.VerifyHttpsTokenAuth(blobUri); } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs index 8d241f2e2585..f94ad5aa9d47 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs @@ -339,5 +339,11 @@ internal HttpPipeline Build(object credentials) /// public bool EnableTenantDiscovery { get; set; } + + /// + /// Gets or sets the Audience to use for authentication with Azure Active Directory (AAD). The audience is not considered when using a shared key. + /// + /// If null, will be assumed. + public BlobAudience? Audience { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs index f28a92e42523..2b86fbbdae2f 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobContainerClient.cs @@ -361,7 +361,10 @@ public BlobContainerClient(Uri blobContainerUri, TokenCredential credential, Blo Errors.VerifyHttpsTokenAuth(blobContainerUri); Argument.AssertNotNull(blobContainerUri, nameof(blobContainerUri)); _uri = blobContainerUri; - _authenticationPolicy = credential.AsPolicy(options); + + string audienceScope = string.IsNullOrEmpty(options?.Audience?.ToString()) ? BlobAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(); + + _authenticationPolicy = credential.AsPolicy(audienceScope, options); options ??= new BlobClientOptions(); _clientConfiguration = new BlobClientConfiguration( diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs index 08af5e3580d6..b198bdb9e951 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs @@ -246,7 +246,13 @@ public BlobServiceClient(Uri serviceUri, AzureSasCredential credential, BlobClie /// every request. /// public BlobServiceClient(Uri serviceUri, TokenCredential credential, BlobClientOptions options = default) - : this(serviceUri, credential.AsPolicy(options), credential, options ?? new BlobClientOptions()) + : this( + serviceUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? BlobAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + credential, + options ?? new BlobClientOptions()) { Errors.VerifyHttpsTokenAuth(serviceUri); } diff --git a/sdk/storage/Azure.Storage.Blobs/src/Models/BlobAudience.cs b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobAudience.cs new file mode 100644 index 000000000000..f6393e7b5806 --- /dev/null +++ b/sdk/storage/Azure.Storage.Blobs/src/Models/BlobAudience.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Azure.Core; + +namespace Azure.Storage.Blobs.Models +{ + /// + /// Audiences available for Blobs + /// + public readonly partial struct BlobAudience : IEquatable + { + private readonly string _value; + + /// + /// Intializes new instance of . + /// + /// + /// The Azure Active Directory audience to use when forming authorization scopes. + /// For the Language service, this value corresponds to a URL that identifies the Azure cloud where the resource is located. + /// For more information: . + /// + /// Please use one of the static constant members over creating a custom value unless you have specific scenario for doing so. + public BlobAudience(string value) + { + Argument.AssertNotNullOrEmpty(value, nameof(value)); + _value = value; + } + + private const string _publicAudience = "https://storage.azure.com/"; + + /// + /// Default Audience. Use to acquire a token for authorizing requests to any Azure Storage account + /// + /// Resource ID: "https://storage.azure.com/ ". + /// + /// If no audience is specified, this is the default value. + /// + public static BlobAudience PublicAudience { get; } = new(_publicAudience); + + /// + /// The service endpoint for a given storage account. + /// Use this method to acquire a token for authorizing requests to that specific Azure Storage account and service only. + /// + /// + /// The storage account name used to populate the service endpoint. + /// + /// + public static BlobAudience GetBlobServiceAccountAudience(string storageAccountName) => new($"https://{storageAccountName}.blob.core.windows.net/"); + + /// Determines if two values are the same. + public static bool operator ==(BlobAudience left, BlobAudience right) => left.Equals(right); + /// Determines if two values are not the same. + public static bool operator !=(BlobAudience left, BlobAudience right) => !left.Equals(right); + /// Converts a string to a . + public static implicit operator BlobAudience(string value) => new BlobAudience(value); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is BlobAudience other && Equals(other); + /// + public bool Equals(BlobAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + /// + public override string ToString() => _value; + + /// + /// Creates a scope with the respective audience and the default scope. + /// + /// + internal string CreateDefaultScope() + { + if (_value.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + { + return $"{(_value)}{Constants.DefaultScope}"; + } + return $"{(_value)}/{Constants.DefaultScope}"; + } + } +} diff --git a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs index b03b4c638f41..487beff6db51 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs @@ -156,6 +156,119 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(test.Container.AccountName)); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadBlob.ExistsAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + [RecordedTest] public void WithSnapshot() { diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs index 5b774aac8a61..e7c006e0bd27 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs @@ -242,6 +242,136 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(test.Container.AccountName)); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + // Hand it a Mock Credential that's supposed to fail + BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadBlob.ExistsAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + #region Sequential Download [RecordedTest] diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobClientTests.cs index 46934e477eb0..8ef1281c1e0c 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobClientTests.cs @@ -194,6 +194,135 @@ public void Ctor_With_Sas_Does_Not_Reorder_Services() StringAssert.Contains("ss=bqtf", transport.SingleRequest.Uri.ToString()); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobClient aadBlob = InstrumentClient(new BlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobClient aadBlob = InstrumentClient(new BlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(test.Container.AccountName)); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobClient aadBlob = InstrumentClient(new BlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + var data = GetRandomBuffer(Constants.KB); + BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName())); + using (var stream = new MemoryStream(data)) + { + await blob.UploadAsync(stream); + } + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + BlobClient aadBlob = InstrumentClient(new BlobClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadBlob.ExistsAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + #region Upload [RecordedTest] diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs index b6fda2025ad2..52c994a5b9a1 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobTestBase.cs @@ -66,6 +66,13 @@ public async Task GetTestContainerAsync( public BlobClientOptions GetOptions(bool parallelRange = false) => BlobsClientBuilder.GetOptions(parallelRange); + public BlobClientOptions GetOptionsWithAudience(BlobAudience audience) + { + BlobClientOptions options = BlobsClientBuilder.GetOptions(false); + options.Audience = audience; + return options; + } + internal async Task GetNewBlobClient(BlobContainerClient container, string blobName = default) { blobName ??= BlobsClientBuilder.GetNewBlobName(); diff --git a/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs index 182bef18f074..8b56ad2a0ca2 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/ContainerClientTests.cs @@ -329,6 +329,103 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = test.Container.Name, + }; + + BlobContainerClient aadContainer = InstrumentClient(new BlobContainerClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadContainer.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = test.Container.Name, + }; + + BlobContainerClient aadContainer = InstrumentClient(new BlobContainerClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadContainer.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(test.Container.AccountName)); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = test.Container.Name, + }; + + BlobContainerClient aadContainer = InstrumentClient(new BlobContainerClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadContainer.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = test.Container.Name, + }; + + BlobContainerClient aadContainer = InstrumentClient(new BlobContainerClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadContainer.ExistsAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + [RecordedTest] public async Task CreateAsync_WithSharedKey() { diff --git a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs index 960bc888e727..8901ea3c48a4 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs @@ -146,6 +146,119 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + PageBlobClient blob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + PageBlobClient aadBlob = InstrumentClient(new PageBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + PageBlobClient blob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + PageBlobClient aadBlob = InstrumentClient(new PageBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + PageBlobClient blob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(test.Container.AccountName)); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + PageBlobClient aadBlob = InstrumentClient(new PageBlobClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadBlob.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(); + + PageBlobClient blob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + await blob.CreateIfNotExistsAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net")); + + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name + }; + + PageBlobClient aadBlob = InstrumentClient(new PageBlobClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadBlob.ExistsAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + [RecordedTest] public async Task CreateAsync_Min() { diff --git a/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs index f63a91feb7ff..3a347a2b2c27 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/ServiceClientTests.cs @@ -175,6 +175,77 @@ public void Ctor_With_Sas_Does_Not_Reorder_Services() StringAssert.Contains("ss=bqtf", transport.SingleRequest.Uri.ToString()); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience); + + BlobServiceClient aadService = InstrumentClient(new BlobServiceClient( + new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{uriBuilder.AccountName}.blob.core.windows.net/")); + + BlobServiceClient aadService = InstrumentClient(new BlobServiceClient( + new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)); + + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(BlobAudience.GetBlobServiceAccountAudience(uriBuilder.AccountName)); + + BlobServiceClient aadService = InstrumentClient(new BlobServiceClient( + new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Act - Create new blob client with the OAuth Credential and Audience + BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net")); + + BlobServiceClient aadContainer = InstrumentClient(new BlobServiceClient( + new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadContainer.GetPropertiesAsync(), + e => Assert.AreEqual(BlobErrorCode.InvalidAuthenticationInfo.ToString(), e.ErrorCode)); + } + [RecordedTest] public async Task ListContainersSegmentAsync() { diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageClientOptions.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageClientOptions.cs index 29bcf90bc3c9..d69ee22760aa 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageClientOptions.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageClientOptions.cs @@ -64,21 +64,23 @@ public static HttpPipelinePolicy AsPolicy(this AzureSasCredential c /// Get an authentication policy to sign Storage requests. /// /// Credential to use. + /// Scope to use. /// The to apply to the credential. /// An authentication policy. - public static HttpPipelinePolicy AsPolicy(this TokenCredential credential, ClientOptions options) => + public static HttpPipelinePolicy AsPolicy(this TokenCredential credential, string scope, ClientOptions options) => new StorageBearerTokenChallengeAuthorizationPolicy( credential ?? throw Errors.ArgumentNull(nameof(credential)), - StorageScope, + scope ?? StorageScope, options is ISupportsTenantIdChallenges { EnableTenantDiscovery: true }); /// /// Get an optional authentication policy to sign Storage requests. /// /// Optional credentials to use. + /// Optional scope /// The /// An optional authentication policy. - public static HttpPipelinePolicy GetAuthenticationPolicy(object credentials = null, ClientOptions options = null) + public static HttpPipelinePolicy GetAuthenticationPolicy(object credentials = null, string scope = default, ClientOptions options = null) { // Use the credentials to decide on the authentication policy switch (credentials) @@ -89,7 +91,7 @@ public static HttpPipelinePolicy GetAuthenticationPolicy(object credentials = nu case StorageSharedKeyCredential sharedKey: return sharedKey.AsPolicy(); case TokenCredential token: - return token.AsPolicy(options); + return token.AsPolicy(scope, options); default: throw Errors.InvalidCredentials(credentials.GetType().FullName); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md b/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md index 3da2f5a68db5..c2cd00d5e4ce 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Files.DataLake/CHANGELOG.md @@ -3,6 +3,7 @@ ## 12.17.0-beta.1 (Unreleased) ### Features Added +- Added support for DataLakeClientOptions.Audience ### Breaking Changes diff --git a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net6.0.cs b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net6.0.cs index e7413dd153d0..14a2b5bcf08b 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net6.0.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/api/Azure.Storage.Files.DataLake.net6.0.cs @@ -3,6 +3,7 @@ namespace Azure.Storage.Files.DataLake public partial class DataLakeClientOptions : Azure.Core.ClientOptions { public DataLakeClientOptions(Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion version = Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Files.DataLake.Models.DataLakeAudience? Audience { get { throw null; } set { } } public Azure.Storage.Files.DataLake.Models.DataLakeCustomerProvidedKey? CustomerProvidedKey { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public System.Uri GeoRedundantSecondaryUri { get { throw null; } set { } } @@ -530,6 +531,24 @@ public DataLakeAnalyticsLogging() { } public string Version { get { throw null; } set { } } public bool Write { get { throw null; } set { } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DataLakeAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DataLakeAudience(string value) { throw null; } + public static Azure.Storage.Files.DataLake.Models.DataLakeAudience PublicAudience { get { throw null; } } + public static Azure.Storage.Files.DataLake.Models.DataLakeAudience DataLakeServiceAccountAudience(string storageAccountName) { throw null; } + public bool Equals(Azure.Storage.Files.DataLake.Models.DataLakeAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static bool operator ==(Azure.Storage.Files.DataLake.Models.DataLakeAudience left, Azure.Storage.Files.DataLake.Models.DataLakeAudience right) { throw null; } + public static implicit operator Azure.Storage.Files.DataLake.Models.DataLakeAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Files.DataLake.Models.DataLakeAudience left, Azure.Storage.Files.DataLake.Models.DataLakeAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class DataLakeCorsRule { public DataLakeCorsRule() { } 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 e7413dd153d0..14a2b5bcf08b 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 @@ -3,6 +3,7 @@ namespace Azure.Storage.Files.DataLake public partial class DataLakeClientOptions : Azure.Core.ClientOptions { public DataLakeClientOptions(Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion version = Azure.Storage.Files.DataLake.DataLakeClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Files.DataLake.Models.DataLakeAudience? Audience { get { throw null; } set { } } public Azure.Storage.Files.DataLake.Models.DataLakeCustomerProvidedKey? CustomerProvidedKey { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public System.Uri GeoRedundantSecondaryUri { get { throw null; } set { } } @@ -530,6 +531,24 @@ public DataLakeAnalyticsLogging() { } public string Version { get { throw null; } set { } } public bool Write { get { throw null; } set { } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DataLakeAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DataLakeAudience(string value) { throw null; } + public static Azure.Storage.Files.DataLake.Models.DataLakeAudience PublicAudience { get { throw null; } } + public static Azure.Storage.Files.DataLake.Models.DataLakeAudience DataLakeServiceAccountAudience(string storageAccountName) { throw null; } + public bool Equals(Azure.Storage.Files.DataLake.Models.DataLakeAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static bool operator ==(Azure.Storage.Files.DataLake.Models.DataLakeAudience left, Azure.Storage.Files.DataLake.Models.DataLakeAudience right) { throw null; } + public static implicit operator Azure.Storage.Files.DataLake.Models.DataLakeAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Files.DataLake.Models.DataLakeAudience left, Azure.Storage.Files.DataLake.Models.DataLakeAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class DataLakeCorsRule { public DataLakeCorsRule() { } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/assets.json b/sdk/storage/Azure.Storage.Files.DataLake/assets.json index 424ad23002c9..8330e66bcb45 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_7989a584e7" + "Tag": "net/storage/Azure.Storage.Files.DataLake_441f7e2d96" } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs index b505ace34752..54747e6c8af2 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeClientOptions.cs @@ -353,5 +353,11 @@ internal HttpPipeline Build(object credentials) { return this.Build(credentials, GeoRedundantSecondaryUri); } + + /// + /// Gets or sets the Audience to use for authentication with Azure Active Directory (AAD). The audience is not considered when using a shared key. + /// + /// If null, will be assumed. + public DataLakeAudience? Audience { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs index 612d9d413cbc..475ffe865d7d 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeDirectoryClient.cs @@ -219,9 +219,8 @@ public DataLakeDirectoryClient(Uri directoryUri, AzureSasCredential credential, /// The token credential used to sign requests. /// public DataLakeDirectoryClient(Uri directoryUri, TokenCredential credential) - : this(directoryUri, credential.AsPolicy(new DataLakeClientOptions()), null, tokenCredential: credential) + : this(directoryUri, credential, new DataLakeClientOptions()) { - Errors.VerifyHttpsTokenAuth(directoryUri); } /// @@ -242,7 +241,13 @@ public DataLakeDirectoryClient(Uri directoryUri, TokenCredential credential) /// every request. /// public DataLakeDirectoryClient(Uri directoryUri, TokenCredential credential, DataLakeClientOptions options) - : this(directoryUri, credential.AsPolicy(options), options, tokenCredential: credential) + : this( + directoryUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? DataLakeAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + tokenCredential: credential) { Errors.VerifyHttpsTokenAuth(directoryUri); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs index b862df6efacc..ad92346dbc08 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileClient.cs @@ -236,7 +236,7 @@ public DataLakeFileClient(Uri fileUri, AzureSasCredential credential, DataLakeCl /// The token credential used to sign requests. /// public DataLakeFileClient(Uri fileUri, TokenCredential credential) - : this(fileUri, credential.AsPolicy(new DataLakeClientOptions()), null, tokenCredential: credential) + : this(fileUri, credential, new DataLakeClientOptions()) { Errors.VerifyHttpsTokenAuth(fileUri); } @@ -258,7 +258,13 @@ public DataLakeFileClient(Uri fileUri, TokenCredential credential) /// applied to every request. /// public DataLakeFileClient(Uri fileUri, TokenCredential credential, DataLakeClientOptions options) - : this(fileUri, credential.AsPolicy(options), options, credential) + : this( + fileUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? DataLakeAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + credential) { Errors.VerifyHttpsTokenAuth(fileUri); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs index a42b4e716d20..e12cd25fbb18 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeFileSystemClient.cs @@ -366,15 +366,8 @@ public DataLakeFileSystemClient(Uri fileSystemUri, AzureSasCredential credential /// The token credential used to sign requests. /// public DataLakeFileSystemClient(Uri fileSystemUri, TokenCredential credential) - : this( - fileSystemUri, - credential.AsPolicy(new DataLakeClientOptions()), - options: null, - storageSharedKeyCredential: null, - sasCredential: null, - tokenCredential: credential) + : this(fileSystemUri, credential, new DataLakeClientOptions()) { - Errors.VerifyHttpsTokenAuth(fileSystemUri); } /// @@ -395,12 +388,14 @@ public DataLakeFileSystemClient(Uri fileSystemUri, TokenCredential credential) /// public DataLakeFileSystemClient(Uri fileSystemUri, TokenCredential credential, DataLakeClientOptions options) : this( - fileSystemUri, - credential.AsPolicy(options), - options, - storageSharedKeyCredential: null, - sasCredential: null, - tokenCredential: credential) + fileSystemUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? DataLakeAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + storageSharedKeyCredential: null, + sasCredential: null, + tokenCredential: credential) { Errors.VerifyHttpsTokenAuth(fileSystemUri); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs index 42f21f591b1d..8ae179ba89be 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakePathClient.cs @@ -454,12 +454,14 @@ public DataLakePathClient(Uri pathUri, TokenCredential credential) /// public DataLakePathClient(Uri pathUri, TokenCredential credential, DataLakeClientOptions options) : this( - pathUri, - credential.AsPolicy(options), - options, - storageSharedKeyCredential: null, - sasCredential: null, - tokenCredential: credential) + pathUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? DataLakeAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + storageSharedKeyCredential: null, + sasCredential: null, + tokenCredential: credential) { Errors.VerifyHttpsTokenAuth(pathUri); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs index c6fe10255581..071bb16ec06e 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/DataLakeServiceClient.cs @@ -293,15 +293,8 @@ public DataLakeServiceClient(Uri serviceUri, AzureSasCredential credential, Data /// The token credential used to sign requests. /// public DataLakeServiceClient(Uri serviceUri, TokenCredential credential) - : this( - serviceUri, - credential.AsPolicy(new DataLakeClientOptions()), - options: null, - storageSharedKeyCredential: null, - sasCredential: null, - tokenCredential: credential) + : this(serviceUri, credential, new DataLakeClientOptions()) { - Errors.VerifyHttpsTokenAuth(serviceUri); } /// @@ -321,12 +314,14 @@ public DataLakeServiceClient(Uri serviceUri, TokenCredential credential) /// public DataLakeServiceClient(Uri serviceUri, TokenCredential credential, DataLakeClientOptions options) : this( - serviceUri, - credential.AsPolicy(options), - options, - storageSharedKeyCredential:null, - sasCredential: null, - tokenCredential: credential) + serviceUri, + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? DataLakeAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), + options, + storageSharedKeyCredential:null, + sasCredential: null, + tokenCredential: credential) { Errors.VerifyHttpsTokenAuth(serviceUri); } diff --git a/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeAudience.cs b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeAudience.cs new file mode 100644 index 000000000000..99e3418c8e92 --- /dev/null +++ b/sdk/storage/Azure.Storage.Files.DataLake/src/Models/DataLakeAudience.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Azure.Core; + +namespace Azure.Storage.Files.DataLake.Models +{ + /// + /// Audiences available for Blobs + /// + public readonly partial struct DataLakeAudience : IEquatable + { + private readonly string _value; + + /// + /// Intializes new instance of . + /// + /// + /// The Azure Active Directory audience to use when forming authorization scopes. + /// For the Language service, this value corresponds to a URL that identifies the Azure cloud where the resource is located. + /// For more information: . + /// + /// Please use one of the static constant members over creating a custom value unless you have specific scenario for doing so. + public DataLakeAudience(string value) + { + Argument.AssertNotNullOrEmpty(value, nameof(value)); + _value = value; + } + + private const string _publicAudience = "https://storage.azure.com/"; + + /// + /// Default Audience. Use to acquire a token for authorizing requests to any Azure Storage account + /// + /// Resource ID: "https://storage.azure.com/ ". + /// + /// If no audience is specified, this is the default value. + /// + public static DataLakeAudience PublicAudience { get; } = new(_publicAudience); + + /// + /// The service endpoint for a given storage account. + /// Use this method to acquire a token for authorizing requests to that specific Azure Storage account and service only. + /// + /// + /// The storage account name used to populate the service endpoint. + /// + /// + public static DataLakeAudience DataLakeServiceAccountAudience(string storageAccountName) => new($"https://{storageAccountName}.blob.core.windows.net/"); + + /// Determines if two values are the same. + public static bool operator ==(DataLakeAudience left, DataLakeAudience right) => left.Equals(right); + /// Determines if two values are not the same. + public static bool operator !=(DataLakeAudience left, DataLakeAudience right) => !left.Equals(right); + /// Converts a string to a . + public static implicit operator DataLakeAudience(string value) => new DataLakeAudience(value); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is DataLakeAudience other && Equals(other); + /// + public bool Equals(DataLakeAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + /// + public override string ToString() => _value; + + /// + /// Creates a scope with the respective audience and the default scope. + /// + /// + internal string CreateDefaultScope() + { + if (_value.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + { + return $"{(_value)}{Constants.DefaultScope}"; + } + return $"{(_value)}/{Constants.DefaultScope}"; + } + } +} diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs index 7eb849f1015c..8f69b5542d2d 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DataLakeTestBase.cs @@ -8,7 +8,6 @@ using System.Net; using System.Threading.Tasks; using Azure.Core; -using Azure.Core.Pipeline; using Azure.Core.TestFramework; using Azure.Storage.Files.DataLake.Models; using Azure.Storage.Sas; @@ -90,6 +89,13 @@ public DataLakeClientOptions GetFaultyDataLakeConnectionOptions( return options; } + public DataLakeClientOptions GetOptionsWithAudience(DataLakeAudience audience) + { + DataLakeClientOptions options = DataLakeClientBuilder.GetOptions(false); + options.Audience = audience; + return options; + } + public DataLakeServiceClient GetServiceClientFromOauthConfig(TenantConfiguration config) => InstrumentClient( new DataLakeServiceClient( @@ -105,6 +111,9 @@ public StorageSharedKeyCredential GetStorageSharedKeyCredentials() TestConfigHierarchicalNamespace.AccountName, TestConfigHierarchicalNamespace.AccountKey); + public TokenCredential GetOAuthHnsCredential() + => Tenants.GetOAuthCredential(Tenants.TestConfigHierarchicalNamespace); + public static void AssertValidStoragePathInfo(PathInfo pathInfo) { Assert.IsNotNull(pathInfo.ETag); diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs index 364f0426ad5a..347dfe198afc 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/DirectoryClientTests.cs @@ -222,6 +222,119 @@ public void Ctor_CPK_Http() new ArgumentException("Cannot use client-provided key without HTTPS.")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeDirectoryClient pathClient = test.FileSystem.GetDirectoryClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.PublicAudience); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeDirectoryClient aadPathClient = InstrumentClient(new DataLakeDirectoryClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadPathClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeDirectoryClient fileClient = test.FileSystem.GetDirectoryClient(GetNewFileName()); + await fileClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience($"https://{test.FileSystem.AccountName}.blob.core.windows.net/")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = fileClient.FileSystemName, + DirectoryOrFilePath = fileClient.Name + }; + + DataLakeDirectoryClient aadDirClient = InstrumentClient(new DataLakeDirectoryClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadDirClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeDirectoryClient pathClient = test.FileSystem.GetDirectoryClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.DataLakeServiceAccountAudience(test.FileSystem.AccountName)); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeDirectoryClient aadDirClient = InstrumentClient(new DataLakeDirectoryClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadDirClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeDirectoryClient pathClient = test.FileSystem.GetDirectoryClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience("https://badaudience.blob.core.windows.net")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeDirectoryClient aadDirClient = InstrumentClient(new DataLakeDirectoryClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadDirClient.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] [TestCase(false)] [TestCase(true)] diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs index 1d2d1e8f20c0..170bb2adae7e 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileClientTests.cs @@ -225,6 +225,119 @@ public void Ctor_CPK_Http() new ArgumentException("Cannot use client-provided key without HTTPS.")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeFileClient pathClient = test.FileSystem.GetFileClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.PublicAudience); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeFileClient aadPathClient = InstrumentClient(new DataLakeFileClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadPathClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeFileClient fileClient = test.FileSystem.GetFileClient(GetNewFileName()); + await fileClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience($"https://{test.FileSystem.AccountName}.blob.core.windows.net/")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = fileClient.FileSystemName, + DirectoryOrFilePath = fileClient.Name + }; + + DataLakeFileClient aadFileClient = InstrumentClient(new DataLakeFileClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadFileClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeFileClient pathClient = test.FileSystem.GetFileClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.DataLakeServiceAccountAudience(test.FileSystem.AccountName)); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeFileClient aadFileClient = InstrumentClient(new DataLakeFileClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadFileClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + DataLakeFileClient pathClient = test.FileSystem.GetFileClient(GetNewFileName()); + await pathClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience("https://badaudience.blob.core.windows.net")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint)) + { + FileSystemName = pathClient.FileSystemName, + DirectoryOrFilePath = pathClient.Name + }; + + DataLakeFileClient aadFileClient = InstrumentClient(new DataLakeFileClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadFileClient.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] [TestCase(false)] [TestCase(true)] diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs index 19228702920f..36220d93799d 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/FileSystemClientTests.cs @@ -233,6 +233,102 @@ public void Ctor_CPK_Http() new ArgumentException("Cannot use client-provided key without HTTPS.")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.PublicAudience); + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = test.FileSystem.Name, + }; + + DataLakeFileSystemClient aadFileSystemClient = InstrumentClient(new DataLakeFileSystemClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadFileSystemClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience($"https://{test.FileSystem.AccountName}.blob.core.windows.net/")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = test.FileSystem.Name, + }; + + DataLakeFileSystemClient aadFileSystemClient = InstrumentClient(new DataLakeFileSystemClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadFileSystemClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.DataLakeServiceAccountAudience(test.FileSystem.AccountName)); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = test.FileSystem.Name, + }; + + DataLakeFileSystemClient aadFileSystemClient = InstrumentClient(new DataLakeFileSystemClient( + uriBuilder.ToUri(), + GetOAuthHnsCredential(), + options)); + + // Assert + bool exists = await aadFileSystemClient.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingFileSystem test = await GetNewFileSystem(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience("https://badaudience.blob.core.windows.net")); + + DataLakeUriBuilder uriBuilder = new DataLakeUriBuilder(new Uri(Tenants.TestConfigHierarchicalNamespace.BlobServiceEndpoint)) + { + FileSystemName = test.FileSystem.Name, + }; + + DataLakeFileSystemClient aadFileSystemClient = InstrumentClient(new DataLakeFileSystemClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadFileSystemClient.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public async Task GetFileClient() { diff --git a/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs index 868fcfed2f1a..ed4f37144ec6 100644 --- a/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.DataLake/tests/ServiceClientTests.cs @@ -191,6 +191,83 @@ public void Ctor_CPK_Http() new ArgumentException("Cannot use client-provided key without HTTPS.")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + DataLakeServiceClient service = DataLakeClientBuilder.GetServiceClient_Hns(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.PublicAudience); + + DataLakeServiceClient aadServiceClient = InstrumentClient(new DataLakeServiceClient( + service.Uri, + GetOAuthHnsCredential(), + options)); + + // Assert + DataLakeServiceProperties properties = await aadServiceClient.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + DataLakeServiceClient service = DataLakeClientBuilder.GetServiceClient_Hns(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience($"https://{service.AccountName}.blob.core.windows.net/")); + + DataLakeServiceClient aadServiceClient = InstrumentClient(new DataLakeServiceClient( + service.Uri, + GetOAuthHnsCredential(), + options)); + + // Assert + DataLakeServiceProperties properties = await aadServiceClient.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + DataLakeServiceClient service = DataLakeClientBuilder.GetServiceClient_Hns(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(DataLakeAudience.DataLakeServiceAccountAudience(service.AccountName)); + + DataLakeServiceClient aadServiceClient = InstrumentClient(new DataLakeServiceClient( + service.Uri, + GetOAuthHnsCredential(), + options)); + + // Assert + DataLakeServiceProperties properties = await aadServiceClient.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + DataLakeServiceClient service = DataLakeClientBuilder.GetServiceClient_Hns(); + + // Act - Create new blob client with the OAuth Credential and Audience + DataLakeClientOptions options = GetOptionsWithAudience(new DataLakeAudience("https://badaudience.blob.core.windows.net")); + + DataLakeServiceClient aadServiceClient = InstrumentClient(new DataLakeServiceClient( + service.Uri, + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadServiceClient.GetPropertiesAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public async Task GetUserDelegationKey() { diff --git a/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md b/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md index b29edce7d428..7bd4aba9f52f 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Files.Shares/CHANGELOG.md @@ -3,6 +3,7 @@ ## 12.17.0-beta.1 (Unreleased) ### Features Added +- Added support for ShareClientOptions.Audience ### Breaking Changes diff --git a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net6.0.cs b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net6.0.cs index 7decf9413ee3..9baaabf89aa5 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net6.0.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/api/Azure.Storage.Files.Shares.net6.0.cs @@ -100,6 +100,7 @@ public partial class ShareClientOptions : Azure.Core.ClientOptions public ShareClientOptions(Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion version = Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion.V2023_08_03) { } public bool? AllowSourceTrailingDot { get { throw null; } set { } } public bool? AllowTrailingDot { get { throw null; } set { } } + public Azure.Storage.Files.Shares.Models.ShareAudience? Audience { get { throw null; } set { } } public Azure.Storage.Files.Shares.Models.ShareTokenIntent? ShareTokenIntent { get { throw null; } set { } } public Azure.Storage.TransferValidationOptions TransferValidation { get { throw null; } } public Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion Version { get { throw null; } } @@ -511,6 +512,24 @@ public ShareAccessPolicy() { } public static bool operator !=(Azure.Storage.Files.Shares.Models.ShareAccessTier left, Azure.Storage.Files.Shares.Models.ShareAccessTier right) { throw null; } public override string ToString() { throw null; } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct ShareAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public ShareAudience(string value) { throw null; } + public static Azure.Storage.Files.Shares.Models.ShareAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Files.Shares.Models.ShareAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Storage.Files.Shares.Models.ShareAudience GetShareServiceAccountAudience(string storageAccountName) { throw null; } + public static bool operator ==(Azure.Storage.Files.Shares.Models.ShareAudience left, Azure.Storage.Files.Shares.Models.ShareAudience right) { throw null; } + public static implicit operator Azure.Storage.Files.Shares.Models.ShareAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Files.Shares.Models.ShareAudience left, Azure.Storage.Files.Shares.Models.ShareAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class ShareCorsRule { public ShareCorsRule() { } 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 7decf9413ee3..9baaabf89aa5 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 @@ -100,6 +100,7 @@ public partial class ShareClientOptions : Azure.Core.ClientOptions public ShareClientOptions(Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion version = Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion.V2023_08_03) { } public bool? AllowSourceTrailingDot { get { throw null; } set { } } public bool? AllowTrailingDot { get { throw null; } set { } } + public Azure.Storage.Files.Shares.Models.ShareAudience? Audience { get { throw null; } set { } } public Azure.Storage.Files.Shares.Models.ShareTokenIntent? ShareTokenIntent { get { throw null; } set { } } public Azure.Storage.TransferValidationOptions TransferValidation { get { throw null; } } public Azure.Storage.Files.Shares.ShareClientOptions.ServiceVersion Version { get { throw null; } } @@ -511,6 +512,24 @@ public ShareAccessPolicy() { } public static bool operator !=(Azure.Storage.Files.Shares.Models.ShareAccessTier left, Azure.Storage.Files.Shares.Models.ShareAccessTier right) { throw null; } public override string ToString() { throw null; } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct ShareAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public ShareAudience(string value) { throw null; } + public static Azure.Storage.Files.Shares.Models.ShareAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Files.Shares.Models.ShareAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Storage.Files.Shares.Models.ShareAudience GetShareServiceAccountAudience(string storageAccountName) { throw null; } + public static bool operator ==(Azure.Storage.Files.Shares.Models.ShareAudience left, Azure.Storage.Files.Shares.Models.ShareAudience right) { throw null; } + public static implicit operator Azure.Storage.Files.Shares.Models.ShareAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Files.Shares.Models.ShareAudience left, Azure.Storage.Files.Shares.Models.ShareAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class ShareCorsRule { public ShareCorsRule() { } diff --git a/sdk/storage/Azure.Storage.Files.Shares/assets.json b/sdk/storage/Azure.Storage.Files.Shares/assets.json index 8c9a945bf459..b8f3fa774088 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_8ce2f9f069" + "Tag": "net/storage/Azure.Storage.Files.Shares_7cb987c9c8" } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareAudience.cs b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareAudience.cs new file mode 100644 index 000000000000..13f889681180 --- /dev/null +++ b/sdk/storage/Azure.Storage.Files.Shares/src/Models/ShareAudience.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Azure.Core; + +namespace Azure.Storage.Files.Shares.Models +{ + /// + /// Audiences available for Blobs + /// + public readonly partial struct ShareAudience : IEquatable + { + private readonly string _value; + + /// + /// Intializes new instance of . + /// + /// + /// The Azure Active Directory audience to use when forming authorization scopes. + /// For the Language service, this value corresponds to a URL that identifies the Azure cloud where the resource is located. + /// For more information: . + /// + /// Please use one of the static constant members over creating a custom value unless you have specific scenario for doing so. + public ShareAudience(string value) + { + Argument.AssertNotNullOrEmpty(value, nameof(value)); + _value = value; + } + + private const string _publicAudience = "https://storage.azure.com/"; + + /// + /// Default Audience. Use to acquire a token for authorizing requests to any Azure Storage account + /// + /// Resource ID: "https://storage.azure.com/ ". + /// + /// If no audience is specified, this is the default value. + /// + public static ShareAudience PublicAudience { get; } = new(_publicAudience); + + /// + /// The service endpoint for a given storage account. + /// Use this method to acquire a token for authorizing requests to that specific Azure Storage account and service only. + /// + /// + /// The storage account name used to populate the service endpoint. + /// + /// + public static ShareAudience GetShareServiceAccountAudience(string storageAccountName) => new($"https://{storageAccountName}.file.core.windows.net/"); + + /// Determines if two values are the same. + public static bool operator ==(ShareAudience left, ShareAudience right) => left.Equals(right); + /// Determines if two values are not the same. + public static bool operator !=(ShareAudience left, ShareAudience right) => !left.Equals(right); + /// Converts a string to a . + public static implicit operator ShareAudience(string value) => new ShareAudience(value); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ShareAudience other && Equals(other); + /// + public bool Equals(ShareAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + /// + public override string ToString() => _value; + + /// + /// Creates a scope with the respective audience and the default scope. + /// + /// + internal string CreateDefaultScope() + { + if (_value.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + { + return $"{(_value)}{Constants.DefaultScope}"; + } + return $"{(_value)}/{Constants.DefaultScope}"; + } + } +} diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs index 4d4dbbedb8a3..f864c8c17396 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClient.cs @@ -293,7 +293,9 @@ public ShareClient( ShareClientOptions options = default) : this( shareUri: shareUri, - authentication: credential.AsPolicy(options), + authentication: credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? ShareAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options: options ?? new ShareClientOptions(), storageSharedKeyCredential: default, sasCredential: default, diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs index 00a1ac84f245..06821a83e92c 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs @@ -265,5 +265,11 @@ private void AddHeadersAndQueryParameters() Diagnostics.LoggedQueryParameters.Add("copyid"); Diagnostics.LoggedQueryParameters.Add("restype"); } + + /// + /// Gets or sets the Audience to use for authentication with Azure Active Directory (AAD). The audience is not considered when using a shared key. + /// + /// If null, will be assumed. + public ShareAudience? Audience { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareDirectoryClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareDirectoryClient.cs index c04054caad4f..e4556b4e40fb 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareDirectoryClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareDirectoryClient.cs @@ -325,7 +325,9 @@ public ShareDirectoryClient( ShareClientOptions options = default) : this( directoryUri: directoryUri, - authentication: credential.AsPolicy(options), + authentication: credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? ShareAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options: options ?? new ShareClientOptions(), storageSharedKeyCredential: null, sasCredential: null, diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs index bc564df197f0..4b9542cb1072 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareFileClient.cs @@ -333,7 +333,9 @@ public ShareFileClient( ShareClientOptions options = default) : this( fileUri: fileUri, - authentication: credential.AsPolicy(options), + authentication: credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? ShareAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options: options ?? new ShareClientOptions(), storageSharedKeyCredential: null, sasCredential: null, diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs index 0a2d18d28ac3..574a02d49510 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareServiceClient.cs @@ -273,7 +273,9 @@ public ShareServiceClient( ShareClientOptions options = default) : this( serviceUri: serviceUri, - authentication: credential.AsPolicy(options), + authentication: credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? ShareAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options: options ?? new ShareClientOptions(), sharedKeyCredential: null, sasCredential: null, diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/DirectoryClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/DirectoryClientTests.cs index b71906014ccc..05d994441734 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/DirectoryClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/DirectoryClientTests.cs @@ -108,6 +108,115 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.PublicAudience); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = directoryClient.Path + }; + + ShareDirectoryClient aadDirClient = InstrumentClient(new ShareDirectoryClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadDirClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience($"https://{directoryClient.AccountName}.file.core.windows.net/")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = directoryClient.Path + }; + + ShareDirectoryClient aadDirClient = InstrumentClient(new ShareDirectoryClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadDirClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.GetShareServiceAccountAudience(directoryClient.AccountName)); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = directoryClient.Path + }; + + ShareDirectoryClient aadDirClient = InstrumentClient(new ShareDirectoryClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadDirClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience("https://badaudience.blob.core.windows.net")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = directoryClient.Path + }; + + ShareDirectoryClient aadDirClient = InstrumentClient(new ShareDirectoryClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadDirClient.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public void DirectoryPathsParsing() { diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs index da6e1838c184..92a50a1cbf4f 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core.TestFramework; +using Azure.Identity; using Azure.Storage.Files.Shares.Models; using Azure.Storage.Files.Shares.Specialized; using Azure.Storage.Sas; @@ -109,6 +110,123 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + ShareFileClient fileClient = directoryClient.GetFileClient(GetNewFileName()); + await fileClient.CreateAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.PublicAudience); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = fileClient.Path + }; + + ShareFileClient aadFileClient = InstrumentClient(new ShareFileClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadFileClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + ShareFileClient fileClient = directoryClient.GetFileClient(GetNewFileName()); + await fileClient.CreateAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience($"https://{test.Share.AccountName}.file.core.windows.net/")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = fileClient.Path + }; + + ShareFileClient aadFileClient = InstrumentClient(new ShareFileClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadFileClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + ShareFileClient fileClient = directoryClient.GetFileClient(GetNewFileName()); + await fileClient.CreateAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.GetShareServiceAccountAudience(test.Share.AccountName)); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = fileClient.Path + }; + + ShareFileClient aadFileClient = InstrumentClient(new ShareFileClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadFileClient.ExistsAsync(); + Assert.IsNotNull(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + ShareDirectoryClient directoryClient = test.Share.GetDirectoryClient(GetNewDirectoryName()); + await directoryClient.CreateIfNotExistsAsync(); + ShareFileClient fileClient = directoryClient.GetFileClient(GetNewFileName()); + await fileClient.CreateAsync(Constants.KB); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience("https://badaudience.blob.core.windows.net")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + DirectoryOrFilePath = fileClient.Path + }; + + ShareFileClient aadFileClient = InstrumentClient(new ShareFileClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadFileClient.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public void FilePathsParsing() { diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/FileTestBase.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/FileTestBase.cs index cbe19b0571b2..1e1ffb9e4627 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/FileTestBase.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/FileTestBase.cs @@ -81,6 +81,14 @@ public ShareClientOptions GetFaultyFileConnectionOptions( return options; } + public ShareClientOptions GetOptionsWithAudience(ShareAudience audience) + { + ShareClientOptions options = SharesClientBuilder.GetOptions(false); + options.Audience = audience; + options.ShareTokenIntent = ShareTokenIntent.Backup; + return options; + } + public ShareServiceClient GetServiceClient_AccountSas(StorageSharedKeyCredential sharedKeyCredentials = default, SasQueryParameters sasCredentials = default) => InstrumentClient( new ShareServiceClient( diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs index 51e75ad106f2..f2bb7b675413 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ServiceClientTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Azure.Core.TestFramework; +using Azure.Identity; using Azure.Storage.Files.Shares.Models; using Azure.Storage.Sas; using Azure.Storage.Test; diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTests.cs index be5828c92abe..aa999bde9b19 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/ShareClientTests.cs @@ -153,6 +153,107 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.PublicAudience); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Container.Name, + }; + + ShareClient aadShare = InstrumentClient(new ShareClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + var permission = "O:S-1-5-21-2127521184-1604012920-1887927527-21560751G:S-1-5-21-2127521184-1604012920-1887927527-513D:AI(A;;FA;;;SY)(A;;FA;;;BA)(A;;0x1200a9;;;S-1-5-21-397955417-626881126-188441444-3053964)S:NO_ACCESS_CONTROL"; + PermissionInfo infoPermission = await aadShare.CreatePermissionAsync(permission); + Assert.IsNotNull(infoPermission); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience($"https://{test.Share.AccountName}.file.core.windows.net/")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + }; + + ShareClient aadShare = InstrumentClient(new ShareClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + var permission = "O:S-1-5-21-2127521184-1604012920-1887927527-21560751G:S-1-5-21-2127521184-1604012920-1887927527-513D:AI(A;;FA;;;SY)(A;;FA;;;BA)(A;;0x1200a9;;;S-1-5-21-397955417-626881126-188441444-3053964)S:NO_ACCESS_CONTROL"; + PermissionInfo infoPermission = await aadShare.CreatePermissionAsync(permission); + Assert.IsNotNull(infoPermission); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(ShareAudience.GetShareServiceAccountAudience(test.Share.AccountName)); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + }; + + ShareClient aadShare = InstrumentClient(new ShareClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + var permission = "O:S-1-5-21-2127521184-1604012920-1887927527-21560751G:S-1-5-21-2127521184-1604012920-1887927527-513D:AI(A;;FA;;;SY)(A;;FA;;;BA)(A;;0x1200a9;;;S-1-5-21-397955417-626881126-188441444-3053964)S:NO_ACCESS_CONTROL"; + PermissionInfo infoPermission = await aadShare.CreatePermissionAsync(permission); + Assert.IsNotNull(infoPermission); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + ShareClientOptions options = GetOptionsWithAudience(new ShareAudience("https://badaudience.blob.core.windows.net")); + + ShareUriBuilder uriBuilder = new ShareUriBuilder(new Uri(Tenants.TestConfigOAuth.FileServiceEndpoint)) + { + ShareName = test.Share.Name, + }; + + ShareClient aadShare = InstrumentClient(new ShareClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + var permission = "O:S-1-5-21-2127521184-1604012920-1887927527-21560751G:S-1-5-21-2127521184-1604012920-1887927527-513D:AI(A;;FA;;;SY)(A;;FA;;;BA)(A;;0x1200a9;;;S-1-5-21-397955417-626881126-188441444-3053964)S:NO_ACCESS_CONTROL"; + await TestHelper.AssertExpectedExceptionAsync( + aadShare.CreatePermissionAsync(permission), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public void WithSnapshot() { diff --git a/sdk/storage/Azure.Storage.Queues/CHANGELOG.md b/sdk/storage/Azure.Storage.Queues/CHANGELOG.md index c8525daa9090..9f8a83cdc36a 100644 --- a/sdk/storage/Azure.Storage.Queues/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Queues/CHANGELOG.md @@ -3,6 +3,7 @@ ## 12.17.0-beta.1 (Unreleased) ### Features Added +- Added support for QueueClientOptions.Audience ### Breaking Changes diff --git a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net6.0.cs b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net6.0.cs index f2f596fb81fb..da13df44eeff 100644 --- a/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net6.0.cs +++ b/sdk/storage/Azure.Storage.Queues/api/Azure.Storage.Queues.net6.0.cs @@ -71,6 +71,7 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public partial class QueueClientOptions : Azure.Core.ClientOptions { public QueueClientOptions(Azure.Storage.Queues.QueueClientOptions.ServiceVersion version = Azure.Storage.Queues.QueueClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Queues.Models.QueueAudience? Audience { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public System.Uri GeoRedundantSecondaryUri { get { throw null; } set { } } public Azure.Storage.Queues.QueueMessageEncoding MessageEncoding { get { throw null; } set { } } @@ -184,6 +185,24 @@ public QueueAnalyticsLogging() { } public string Version { get { throw null; } set { } } public bool Write { get { throw null; } set { } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct QueueAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public QueueAudience(string value) { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Queues.Models.QueueAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience GetQueueServiceAccountAudience(string storageAccountName) { throw null; } + public static bool operator ==(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public static implicit operator Azure.Storage.Queues.Models.QueueAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class QueueCorsRule { public QueueCorsRule() { } 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 f2f596fb81fb..da13df44eeff 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 @@ -71,6 +71,7 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public partial class QueueClientOptions : Azure.Core.ClientOptions { public QueueClientOptions(Azure.Storage.Queues.QueueClientOptions.ServiceVersion version = Azure.Storage.Queues.QueueClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Queues.Models.QueueAudience? Audience { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public System.Uri GeoRedundantSecondaryUri { get { throw null; } set { } } public Azure.Storage.Queues.QueueMessageEncoding MessageEncoding { get { throw null; } set { } } @@ -184,6 +185,24 @@ public QueueAnalyticsLogging() { } public string Version { get { throw null; } set { } } public bool Write { get { throw null; } set { } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct QueueAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public QueueAudience(string value) { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Queues.Models.QueueAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience GetQueueServiceAccountAudience(string storageAccountName) { throw null; } + public static bool operator ==(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public static implicit operator Azure.Storage.Queues.Models.QueueAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class QueueCorsRule { public QueueCorsRule() { } 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 f2f596fb81fb..da13df44eeff 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 @@ -71,6 +71,7 @@ public QueueClient(System.Uri queueUri, Azure.Storage.StorageSharedKeyCredential public partial class QueueClientOptions : Azure.Core.ClientOptions { public QueueClientOptions(Azure.Storage.Queues.QueueClientOptions.ServiceVersion version = Azure.Storage.Queues.QueueClientOptions.ServiceVersion.V2023_08_03) { } + public Azure.Storage.Queues.Models.QueueAudience? Audience { get { throw null; } set { } } public bool EnableTenantDiscovery { get { throw null; } set { } } public System.Uri GeoRedundantSecondaryUri { get { throw null; } set { } } public Azure.Storage.Queues.QueueMessageEncoding MessageEncoding { get { throw null; } set { } } @@ -184,6 +185,24 @@ public QueueAnalyticsLogging() { } public string Version { get { throw null; } set { } } public bool Write { get { throw null; } set { } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct QueueAudience : System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public QueueAudience(string value) { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience PublicAudience { get { throw null; } } + public bool Equals(Azure.Storage.Queues.Models.QueueAudience other) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Storage.Queues.Models.QueueAudience GetQueueServiceAccountAudience(string storageAccountName) { throw null; } + public static bool operator ==(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public static implicit operator Azure.Storage.Queues.Models.QueueAudience (string value) { throw null; } + public static bool operator !=(Azure.Storage.Queues.Models.QueueAudience left, Azure.Storage.Queues.Models.QueueAudience right) { throw null; } + public override string ToString() { throw null; } + } public partial class QueueCorsRule { public QueueCorsRule() { } diff --git a/sdk/storage/Azure.Storage.Queues/assets.json b/sdk/storage/Azure.Storage.Queues/assets.json index f783f950888b..10846f8f5697 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_b61c362602" + "Tag": "net/storage/Azure.Storage.Queues_03962ffb14" } diff --git a/sdk/storage/Azure.Storage.Queues/src/Models/QueueAudience.cs b/sdk/storage/Azure.Storage.Queues/src/Models/QueueAudience.cs new file mode 100644 index 000000000000..fa92c17aa1c3 --- /dev/null +++ b/sdk/storage/Azure.Storage.Queues/src/Models/QueueAudience.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Azure.Core; + +namespace Azure.Storage.Queues.Models +{ + /// + /// Audiences available for Blobs + /// + public readonly partial struct QueueAudience : IEquatable + { + private readonly string _value; + + /// + /// Intializes new instance of . + /// + /// + /// The Azure Active Directory audience to use when forming authorization scopes. + /// For the Language service, this value corresponds to a URL that identifies the Azure cloud where the resource is located. + /// For more information: . + /// + /// Please use one of the static constant members over creating a custom value unless you have specific scenario for doing so. + public QueueAudience(string value) + { + Argument.AssertNotNullOrEmpty(value, nameof(value)); + _value = value; + } + + private const string _publicAudience = "https://storage.azure.com/"; + + /// + /// Default Audience. Use to acquire a token for authorizing requests to any Azure Storage account + /// + /// Resource ID: "https://storage.azure.com/ ". + /// + /// If no audience is specified, this is the default value. + /// + public static QueueAudience PublicAudience { get; } = new(_publicAudience); + + /// + /// The service endpoint for a given storage account. + /// Use this method to acquire a token for authorizing requests to that specific Azure Storage account and service only. + /// + /// + /// The storage account name used to populate the service endpoint. + /// + /// + public static QueueAudience GetQueueServiceAccountAudience(string storageAccountName) => new($"https://{storageAccountName}.queue.core.windows.net/"); + + /// Determines if two values are the same. + public static bool operator ==(QueueAudience left, QueueAudience right) => left.Equals(right); + /// Determines if two values are not the same. + public static bool operator !=(QueueAudience left, QueueAudience right) => !left.Equals(right); + /// Converts a string to a . + public static implicit operator QueueAudience(string value) => new QueueAudience(value); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is QueueAudience other && Equals(other); + /// + public bool Equals(QueueAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + /// + public override string ToString() => _value; + + /// + /// Creates a scope with the respective audience and the default scope. + /// + /// + internal string CreateDefaultScope() + { + if (_value.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + { + return $"{(_value)}{Constants.DefaultScope}"; + } + return $"{(_value)}/{Constants.DefaultScope}"; + } + } +} diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs index b7ffcf191da3..462e614fd437 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueClient.cs @@ -324,7 +324,9 @@ public QueueClient(Uri queueUri, AzureSasCredential credential, QueueClientOptio public QueueClient(Uri queueUri, TokenCredential credential, QueueClientOptions options = default) : this( queueUri, - credential.AsPolicy(options), + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? QueueAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options, sharedKeyCredential: null, sasCredential: null, diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs b/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs index fcbe43a68cae..fc7181e1365e 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueClientOptions.cs @@ -303,5 +303,11 @@ internal HttpPipeline Build(object credentials) { return this.Build(credentials, GeoRedundantSecondaryUri); } + + /// + /// Gets or sets the Audience to use for authentication with Azure Active Directory (AAD). The audience is not considered when using a shared key. + /// + /// If null, will be assumed. + public QueueAudience? Audience { get; set; } } } diff --git a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs index 73a5ad529eef..dabe13ca291c 100644 --- a/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs +++ b/sdk/storage/Azure.Storage.Queues/src/QueueServiceClient.cs @@ -240,7 +240,9 @@ public QueueServiceClient(Uri serviceUri, AzureSasCredential credential, QueueCl public QueueServiceClient(Uri serviceUri, TokenCredential credential, QueueClientOptions options = default) : this( serviceUri, - credential.AsPolicy(options), + credential.AsPolicy( + string.IsNullOrEmpty(options?.Audience?.ToString()) ? QueueAudience.PublicAudience.CreateDefaultScope() : options.Audience.Value.CreateDefaultScope(), + options), options, sharedKeyCredential: null, sasCredential: null, diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTests.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTests.cs index 8cea4ddaded4..0c99071c7d8c 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueClientTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueClientTests.cs @@ -16,6 +16,7 @@ using Azure.Storage.Queues.Specialized; using Moq.Protected; using Azure.Core.TestFramework; +using Azure.Identity; namespace Azure.Storage.Queues.Test { @@ -164,6 +165,103 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Arrange + await using DisposingQueue test = await GetTestQueueAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(QueueAudience.PublicAudience); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)) + { + QueueName = test.Queue.Name, + }; + + QueueClient aadQueue = InstrumentClient(new QueueClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadQueue.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + await using DisposingQueue test = await GetTestQueueAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(new QueueAudience($"https://{test.Queue.AccountName}.queue.core.windows.net/")); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)) + { + QueueName = test.Queue.Name, + }; + + QueueClient aadQueue = InstrumentClient(new QueueClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadQueue.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + await using DisposingQueue test = await GetTestQueueAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(QueueAudience.GetQueueServiceAccountAudience(test.Queue.AccountName)); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)) + { + QueueName = test.Queue.Name, + }; + + QueueClient aadQueue = InstrumentClient(new QueueClient( + uriBuilder.ToUri(), + Tenants.GetOAuthCredential(), + options)); + + // Assert + bool exists = await aadQueue.ExistsAsync(); + Assert.IsTrue(exists); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Arrange + await using DisposingQueue test = await GetTestQueueAsync(); + + // Act - Create new blob client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(new QueueAudience("https://badaudience.queue.core.windows.net")); + + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)) + { + QueueName = test.Queue.Name, + }; + + QueueClient aadQueue = InstrumentClient(new QueueClient( + uriBuilder.ToUri(), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadQueue.ExistsAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public async Task CreateAsync_WithSharedKey() { diff --git a/sdk/storage/Azure.Storage.Queues/tests/QueueTestBase.cs b/sdk/storage/Azure.Storage.Queues/tests/QueueTestBase.cs index 630c31bb0289..d718bd3cd779 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/QueueTestBase.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/QueueTestBase.cs @@ -51,6 +51,13 @@ public QueueTestBase(bool async, RecordedTestMode? mode = null) public QueueClientOptions GetOptions() => QueuesClientBuilder.GetOptions(); + public QueueClientOptions GetOptionsWithAudience(QueueAudience audience) + { + QueueClientOptions options = QueuesClientBuilder.GetOptions(false); + options.Audience = audience; + return options; + } + public QueueServiceClient GetServiceClient_SharedKey(QueueClientOptions options = default) => InstrumentClient(GetServiceClient_SharedKey_UnInstrumented(options)); diff --git a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs index b4d23988f5d0..9f00fbe5c5a4 100644 --- a/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs +++ b/sdk/storage/Azure.Storage.Queues/tests/ServiceClientTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Azure.Core; using Azure.Core.TestFramework; +using Azure.Identity; using Azure.Storage.Queues.Models; using Azure.Storage.Queues.Tests; using Azure.Storage.Sas; @@ -106,6 +107,77 @@ public void Ctor_AzureSasCredential_VerifyNoSasInUri() e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature")); } + [RecordedTest] + public async Task Ctor_DefaultAudience() + { + // Act - Create new Queue client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(QueueAudience.PublicAudience); + + QueueServiceClient aadService = InstrumentClient(new QueueServiceClient( + new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_CustomAudience() + { + // Arrange + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)); + + // Act - Create new Queue client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(new QueueAudience($"https://{uriBuilder.AccountName}.queue.core.windows.net/")); + + QueueServiceClient aadService = InstrumentClient(new QueueServiceClient( + new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_StorageAccountAudience() + { + // Arrange + QueueUriBuilder uriBuilder = new QueueUriBuilder(new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint)); + + // Act - Create new Queue client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(QueueAudience.GetQueueServiceAccountAudience(uriBuilder.AccountName)); + + QueueServiceClient aadService = InstrumentClient(new QueueServiceClient( + new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint), + Tenants.GetOAuthCredential(), + options)); + + // Assert + Response properties = await aadService.GetPropertiesAsync(); + Assert.IsNotNull(properties); + } + + [RecordedTest] + public async Task Ctor_AudienceError() + { + // Act - Create new Queue client with the OAuth Credential and Audience + QueueClientOptions options = GetOptionsWithAudience(new QueueAudience("https://badaudience.queue.core.windows.net")); + + QueueServiceClient aadContainer = InstrumentClient(new QueueServiceClient( + new Uri(Tenants.TestConfigOAuth.QueueServiceEndpoint), + new MockCredential(), + options)); + + // Assert + await TestHelper.AssertExpectedExceptionAsync( + aadContainer.GetPropertiesAsync(), + e => Assert.AreEqual("InvalidAuthenticationInfo", e.ErrorCode)); + } + [RecordedTest] public async Task GetQueuesAsync() {