Add Dynamic Sas support (#6868)

This commit is contained in:
microzchang 2025-12-09 17:51:04 +08:00 committed by microzchang
parent 464b57b2a0
commit 579af3e43b
6 changed files with 308 additions and 14 deletions

View File

@ -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<std::string, std::string> 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<std::string, std::string> RequestQueryParameters;
/**
* @brief Override the value returned for Cache-Control response header..
*/

View File

@ -6,7 +6,9 @@
#include <azure/core/http/http.hpp>
#include <azure/storage/common/crypt.hpp>
/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid */
#include <iostream>
/* 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<std::string, std::string>& 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<std::string, std::string>& 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<std::string, std::string>& 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<uint8_t>(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

View File

@ -920,7 +920,7 @@ namespace Azure { namespace Storage { namespace Test {
AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken),
GetTestCredential(),
InitStorageClientOptions<Blobs::BlobClientOptions>());
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<Blobs::BlobClientOptions>());
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<Blobs::BlobClientOptions>());
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<Blobs::BlobClientOptions>());
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<std::string, std::string> requestHeaders;
requestHeaders["x-ms-range"] = "bytes=0-1023";
requestHeaders["x-ms-range-get-content-md5"] = "true";
std::map<std::string, std::string> 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<Blobs::BlobClientOptions>());
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<Blobs::BlobClientOptions>());
EXPECT_THROW(blobClient2.Download(downloadOptions), StorageException);
}
}}} // namespace Azure::Storage::Test

View File

@ -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<std::string, std::string> 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<std::string, std::string> RequestQueryParameters;
/**
* @brief Override the value returned for Cache-Control response header.
*/

View File

@ -6,7 +6,8 @@
#include <azure/core/http/http.hpp>
#include <azure/storage/common/crypt.hpp>
/* 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<std::string, std::string>& 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<std::string, std::string>& 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<std::string, std::string>& 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<uint8_t>(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

View File

@ -989,4 +989,78 @@ namespace Azure { namespace Storage { namespace Test {
InitStorageClientOptions<Files::DataLake::DataLakeClientOptions>());
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<std::string, std::string> requestHeaders;
requestHeaders["x-ms-range"] = "bytes=0-1023";
requestHeaders["x-ms-upn"] = "true";
std::map<std::string, std::string> 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<Files::DataLake::DataLakeClientOptions>());
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<Files::DataLake::DataLakeClientOptions>());
EXPECT_THROW(fileClient2.Download(downloadOptions), StorageException);
}
}}} // namespace Azure::Storage::Test