diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp index 79ff2210d4..500e9e19ec 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp @@ -262,6 +262,18 @@ namespace Azure { namespace Storage { namespace Sas { */ std::string DelegatedUserObjectId; + /** + * @brief Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + * include these headers and values in the request. + */ + std::map RequestHeaders; + + /** + * @brief Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS + * must include these query parameters and values in the request. + */ + std::map RequestQueryParameters; + /** * @brief Override the value returned for Cache-Control response header.. */ diff --git a/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp b/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp index ee129449e6..b3c2ade702 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp @@ -6,7 +6,9 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid */ +#include + +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid, srh, srq */ namespace Azure { namespace Storage { namespace Sas { @@ -36,6 +38,53 @@ namespace Azure { namespace Storage { namespace Sas { throw std::invalid_argument("Unknown BlobSasResource value."); } } + + std::string ParseRequestQueryParameters( + const std::map& queryParameters) + { + if (queryParameters.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : queryParameters) + { + result += "\n" + pair.first + ":" + pair.second; + } + return result; + } + + std::string ParseRequestHeaders(const std::map& headers) + { + if (headers.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : headers) + { + result += pair.first + ":" + pair.second + "\n"; + } + return result; + } + + std::string ParseRequestKeys(const std::map& map) + { + if (map.empty()) + { + return ""; + } + std::string result; + for (auto it = map.begin(); it != map.end(); ++it) + { + result += it->first; + if (std::next(it) != map.end()) + { + result += ","; + } + } + return result; + } } // namespace void BlobSasBuilder::SetPermissions(BlobContainerSasPermissions permissions) @@ -267,8 +316,9 @@ namespace Azure { namespace Storage { namespace Sas { : "") + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" - + EncryptionScope + "\n\n\n" + CacheControl + "\n" + ContentDisposition + "\n" - + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; + + EncryptionScope + "\n" + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( std::vector(stringToSign.begin(), stringToSign.end()), @@ -309,6 +359,16 @@ namespace Azure { namespace Storage { namespace Sas { builder.AppendQueryParameter( "sduoid", _internal::UrlEncodeQueryParameter(DelegatedUserObjectId)); } + if (!RequestHeaders.empty()) + { + builder.AppendQueryParameter( + "srh", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestHeaders))); + } + if (!RequestQueryParameters.empty()) + { + builder.AppendQueryParameter( + "srq", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestQueryParameters))); + } if (!CacheControl.empty()) { builder.AppendQueryParameter("rscc", _internal::UrlEncodeQueryParameter(CacheControl)); @@ -418,8 +478,9 @@ namespace Azure { namespace Storage { namespace Sas { : "") + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" - + EncryptionScope + "\n\n\n" + CacheControl + "\n" + ContentDisposition + "\n" - + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; + + EncryptionScope + "\n" + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp index 4ea51571f1..81bd689076 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp @@ -920,7 +920,7 @@ namespace Azure { namespace Storage { namespace Test { AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), GetTestCredential(), InitStorageClientOptions()); - EXPECT_NO_THROW(blobClient1.GetProperties()); + EXPECT_NO_THROW(blobClient1.Download()); blobSasBuilder.DelegatedUserObjectId = "invalidObjectId"; sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); @@ -928,7 +928,7 @@ namespace Azure { namespace Storage { namespace Test { AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), GetTestCredential(), InitStorageClientOptions()); - EXPECT_THROW(blobClient2.GetProperties(), StorageException); + EXPECT_THROW(blobClient2.Download(), StorageException); } TEST_F(BlobSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) @@ -993,4 +993,79 @@ namespace Azure { namespace Storage { namespace Test { InitStorageClientOptions()); EXPECT_THROW(blobClient2.Download(), StorageException); } + + TEST_F(BlobSasTest, DISABLED_DynamicSas) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + + Blobs::Models::UserDelegationKey userDelegationKey; + { + auto blobServiceClient = Blobs::BlobServiceClient( + m_blobServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn).Value; + } + + auto blobContainerClient = *m_blobContainerClient; + auto blobClient = *m_blockBlobClient; + const std::string blobName = m_blobName; + + Sas::BlobSasBuilder blobSasBuilder; + blobSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + blobSasBuilder.StartsOn = sasStartsOn; + blobSasBuilder.ExpiresOn = sasExpiresOn; + blobSasBuilder.BlobContainerName = m_containerName; + blobSasBuilder.BlobName = blobName; + blobSasBuilder.Resource = Sas::BlobSasResource::Blob; + + blobSasBuilder.SetPermissions(Sas::BlobSasPermissions::All); + + std::map requestHeaders; + requestHeaders["x-ms-range"] = "bytes=0-1023"; + requestHeaders["x-ms-range-get-content-md5"] = "true"; + + std::map requestQueryParameters; + requestQueryParameters["spr"] = "https,http"; + requestQueryParameters["sks"] = "b"; + + blobSasBuilder.RequestHeaders = requestHeaders; + blobSasBuilder.RequestQueryParameters = requestQueryParameters; + auto sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Blobs::DownloadBlobOptions downloadOptions; + Core::Http::HttpRange range; + range.Offset = 0; + range.Length = 1024; + downloadOptions.Range = range; + downloadOptions.RangeHashAlgorithm = HashAlgorithm::Md5; + + Blobs::BlockBlobClient blobClient1( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_NO_THROW(blobClient1.Download(downloadOptions)); + + requestHeaders["foo$"] = "bar!"; + requestHeaders["company"] = "msft"; + requestHeaders["city"] = "redmond,atlanta,reston"; + + requestQueryParameters["hello$"] = "world!"; + requestQueryParameters["abra"] = "cadabra"; + requestQueryParameters["firstName"] = "john,Tim"; + + blobSasBuilder.RequestHeaders = requestHeaders; + blobSasBuilder.RequestQueryParameters = requestQueryParameters; + + sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Blobs::BlockBlobClient blobClient2( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_THROW(blobClient2.Download(downloadOptions), StorageException); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp index f1cf2aae21..c981eb53f3 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp @@ -249,6 +249,18 @@ namespace Azure { namespace Storage { namespace Sas { */ std::string DelegatedUserObjectId; + /** + * @brief Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + * include these headers and values in the request. + */ + std::map RequestHeaders; + + /** + * @brief Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS + * must include these query parameters and values in the request. + */ + std::map RequestQueryParameters; + /** * @brief Override the value returned for Cache-Control response header. */ diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp index b52d3a0908..15c9770796 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp @@ -6,7 +6,8 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, skdutid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, skdutid, sduoid, + * srh, srq */ namespace Azure { namespace Storage { namespace Sas { namespace { @@ -31,6 +32,53 @@ namespace Azure { namespace Storage { namespace Sas { throw std::invalid_argument("Unknown DataLakeSasResource value."); } } + + std::string ParseRequestQueryParameters( + const std::map& queryParameters) + { + if (queryParameters.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : queryParameters) + { + result += "\n" + pair.first + ":" + pair.second; + } + return result; + } + + std::string ParseRequestHeaders(const std::map& headers) + { + if (headers.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : headers) + { + result += pair.first + ":" + pair.second + "\n"; + } + return result; + } + + std::string ParseRequestKeys(const std::map& map) + { + if (map.empty()) + { + return ""; + } + std::string result; + for (auto it = map.begin(); it != map.end(); ++it) + { + result += it->first; + if (std::next(it) != map.end()) + { + result += ","; + } + } + return result; + } } // namespace void DataLakeSasBuilder::SetPermissions(DataLakeFileSystemSasPermissions permissions) @@ -231,9 +279,10 @@ namespace Azure { namespace Storage { namespace Sas { ? userDelegationKey.SignedDelegatedUserTid.Value() : "") + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" - + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n\n\n" - + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage - + "\n" + ContentType; + + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n" + + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( std::vector(stringToSign.begin(), stringToSign.end()), @@ -292,6 +341,16 @@ namespace Azure { namespace Storage { namespace Sas { builder.AppendQueryParameter( "sdd", _internal::UrlEncodeQueryParameter(std::to_string(DirectoryDepth.Value()))); } + if (!RequestHeaders.empty()) + { + builder.AppendQueryParameter( + "srh", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestHeaders))); + } + if (!RequestQueryParameters.empty()) + { + builder.AppendQueryParameter( + "srq", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestQueryParameters))); + } if (!CacheControl.empty()) { builder.AppendQueryParameter("rscc", _internal::UrlEncodeQueryParameter(CacheControl)); @@ -379,9 +438,10 @@ namespace Azure { namespace Storage { namespace Sas { ? userDelegationKey.SignedDelegatedUserTid.Value() : "") + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" - + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n\n\n" - + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage - + "\n" + ContentType; + + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n" + + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp index 6650995353..6f81961ae1 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp @@ -989,4 +989,78 @@ namespace Azure { namespace Storage { namespace Test { InitStorageClientOptions()); EXPECT_THROW(fileClient2.GetProperties(), StorageException); } + + TEST_F(DataLakeSasTest, DISABLED_DynamicSas) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential = _internal::ParseConnectionString(AdlsGen2ConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + + Files::DataLake::Models::UserDelegationKey userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn).Value; + + std::string fileName = RandomString(); + + auto dataLakeFileSystemClient = *m_fileSystemClient; + auto dataLakeFileClient = dataLakeFileSystemClient.GetFileClient(fileName); + dataLakeFileClient.Create(); + auto buffer = RandomBuffer(1024); + auto stream = Azure::Core::IO::MemoryBodyStream(buffer); + Files::DataLake::AppendFileOptions appendOptions; + appendOptions.Flush = true; + dataLakeFileClient.Append(stream, 0, appendOptions); + + Sas::DataLakeSasBuilder fileSasBuilder; + fileSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + fileSasBuilder.StartsOn = sasStartsOn; + fileSasBuilder.ExpiresOn = sasExpiresOn; + fileSasBuilder.FileSystemName = m_fileSystemName; + fileSasBuilder.Path = fileName; + fileSasBuilder.Resource = Sas::DataLakeSasResource::File; + + fileSasBuilder.SetPermissions(Sas::DataLakeSasPermissions::All); + + std::map requestHeaders; + requestHeaders["x-ms-range"] = "bytes=0-1023"; + requestHeaders["x-ms-upn"] = "true"; + + std::map requestQueryParameters; + requestQueryParameters["spr"] = "https,http"; + requestQueryParameters["sks"] = "b"; + + fileSasBuilder.RequestHeaders = requestHeaders; + fileSasBuilder.RequestQueryParameters = requestQueryParameters; + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::DataLake::DownloadFileOptions downloadOptions; + Core::Http::HttpRange range; + range.Offset = 0; + range.Length = 1024; + downloadOptions.Range = range; + downloadOptions.IncludeUserPrincipalName = true; + + Files::DataLake::DataLakeFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_NO_THROW(fileClient1.Download(downloadOptions)); + + requestHeaders["foo$"] = "bar!"; + requestHeaders["company"] = "msft"; + requestHeaders["city"] = "redmond,atlanta,reston"; + + requestQueryParameters["hello$"] = "world!"; + requestQueryParameters["abra"] = "cadabra"; + requestQueryParameters["firstName"] = "john,Tim"; + + fileSasBuilder.RequestHeaders = requestHeaders; + fileSasBuilder.RequestQueryParameters = requestQueryParameters; + + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::DataLake::DataLakeFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_THROW(fileClient2.Download(downloadOptions), StorageException); + } }}} // namespace Azure::Storage::Test