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 f77c5542e9..0adefbc4a3 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 @@ -275,6 +275,11 @@ namespace Azure { namespace Storage { namespace Blobs { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** 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 8fd3c4871e..ee129449e6 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,7 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { @@ -261,10 +261,14 @@ namespace Azure { namespace Storage { namespace Sas { + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion - + "\n\n\n\n\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") - + "\n" + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" - + EncryptionScope + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding - + "\n" + ContentLanguage + "\n" + ContentType; + + "\n\n\n\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\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; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( std::vector(stringToSign.begin(), stringToSign.end()), @@ -294,6 +298,12 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -402,10 +412,14 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n\n\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + resource + "\n" + snapshotVersion + "\n" + EncryptionScope + "\n" + CacheControl + "\n" - + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; + + "\n" + userDelegationKey.SignedVersion + "\n\n\n\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\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; } }}} // namespace Azure::Storage::Sas 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 df2fa6c105..fed8532203 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp @@ -184,6 +184,7 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, _internal::WithReplicaStatus(context)); } 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 e497636ed4..4ea51571f1 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 @@ -880,7 +880,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(BlobSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(BlobSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -930,4 +930,67 @@ namespace Azure { namespace Storage { namespace Test { InitStorageClientOptions()); EXPECT_THROW(blobClient2.GetProperties(), StorageException); } + + TEST_F(BlobSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + 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; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto blobServiceClient = Blobs::BlobServiceClient( + m_blobServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + Blobs::Models::UserDelegationKey userDelegationKey; + { + Blobs::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn, options).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.DelegatedUserObjectId = delegatedUserObjectId; + + blobSasBuilder.SetPermissions(Sas::BlobSasPermissions::All); + auto sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Blobs::BlockBlobClient blobClient1( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(blobClient1.Download()); + + { + Blobs::GetUserDelegationKeyOptions options; + // Invalid Tenant Id + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Blobs::BlockBlobClient blobClient2( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + GetTestCredential(), + InitStorageClientOptions()); + EXPECT_THROW(blobClient2.Download(), 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 6bdd9f20c2..54d3f1c154 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 @@ -321,7 +321,7 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.HasValue(), properties.DefaultServiceVersion.HasValue()); - if (downloadedProperties.DefaultServiceVersion.HasValue()) + if (downloadedProperties.DefaultServiceVersion.HasValue() && !m_testContext.IsPlaybackMode()) { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.Value(), diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp index 68350fc818..dfeede6b7e 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp @@ -27,7 +27,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { /** * The version used for the operations to Azure storage services. */ - constexpr static const char* ApiVersion = "2026-02-06"; + constexpr static const char* ApiVersion = "2026-04-06"; } // namespace _detail namespace Models { namespace _detail { 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 d413772136..b52d3a0908 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,7 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { namespace { @@ -226,9 +226,12 @@ namespace Azure { namespace Storage { namespace Sas { + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" - + PreauthorizedAgentObjectId + "\n" + AgentObjectId + "\n" + CorrelationId + "\n" + "\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" - + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n" + + PreauthorizedAgentObjectId + "\n" + AgentObjectId + "\n" + CorrelationId + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? 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; @@ -273,6 +276,12 @@ namespace Azure { namespace Storage { namespace Sas { { builder.AppendQueryParameter("scid", _internal::UrlEncodeQueryParameter(CorrelationId)); } + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -365,10 +374,14 @@ namespace Azure { namespace Storage { namespace Sas { + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + PreauthorizedAgentObjectId + "\n" - + AgentObjectId + "\n" + CorrelationId + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + resource + "\n" + "\n" + EncryptionScope + "\n" + CacheControl + "\n" + ContentDisposition - + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; + + AgentObjectId + "\n" + CorrelationId + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? 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; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp b/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp index 9fa5cee982..695b7b1583 100644 --- a/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp @@ -61,7 +61,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.ContinuationToken.HasValue() && !options.ContinuationToken.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -162,7 +162,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Resource.HasValue() && !options.Resource.Value().ToString().empty()) { request.GetUrl().AppendQueryParameter( @@ -350,7 +350,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Recursive.HasValue()) { request.GetUrl().AppendQueryParameter( @@ -448,7 +448,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -495,7 +495,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-acl", options.Acl.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -548,7 +548,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-undelete-source", options.UndeleteSource.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -599,7 +599,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -698,7 +698,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EncryptionKey.HasValue() && !options.EncryptionKey.Value().empty()) { request.SetHeader("x-ms-encryption-key", options.EncryptionKey.Value()); @@ -782,7 +782,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EncryptionKey.HasValue() && !options.EncryptionKey.Value().empty()) { request.SetHeader("x-ms-encryption-key", options.EncryptionKey.Value()); diff --git a/sdk/storage/azure-storage-files-datalake/swagger/README.md b/sdk/storage/azure-storage-files-datalake/swagger/README.md index c64a73380f..6aeaf8fc5c 100644 --- a/sdk/storage/azure-storage-files-datalake/swagger/README.md +++ b/sdk/storage/azure-storage-files-datalake/swagger/README.md @@ -88,12 +88,12 @@ directive: "name": "ApiVersion", "modelAsString": false }, - "enum": ["2026-02-06"] + "enum": ["2026-04-06"] }; - from: swagger-document where: $.parameters transform: > - $.ApiVersionParameter.enum[0] = "2026-02-06"; + $.ApiVersionParameter.enum[0] = "2026-04-06"; ``` ### Rename Operations 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 60f5c174bc..6650995353 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 @@ -886,7 +886,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(DataLakeSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(DataLakeSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -932,4 +932,61 @@ namespace Azure { namespace Storage { namespace Test { InitStorageClientOptions()); EXPECT_THROW(fileClient2.GetProperties(), StorageException); } + + TEST_F(DataLakeSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + 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; + + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + Files::DataLake::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + Files::DataLake::Models::UserDelegationKey userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn, options).Value; + + std::string fileName = RandomString(); + + auto dataLakeFileSystemClient = *m_fileSystemClient; + auto dataLakeFileClient = dataLakeFileSystemClient.GetFileClient(fileName); + dataLakeFileClient.Create(); + + 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.DelegatedUserObjectId = delegatedUserObjectId; + + fileSasBuilder.SetPermissions(Sas::DataLakeSasPermissions::All); + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::DataLake::DataLakeFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(fileClient1.GetProperties()); + + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn, options).Value; + + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::DataLake::DataLakeFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_THROW(fileClient2.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test 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 e937b2fb6b..09f29797c8 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 @@ -377,6 +377,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** diff --git a/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp b/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp index b0f3d59b35..0f1ba85c45 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp @@ -8,7 +8,7 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { @@ -185,8 +185,11 @@ namespace Azure { namespace Storage { namespace Sas { std::string stringToSign = Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr - + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; @@ -224,6 +227,12 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -307,10 +316,13 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage - + "\n" + ContentType; + + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } }}} // namespace Azure::Storage::Sas 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 3b3a7a63cd..c7d7bc0699 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 @@ -194,6 +194,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, context); } diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp index 6b452fedb2..ee0cf5357b 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp @@ -753,7 +753,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(ShareSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(ShareSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -791,10 +791,12 @@ namespace Azure { namespace Storage { namespace Test { fileSasBuilder.SetPermissions(Sas::ShareFileSasPermissions::All); auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + auto fileOptions = InitStorageClientOptions(); + fileOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; Files::Shares::ShareFileClient fileClient1( AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), GetTestCredential(), - InitStorageClientOptions()); + fileOptions); EXPECT_NO_THROW(fileClient1.GetProperties()); fileSasBuilder.DelegatedUserObjectId = "invalidObjectId"; @@ -802,7 +804,73 @@ namespace Azure { namespace Storage { namespace Test { Files::Shares::ShareFileClient fileClient2( AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), GetTestCredential(), + fileOptions); + EXPECT_THROW(fileClient2.GetProperties(), StorageException); + } + + TEST_F(ShareSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + std::string fileName = RandomString(); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto shareServiceClient = Files::Shares::ShareServiceClient( + m_shareServiceClient->GetUrl(), + GetTestCredential(), InitStorageClientOptions()); + Files::Shares::Models::UserDelegationKey userDelegationKey; + { + Files::Shares::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = shareServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + + Sas::ShareSasBuilder fileSasBuilder; + fileSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + fileSasBuilder.StartsOn = sasStartsOn; + fileSasBuilder.ExpiresOn = sasExpiresOn; + fileSasBuilder.ShareName = m_shareName; + fileSasBuilder.FilePath = fileName; + fileSasBuilder.Resource = Sas::ShareSasResource::File; + fileSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + auto shareClient = *m_shareClient; + auto fileClient = shareClient.GetRootDirectoryClient().GetFileClient(fileName); + fileClient.Create(1); + + auto fileOptions = InitStorageClientOptions(); + fileOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; + fileSasBuilder.SetPermissions(Sas::ShareFileSasPermissions::All); + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::Shares::ShareFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), + endUserCredential, + fileOptions); + EXPECT_NO_THROW(fileClient1.GetProperties()); + + { + Files::Shares::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = shareServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::Shares::ShareFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), + endUserCredential, + fileOptions); EXPECT_THROW(fileClient2.GetProperties(), StorageException); } }}} // namespace Azure::Storage::Test 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 f20f54224a..3a10cf22fc 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 @@ -208,6 +208,11 @@ namespace Azure { namespace Storage { namespace Queues { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** diff --git a/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp b/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp index 72f1ddd8d2..b0e444784f 100644 --- a/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp @@ -8,7 +8,7 @@ #include #include -/* cSpell:ignore skoid, sktid, sduoid */ +/* cSpell:ignore skoid, sktid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { @@ -113,8 +113,11 @@ namespace Azure { namespace Storage { namespace Sas { std::string stringToSign = Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr - + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( @@ -150,6 +153,12 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -204,7 +213,11 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion; + + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion; } }}} // namespace Azure::Storage::Sas 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 480ba9a685..acbcbdb059 100644 --- a/sdk/storage/azure-storage-queues/src/queue_service_client.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_service_client.cpp @@ -203,6 +203,7 @@ namespace Azure { namespace Storage { namespace Queues { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, context); } diff --git a/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp b/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp index ee2e400d84..85e0ee77ac 100644 --- a/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp +++ b/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp @@ -499,7 +499,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(QueueSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(QueueSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -546,4 +546,61 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_THROW(queueClient2.GetProperties(), StorageException); } + TEST_F(QueueSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + 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; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto queueServiceClient = Queues::QueueServiceClient( + m_queueServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + Queues::Models::UserDelegationKey userDelegationKey; + { + Queues::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = queueServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + + Sas::QueueSasBuilder queueSasBuilder; + queueSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + queueSasBuilder.StartsOn = sasStartsOn; + queueSasBuilder.ExpiresOn = sasExpiresOn; + queueSasBuilder.QueueName = m_queueName; + queueSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + auto queueClient = *m_queueClient; + auto accountName = keyCredential->AccountName; + + queueSasBuilder.SetPermissions(Sas::QueueSasPermissions::All); + auto sasToken = queueSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Queues::QueueClient queueClient1( + AppendQueryParameters(Azure::Core::Url(queueClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(queueClient1.GetProperties()); + + { + Queues::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = queueServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = queueSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Queues::QueueClient queueClient2( + AppendQueryParameters(Azure::Core::Url(queueClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_THROW(queueClient2.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test