diff --git a/sdk/storage/assets.json b/sdk/storage/assets.json index cfaf0d4b1..f6fc7b570 100644 --- a/sdk/storage/assets.json +++ b/sdk/storage/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "cpp", "TagPrefix": "cpp/storage", - "Tag": "cpp/storage_a5249cec25" + "Tag": "cpp/storage_e44851d82e" } diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp index 2378f2407..d016ed076 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp @@ -6,6 +6,7 @@ #include "azure/storage/blobs/rest_client.hpp" #include +#include #include #include #include @@ -20,6 +21,35 @@ namespace Azure { namespace Storage { namespace Blobs { + namespace Models { + + /** + * @brief Audiences available for Blobs + * + */ + class BlobAudience final : public Azure::Core::_internal::ExtendableEnumeration { + public: + /** + * @brief Construct a new BlobAudience object + * + * @param blobAudience 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: See + * https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory + */ + explicit BlobAudience(std::string blobAudience) + : ExtendableEnumeration(std::move(blobAudience)) + { + } + + /** + * @brief Default Audience. Use to acquire a token for authorizing requests to any Azure + * Storage account. + */ + AZ_STORAGE_BLOBS_DLLEXPORT const static BlobAudience PublicAudience; + }; + } // namespace Models + /** * @brief Specifies access conditions for a container. */ @@ -165,6 +195,13 @@ namespace Azure { namespace Storage { namespace Blobs { * to prompt a challenge in order to discover the correct tenant for the resource. */ bool EnableTenantDiscovery = false; + + /** + * The Audience to use for authentication with Azure Active Directory (AAD). + * #Azure::Storage::Blobs::Models::BlobAudience::PublicAudience will be assumed if Audience is + * not set. + */ + Azure::Nullable Audience; }; /** diff --git a/sdk/storage/azure-storage-blobs/src/blob_client.cpp b/sdk/storage/azure-storage-blobs/src/blob_client.cpp index 66c67b877..e24e23f3d 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_client.cpp @@ -86,7 +86,9 @@ namespace Azure { namespace Storage { namespace Blobs { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::BlobAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-blobs/src/blob_container_client.cpp b/sdk/storage/azure-storage-blobs/src/blob_container_client.cpp index 39d3313cc..2b2a27646 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_container_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_container_client.cpp @@ -169,7 +169,9 @@ namespace Azure { namespace Storage { namespace Blobs { std::unique_ptr tokenAuthPolicy; { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::BlobAudience::PublicAudience.ToString()); tokenAuthPolicy = std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery); perRetryPolicies.emplace_back(tokenAuthPolicy->Clone()); diff --git a/sdk/storage/azure-storage-blobs/src/blob_options.cpp b/sdk/storage/azure-storage-blobs/src/blob_options.cpp index 36495fb77..8f99b9245 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_options.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_options.cpp @@ -5,6 +5,10 @@ namespace Azure { namespace Storage { namespace Blobs { + namespace Models { + const BlobAudience BlobAudience::PublicAudience(Azure::Storage::_internal::StorageScope); + } // namespace Models + BlobQueryInputTextOptions BlobQueryInputTextOptions::CreateCsvTextOptions( const std::string& recordSeparator, const std::string& columnSeparator, diff --git a/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp b/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp index d1cac1179..794a9a623 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp @@ -82,7 +82,9 @@ namespace Azure { namespace Storage { namespace Blobs { std::unique_ptr tokenAuthPolicy; { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::BlobAudience::PublicAudience.ToString()); tokenAuthPolicy = std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery); perRetryPolicies.emplace_back(tokenAuthPolicy->Clone()); diff --git a/sdk/storage/azure-storage-blobs/test/ut/bearer_token_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/bearer_token_test.cpp index 9aefa9676..b71f6f9bf 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/bearer_token_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/bearer_token_test.cpp @@ -49,6 +49,18 @@ namespace Azure { namespace Storage { namespace Test { clientOptions); EXPECT_NO_THROW(blobClient.GetProperties()); + // With custom audience + auto blobUrl = Azure::Core::Url(m_blockBlobClient->GetUrl()); + clientOptions.Audience = Blobs::Models::BlobAudience( + blobUrl.GetScheme() + "://" + blobUrl.GetHost() + "/.default"); + blobClient = Blobs::BlobClient( + m_blockBlobClient->GetUrl(), + std::make_shared( + "", AadClientId(), AadClientSecret(), options), + clientOptions); + EXPECT_NO_THROW(blobClient.GetProperties()); + clientOptions.Audience.Reset(); + // With error tenantId clientOptions.EnableTenantDiscovery = true; options.AdditionallyAllowedTenants = {"*"}; diff --git a/sdk/storage/azure-storage-blobs/test/ut/blob_container_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/blob_container_client_test.cpp index 8ad6a7409..0777c5a4c 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/blob_container_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/blob_container_client_test.cpp @@ -1439,4 +1439,33 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_FALSE(downloadResponse.Details.ObjectReplicationDestinationPolicyId.Value().empty()); } } + + TEST_F(BlobContainerClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto containerClient + = Blobs::BlobContainerClient(m_blobContainerClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(containerClient.GetProperties()); + + // custom audience + auto containerUrl = Azure::Core::Url(containerClient.GetUrl()); + clientOptions.Audience = Blobs::Models::BlobAudience( + containerUrl.GetScheme() + "://" + containerUrl.GetHost() + "/.default"); + containerClient + = Blobs::BlobContainerClient(m_blobContainerClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(containerClient.GetProperties()); + + // error audience + clientOptions.Audience = Blobs::Models::BlobAudience("https://disk.compute.azure.com/.default"); + containerClient + = Blobs::BlobContainerClient(m_blobContainerClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(containerClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp index 958d90591..ba8edb3d4 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp @@ -497,4 +497,33 @@ namespace Azure { namespace Storage { namespace Test { auto destContainerClient2 = serviceClient.GetBlobContainerClient(destContainerName2); destContainerClient2.Delete(); } + + TEST_F(BlobServiceClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto serviceClient + = Blobs::BlobServiceClient(m_blobServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(serviceClient.GetProperties()); + + // custom audience + auto serviceUrl = Azure::Core::Url(serviceClient.GetUrl()); + clientOptions.Audience = Blobs::Models::BlobAudience( + serviceUrl.GetScheme() + "://" + serviceUrl.GetHost() + "/.default"); + serviceClient + = Blobs::BlobServiceClient(m_blobServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(serviceClient.GetProperties()); + + // error audience + clientOptions.Audience = Blobs::Models::BlobAudience("https://disk.compute.azure.com/.default"); + serviceClient + = Blobs::BlobServiceClient(m_blobServiceClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(serviceClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp index 5fac79ccc..1f9cf3213 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp @@ -2026,4 +2026,32 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_EQ(properties.CopyStatus.Value(), Blobs::Models::CopyStatus::Aborted); } + TEST_F(BlockBlobClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto blockBlobClient + = Blobs::BlockBlobClient(m_blockBlobClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(blockBlobClient.GetProperties()); + + // custom audience + auto blobUrl = Azure::Core::Url(blockBlobClient.GetUrl()); + clientOptions.Audience = Blobs::Models::BlobAudience( + blobUrl.GetScheme() + "://" + blobUrl.GetHost() + "/.default"); + blockBlobClient + = Blobs::BlockBlobClient(m_blockBlobClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(blockBlobClient.GetProperties()); + + // error audience + clientOptions.Audience = Blobs::Models::BlobAudience("https://disk.compute.azure.com/.default"); + blockBlobClient + = Blobs::BlockBlobClient(m_blockBlobClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(blockBlobClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/CMakeLists.txt b/sdk/storage/azure-storage-files-datalake/CMakeLists.txt index 0aed0dad2..d956c0671 100644 --- a/sdk/storage/azure-storage-files-datalake/CMakeLists.txt +++ b/sdk/storage/azure-storage-files-datalake/CMakeLists.txt @@ -65,6 +65,7 @@ set( src/datalake_file_client.cpp src/datalake_file_system_client.cpp src/datalake_lease_client.cpp + src/datalake_options.cpp src/datalake_path_client.cpp src/datalake_responses.cpp src/datalake_sas_builder.cpp diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp index 3b9b86381..125d2ebe9 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp @@ -78,6 +78,33 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { */ static std::string SerializeAcls(const std::vector& aclsArray); }; + + /** + * @brief Audiences available for Blobs + * + */ + class DataLakeAudience final + : public Azure::Core::_internal::ExtendableEnumeration { + public: + /** + * @brief Construct a new DataLakeAudience object + * + * @param dataLakeAudience 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: See + * https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory + */ + explicit DataLakeAudience(std::string dataLakeAudience) + : ExtendableEnumeration(std::move(dataLakeAudience)) + { + } + + /** + * @brief Default Audience. Use to acquire a token for authorizing requests to any Azure + * Storage account. + */ + AZ_STORAGE_FILES_DATALAKE_DLLEXPORT const static DataLakeAudience PublicAudience; + }; } // namespace Models using DownloadFileToOptions = Blobs::DownloadBlobToOptions; @@ -143,6 +170,13 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * to prompt a challenge in order to discover the correct tenant for the resource. */ bool EnableTenantDiscovery = false; + + /** + * The Audience to use for authentication with Azure Active Directory (AAD). + * #Azure::Storage::Files::DataLake::Models::DataLakeAudience::PublicAudience will be assumed if + * Audience is not set. + */ + Azure::Nullable Audience; }; /** diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_file_system_client.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_file_system_client.cpp index 17cb1ebb6..999c2ffb5 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_file_system_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_file_system_client.cpp @@ -97,7 +97,9 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::DataLakeAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_options.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_options.cpp new file mode 100644 index 000000000..3836910ee --- /dev/null +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_options.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/storage/files/datalake/datalake_options.hpp" + +namespace Azure { namespace Storage { namespace Files { namespace DataLake { namespace Models { + + const DataLakeAudience DataLakeAudience::PublicAudience(Azure::Storage::_internal::StorageScope); + +}}}}} // namespace Azure::Storage::Files::DataLake::Models diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp index 67f2d6992..b2d5b6c8b 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp @@ -95,7 +95,9 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::DataLakeAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_service_client.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_service_client.cpp index 096525605..6e9d7ce2d 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_service_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_service_client.cpp @@ -91,7 +91,9 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::DataLakeAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp index ced748cb9..4b3eef46f 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp @@ -98,6 +98,10 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { nam blobOptions.ApiVersion = options.ApiVersion; blobOptions.CustomerProvidedKey = options.CustomerProvidedKey; blobOptions.EnableTenantDiscovery = options.EnableTenantDiscovery; + if (options.Audience.HasValue()) + { + blobOptions.Audience = Blobs::Models::BlobAudience(options.Audience.Value().ToString()); + } return blobOptions; } diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_system_client_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_system_client_test.cpp index 286f8865e..ae1269cb5 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_system_client_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_system_client_test.cpp @@ -907,4 +907,33 @@ namespace Azure { namespace Storage { namespace Test { } } + TEST_F(DataLakeFileSystemClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto fileSystemClient = Files::DataLake::DataLakeFileSystemClient( + m_fileSystemClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(fileSystemClient.GetProperties()); + + // custom audience + auto fileSystemUrl = Azure::Core::Url(fileSystemClient.GetUrl()); + clientOptions.Audience = Files::DataLake::Models::DataLakeAudience( + fileSystemUrl.GetScheme() + "://" + fileSystemUrl.GetHost() + "/.default"); + fileSystemClient = Files::DataLake::DataLakeFileSystemClient( + m_fileSystemClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(fileSystemClient.GetProperties()); + + // error audience + clientOptions.Audience + = Files::DataLake::Models::DataLakeAudience("https://disk.compute.azure.com/.default"); + fileSystemClient = Files::DataLake::DataLakeFileSystemClient( + m_fileSystemClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(fileSystemClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_path_client_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_path_client_test.cpp index 696a658bc..7089bd872 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_path_client_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_path_client_test.cpp @@ -479,4 +479,34 @@ namespace Azure { namespace Storage { namespace Test { ASSERT_TRUE(properties.Group.HasValue()); ASSERT_TRUE(properties.Permissions.HasValue()); } + + TEST_F(DataLakePathClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto pathClient + = Files::DataLake::DataLakePathClient(m_pathClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(pathClient.GetProperties()); + + // custom audience + auto pathUrl = Azure::Core::Url(pathClient.GetUrl()); + clientOptions.Audience = Files::DataLake::Models::DataLakeAudience( + pathUrl.GetScheme() + "://" + pathUrl.GetHost() + "/.default"); + pathClient + = Files::DataLake::DataLakePathClient(m_pathClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(pathClient.GetProperties()); + + // error audience + clientOptions.Audience + = Files::DataLake::Models::DataLakeAudience("https://disk.compute.azure.com/.default"); + pathClient + = Files::DataLake::DataLakePathClient(m_pathClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(pathClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp index 4de20e026..94456465f 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp @@ -343,4 +343,33 @@ namespace Azure { namespace Storage { namespace Test { auto res = m_dataLakeServiceClient->SetProperties(originalProperties); } + TEST_F(DataLakeServiceClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto serviceClient = Files::DataLake::DataLakeServiceClient( + m_dataLakeServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(serviceClient.GetProperties()); + + // custom audience + auto fileSystemUrl = Azure::Core::Url(serviceClient.GetUrl()); + clientOptions.Audience = Files::DataLake::Models::DataLakeAudience( + fileSystemUrl.GetScheme() + "://" + fileSystemUrl.GetHost() + "/.default"); + serviceClient = Files::DataLake::DataLakeServiceClient( + m_dataLakeServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(serviceClient.GetProperties()); + + // error audience + clientOptions.Audience + = Files::DataLake::Models::DataLakeAudience("https://disk.compute.azure.com/.default"); + serviceClient = Files::DataLake::DataLakeServiceClient( + m_dataLakeServiceClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(serviceClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-shares/CMakeLists.txt b/sdk/storage/azure-storage-files-shares/CMakeLists.txt index 2f95d2e05..33071a9c0 100644 --- a/sdk/storage/azure-storage-files-shares/CMakeLists.txt +++ b/sdk/storage/azure-storage-files-shares/CMakeLists.txt @@ -64,6 +64,7 @@ set( src/share_directory_client.cpp src/share_file_client.cpp src/share_lease_client.cpp + src/share_options.cpp src/share_responses.cpp src/share_sas_builder.cpp src/share_service_client.cpp diff --git a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp index 75f7db4ef..86684096b 100644 --- a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp +++ b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp @@ -6,6 +6,7 @@ #include "azure/storage/files/shares/rest_client.hpp" #include +#include #include #include @@ -17,6 +18,36 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { + namespace Models { + + /** + * @brief Audiences available for Blobs + * + */ + class ShareAudience final + : public Azure::Core::_internal::ExtendableEnumeration { + public: + /** + * @brief Construct a new ShareAudience object + * + * @param shareAudience 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: See + * https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory + */ + explicit ShareAudience(std::string shareAudience) + : ExtendableEnumeration(std::move(shareAudience)) + { + } + + /** + * @brief Default Audience. Use to acquire a token for authorizing requests to any Azure + * Storage account. + */ + AZ_STORAGE_FILES_SHARES_DLLEXPORT const static ShareAudience PublicAudience; + }; + } // namespace Models + /** * @brief Client options used to initialize share clients. */ @@ -46,6 +77,13 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * request. This is currently required when using token authentication. */ Nullable ShareTokenIntent; + + /** + * The Audience to use for authentication with Azure Active Directory (AAD). + * #Azure::Storage::Files::Shares::Models::ShareAudience::PublicAudience will be assumed if + * Audience is not set. + */ + Azure::Nullable Audience; }; /** diff --git a/sdk/storage/azure-storage-files-shares/src/share_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_client.cpp index 42611b95c..cf8b74bb4 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_client.cpp @@ -78,7 +78,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::ShareAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique( credential, tokenContext)); diff --git a/sdk/storage/azure-storage-files-shares/src/share_directory_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_directory_client.cpp index 93c759974..7b62db02d 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_directory_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_directory_client.cpp @@ -80,7 +80,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::ShareAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique( credential, tokenContext)); diff --git a/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp index 7a8ab02e2..e7d338729 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp @@ -85,7 +85,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::ShareAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique( credential, tokenContext)); diff --git a/sdk/storage/azure-storage-files-shares/src/share_options.cpp b/sdk/storage/azure-storage-files-shares/src/share_options.cpp new file mode 100644 index 000000000..098332f44 --- /dev/null +++ b/sdk/storage/azure-storage-files-shares/src/share_options.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/storage/files/shares/share_options.hpp" + +namespace Azure { namespace Storage { namespace Files { namespace Shares { namespace Models { + + const ShareAudience ShareAudience::PublicAudience(Azure::Storage::_internal::StorageScope); + +}}}}} // namespace Azure::Storage::Files::Shares::Models diff --git a/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp index 9985546d5..101604dc1 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp @@ -75,7 +75,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::ShareAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique( credential, tokenContext)); diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_client_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_client_test.cpp index 5e809aa2d..fe5a6d802 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_client_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_client_test.cpp @@ -696,4 +696,38 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_TRUE(client1.GetUrl().find("snapshot=" + timestamp1) == std::string::npos); EXPECT_TRUE(client1.GetUrl().find("snapshot=" + timestamp2) == std::string::npos); } + + TEST_F(FileShareClientTest, Audience_PLAYBACKONLY_) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + clientOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; + std::string 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)"; + + // default audience + auto shareClient + = Files::Shares::ShareClient(m_shareClient->GetUrl(), credential, clientOptions); + Files::Shares::Models::CreateSharePermissionResult created; + EXPECT_NO_THROW(created = shareClient.CreatePermission(permission).Value); + EXPECT_NO_THROW(shareClient.GetPermission(created.FilePermissionKey)); + + // custom audience + auto shareUrl = Azure::Core::Url(shareClient.GetUrl()); + clientOptions.Audience = Files::Shares::Models::ShareAudience( + shareUrl.GetScheme() + "://" + shareUrl.GetHost() + "/.default"); + shareClient = Files::Shares::ShareClient(m_shareClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(shareClient.GetPermission(created.FilePermissionKey)); + + // error audience + clientOptions.Audience + = Files::Shares::Models::ShareAudience("https://disk.compute.azure.com/.default"); + shareClient = Files::Shares::ShareClient(m_shareClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(shareClient.GetPermission(created.FilePermissionKey), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_directory_client_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_directory_client_test.cpp index c07ec1bf0..887f8a7a9 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_directory_client_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_directory_client_test.cpp @@ -1205,4 +1205,35 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_TRUE(client1.GetUrl().find("snapshot=" + timestamp1) == std::string::npos); EXPECT_TRUE(client1.GetUrl().find("snapshot=" + timestamp2) == std::string::npos); } + + TEST_F(FileShareDirectoryClientTest, Audience_PLAYBACKONLY_) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + clientOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; + + // default audience + auto directoryClient = Files::Shares::ShareDirectoryClient( + m_fileShareDirectoryClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(directoryClient.GetProperties()); + + // custom audience + auto directoryUrl = Azure::Core::Url(directoryClient.GetUrl()); + clientOptions.Audience = Files::Shares::Models::ShareAudience( + directoryUrl.GetScheme() + "://" + directoryUrl.GetHost() + "/.default"); + directoryClient = Files::Shares::ShareDirectoryClient( + m_fileShareDirectoryClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(directoryClient.GetProperties()); + + // error audience + clientOptions.Audience + = Files::Shares::Models::ShareAudience("https://disk.compute.azure.com/.default"); + directoryClient = Files::Shares::ShareDirectoryClient( + m_fileShareDirectoryClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(directoryClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp index eb503e727..6cafc8df7 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp @@ -1692,6 +1692,51 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_TRUE(client1.GetUrl().find("snapshot=" + timestamp2) == std::string::npos); } + TEST_F(FileShareFileClientTest, Audience_PLAYBACKONLY_) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + clientOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; + + // default audience + auto fileClient + = Files::Shares::ShareFileClient(m_fileClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(fileClient.GetProperties()); + + // custom audience + auto fileUrl = Azure::Core::Url(fileClient.GetUrl()); + clientOptions.Audience = Files::Shares::Models::ShareAudience( + fileUrl.GetScheme() + "://" + fileUrl.GetHost() + "/.default"); + fileClient = Files::Shares::ShareFileClient(m_fileClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(fileClient.GetProperties()); + + fileClient = Files::Shares::ShareServiceClient( + m_shareServiceClient->GetUrl(), credential, clientOptions) + .GetShareClient(m_shareName) + .GetRootDirectoryClient() + .GetSubdirectoryClient(m_directoryName) + .GetFileClient(m_fileName); + EXPECT_NO_THROW(fileClient.GetProperties()); + + // error audience + clientOptions.Audience + = Files::Shares::Models::ShareAudience("https://disk.compute.azure.com/.default"); + fileClient = Files::Shares::ShareFileClient(m_fileClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(fileClient.GetProperties(), StorageException); + + fileClient = Files::Shares::ShareServiceClient( + m_shareServiceClient->GetUrl(), credential, clientOptions) + .GetShareClient(m_shareName) + .GetRootDirectoryClient() + .GetSubdirectoryClient(m_directoryName) + .GetFileClient(m_fileName); + EXPECT_THROW(fileClient.GetProperties(), StorageException); + } + TEST(ShareFileHandleAccessRightsTest, ShareFileHandleAccessRights) { Files::Shares::Models::ShareFileHandleAccessRights accessRightsA diff --git a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp index 059887969..f96f05b85 100644 --- a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp +++ b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp @@ -11,12 +11,42 @@ #include "azure/storage/queues/rest_client.hpp" #include +#include #include #include #include namespace Azure { namespace Storage { namespace Queues { + namespace Models { + + /** + * @brief Audiences available for Blobs + * + */ + class QueueAudience final + : public Azure::Core::_internal::ExtendableEnumeration { + public: + /** + * @brief Construct a new QueueAudience object + * + * @param queueAudience 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: See + * https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory + */ + explicit QueueAudience(std::string queueAudience) + : ExtendableEnumeration(std::move(queueAudience)) + { + } + + /** + * @brief Default Audience. Use to acquire a token for authorizing requests to any Azure + * Storage account. + */ + AZ_STORAGE_QUEUES_DLLEXPORT const static QueueAudience PublicAudience; + }; + } // namespace Models /** * @brief API version for Storage Queue service. @@ -91,6 +121,13 @@ namespace Azure { namespace Storage { namespace Queues { * to prompt a challenge in order to discover the correct tenant for the resource. */ bool EnableTenantDiscovery = false; + + /** + * The Audience to use for authentication with Azure Active Directory (AAD). + * #Azure::Storage::Queues::Models::QueueAudience::PublicAudience will be assumed if + * Audience is not set. + */ + Azure::Nullable Audience; }; /** diff --git a/sdk/storage/azure-storage-queues/src/queue_client.cpp b/sdk/storage/azure-storage-queues/src/queue_client.cpp index 0f4477ca5..cd2d9e20f 100644 --- a/sdk/storage/azure-storage-queues/src/queue_client.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_client.cpp @@ -74,7 +74,9 @@ namespace Azure { namespace Storage { namespace Queues { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::QueueAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-queues/src/queue_options.cpp b/sdk/storage/azure-storage-queues/src/queue_options.cpp index c179990dd..8f7421c4c 100644 --- a/sdk/storage/azure-storage-queues/src/queue_options.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_options.cpp @@ -5,6 +5,11 @@ namespace Azure { namespace Storage { namespace Queues { + namespace Models { + + const QueueAudience QueueAudience::PublicAudience(Azure::Storage::_internal::StorageScope); + } // namespace Models + const ServiceVersion ServiceVersion::V2018_03_28(std::string("2018-03-28")); const ServiceVersion ServiceVersion::V2019_12_12(std::string("2019-12-12")); const std::chrono::seconds EnqueueMessageOptions::MessageNeverExpires{-1}; diff --git a/sdk/storage/azure-storage-queues/src/queue_service_client.cpp b/sdk/storage/azure-storage-queues/src/queue_service_client.cpp index 39f2ef591..2cdbba9a6 100644 --- a/sdk/storage/azure-storage-queues/src/queue_service_client.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_service_client.cpp @@ -72,7 +72,9 @@ namespace Azure { namespace Storage { namespace Queues { perRetryPolicies.emplace_back(std::make_unique<_internal::StoragePerRetryPolicy>()); { Azure::Core::Credentials::TokenRequestContext tokenContext; - tokenContext.Scopes.emplace_back(_internal::StorageScope); + tokenContext.Scopes.emplace_back( + options.Audience.HasValue() ? options.Audience.Value().ToString() + : Models::QueueAudience::PublicAudience.ToString()); perRetryPolicies.emplace_back( std::make_unique<_internal::StorageBearerTokenAuthenticationPolicy>( credential, tokenContext, options.EnableTenantDiscovery)); diff --git a/sdk/storage/azure-storage-queues/test/ut/queue_client_test.cpp b/sdk/storage/azure-storage-queues/test/ut/queue_client_test.cpp index 86a598fc2..e069ce4a7 100644 --- a/sdk/storage/azure-storage-queues/test/ut/queue_client_test.cpp +++ b/sdk/storage/azure-storage-queues/test/ut/queue_client_test.cpp @@ -233,4 +233,40 @@ namespace Azure { namespace Storage { namespace Test { queueClient.Delete(); } + TEST_F(QueueClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto queueClient = Queues::QueueClient(m_queueClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(queueClient.GetProperties()); + + // custom audience + auto queueUrl = Azure::Core::Url(queueClient.GetUrl()); + clientOptions.Audience = Queues::Models::QueueAudience( + queueUrl.GetScheme() + "://" + queueUrl.GetHost() + "/.default"); + queueClient = Queues::QueueClient(m_queueClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(queueClient.GetProperties()); + + queueClient + = Queues::QueueServiceClient(m_queueServiceClient->GetUrl(), credential, clientOptions) + .GetQueueClient(m_queueName); + EXPECT_NO_THROW(queueClient.GetProperties()); + + // error audience + clientOptions.Audience + = Queues::Models::QueueAudience("https://disk.compute.azure.com/.default"); + queueClient = Queues::QueueClient(m_queueClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(queueClient.GetProperties(), StorageException); + + queueClient + = Queues::QueueServiceClient(m_queueServiceClient->GetUrl(), credential, clientOptions) + .GetQueueClient(m_queueName); + EXPECT_THROW(queueClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-queues/test/ut/queue_service_client_test.cpp b/sdk/storage/azure-storage-queues/test/ut/queue_service_client_test.cpp index 612c3e8eb..32a514503 100644 --- a/sdk/storage/azure-storage-queues/test/ut/queue_service_client_test.cpp +++ b/sdk/storage/azure-storage-queues/test/ut/queue_service_client_test.cpp @@ -314,4 +314,33 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_THROW(queueClient.Value.GetProperties(), StorageException); } + TEST_F(QueueServiceClientTest, Audience) + { + auto credential = std::make_shared( + AadTenantId(), + AadClientId(), + AadClientSecret(), + InitStorageClientOptions()); + auto clientOptions = InitStorageClientOptions(); + + // default audience + auto queueServiceClient + = Queues::QueueServiceClient(m_queueServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(queueServiceClient.GetProperties()); + + // custom audience + auto queueUrl = Azure::Core::Url(queueServiceClient.GetUrl()); + clientOptions.Audience = Queues::Models::QueueAudience( + queueUrl.GetScheme() + "://" + queueUrl.GetHost() + "/.default"); + queueServiceClient + = Queues::QueueServiceClient(m_queueServiceClient->GetUrl(), credential, clientOptions); + EXPECT_NO_THROW(queueServiceClient.GetProperties()); + + // error audience + clientOptions.Audience + = Queues::Models::QueueAudience("https://disk.compute.azure.com/.default"); + queueServiceClient + = Queues::QueueServiceClient(m_queueServiceClient->GetUrl(), credential, clientOptions); + EXPECT_THROW(queueServiceClient.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test