diff --git a/src/Storage/Storage.Management.Test/Storage.Management.Test.csproj b/src/Storage/Storage.Management.Test/Storage.Management.Test.csproj index 8269f9ae3cc8..850f7a4e1db5 100644 --- a/src/Storage/Storage.Management.Test/Storage.Management.Test.csproj +++ b/src/Storage/Storage.Management.Test/Storage.Management.Test.csproj @@ -1,4 +1,4 @@ - + Management.Storage @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/src/Storage/Storage.Management/ChangeLog.md b/src/Storage/Storage.Management/ChangeLog.md index 4b6506de9d2c..83addf2b0583 100644 --- a/src/Storage/Storage.Management/ChangeLog.md +++ b/src/Storage/Storage.Management/ChangeLog.md @@ -18,6 +18,14 @@ - Additional information about change #1 --> ## Upcoming Release +* Supported user delegation SAS + bearer token authentication in Blob, DatalakeGen2, File, Queue service. + - `New-AzStorageContext` + - `New-AzStorageBlobSASToken` + - `New-AzStorageContainerSASToken` + - `New-AzDataLakeGen2SASToken` + - `New-AzStorageFileSASToken` + - `New-AzStorageQueueSASToken` + - `New-AzStorageShareSASToken` * Upgraded management plane SDK and auto generated cmdlets to base on API spec generated by TSP * Updated Azure.Core from 1.47.3 to 1.50.0 diff --git a/src/Storage/Storage.Management/help/New-AzDataLakeGen2SasToken.md b/src/Storage/Storage.Management/help/New-AzDataLakeGen2SasToken.md index a37d7bfee9d4..f1beaa655f2b 100644 --- a/src/Storage/Storage.Management/help/New-AzDataLakeGen2SasToken.md +++ b/src/Storage/Storage.Management/help/New-AzDataLakeGen2SasToken.md @@ -15,17 +15,17 @@ Generates a SAS token for Azure DatalakeGen2 item. ### ReceiveManual (Default) ``` New-AzDataLakeGen2SasToken [-FileSystem] [-Path ] [-Permission ] - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-EncryptionScope ] [-FullUri] [-Context ] - [-DefaultProfile ] [] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-EncryptionScope ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ### ItemPipeline ``` New-AzDataLakeGen2SasToken -InputObject [-Permission ] - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-EncryptionScope ] [-FullUri] [-Context ] - [-DefaultProfile ] [] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-EncryptionScope ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ## DESCRIPTION @@ -79,6 +79,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -EncryptionScope Encryption scope to use when sending requests authorized with this SAS URI. diff --git a/src/Storage/Storage.Management/help/New-AzStorageBlobSASToken.md b/src/Storage/Storage.Management/help/New-AzStorageBlobSASToken.md index 74691e60b783..9c10a53f1e0b 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageBlobSASToken.md +++ b/src/Storage/Storage.Management/help/New-AzStorageBlobSASToken.md @@ -16,36 +16,36 @@ Generates a SAS token for an Azure storage blob. ### BlobNameWithPermission (Default) ``` New-AzStorageBlobSASToken [-Container] [-Blob] [-Permission ] - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] - [-DefaultProfile ] [-WhatIf] [-Confirm] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-FullUri] [-EncryptionScope ] + [-Context ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ### BlobPipelineWithPolicy ``` New-AzStorageBlobSASToken -CloudBlob [-BlobBaseClient ] -Policy - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] - [-DefaultProfile ] [-WhatIf] [-Confirm] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-FullUri] [-EncryptionScope ] + [-Context ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ### BlobPipelineWithPermission ``` New-AzStorageBlobSASToken -CloudBlob [-BlobBaseClient ] [-Permission ] - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] - [-DefaultProfile ] [-WhatIf] [-Confirm] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-FullUri] [-EncryptionScope ] + [-Context ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ### BlobNameWithPolicy ``` New-AzStorageBlobSASToken [-Container] [-Blob] -Policy - [-Protocol ] [-IPAddressOrRange ] [-StartTime ] - [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] - [-DefaultProfile ] [-WhatIf] [-Confirm] + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] + [-StartTime ] [-ExpiryTime ] [-FullUri] [-EncryptionScope ] + [-Context ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` @@ -174,6 +174,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -EncryptionScope Encryption scope to use when sending requests authorized with this SAS URI. diff --git a/src/Storage/Storage.Management/help/New-AzStorageContainerSASToken.md b/src/Storage/Storage.Management/help/New-AzStorageContainerSASToken.md index c9517f65e8e2..3c34fd98f73e 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageContainerSASToken.md +++ b/src/Storage/Storage.Management/help/New-AzStorageContainerSASToken.md @@ -15,18 +15,18 @@ Generates an SAS token for an Azure storage container. ### SasPolicy ``` -New-AzStorageContainerSASToken [-Name] -Policy [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-EncryptionScope ] [-Context ] [-DefaultProfile ] - [-WhatIf] [-Confirm] [] +New-AzStorageContainerSASToken [-Name] -Policy [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] + [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] + [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ### SasPermission ``` -New-AzStorageContainerSASToken [-Name] [-Permission ] [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-EncryptionScope ] [-Context ] [-DefaultProfile ] - [-WhatIf] [-Confirm] [] +New-AzStorageContainerSASToken [-Name] [-Permission ] [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] + [-ExpiryTime ] [-FullUri] [-EncryptionScope ] [-Context ] + [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ## DESCRIPTION @@ -99,6 +99,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -EncryptionScope Encryption scope to use when sending requests authorized with this SAS URI. diff --git a/src/Storage/Storage.Management/help/New-AzStorageContext.md b/src/Storage/Storage.Management/help/New-AzStorageContext.md index b7d68737da9c..aed816bbb4d7 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageContext.md +++ b/src/Storage/Storage.Management/help/New-AzStorageContext.md @@ -16,8 +16,7 @@ Creates an Azure Storage context. ### OAuthAccount (Default) ``` New-AzStorageContext [-StorageAccountName] [-UseConnectedAccount] [-Protocol ] - [-Endpoint ] [-EnableFileBackupRequestIntent] - [] + [-Endpoint ] [-EnableFileBackupRequestIntent] [] ``` ### AccountNameAndKey @@ -46,28 +45,26 @@ New-AzStorageContext [-StorageAccountName] [-Anonymous] [-Protocol -SasToken [-Protocol ] - [-Endpoint ] [] +New-AzStorageContext [-StorageAccountName] -SasToken [-UseConnectedAccount] + [-Protocol ] [-Endpoint ] [-EnableFileBackupRequestIntent] [] ``` ### SasTokenWithAzureEnvironment ``` -New-AzStorageContext [-StorageAccountName] -SasToken -Environment - [] +New-AzStorageContext [-StorageAccountName] -SasToken [-UseConnectedAccount] + -Environment [-EnableFileBackupRequestIntent] [] ``` ### OAuthAccountEnvironment ``` New-AzStorageContext [-StorageAccountName] [-UseConnectedAccount] [-Protocol ] - -Environment [-EnableFileBackupRequestIntent] - [] + -Environment [-EnableFileBackupRequestIntent] [] ``` ### AccountNameAndKeyServiceEndpoint ``` New-AzStorageContext [-StorageAccountName] [-StorageAccountKey] -BlobEndpoint - [-FileEndpoint ] [-QueueEndpoint ] [-TableEndpoint ] - [] + [-FileEndpoint ] [-QueueEndpoint ] [-TableEndpoint ] [] ``` ### OAuthAccountServiceEndpoint @@ -79,8 +76,9 @@ New-AzStorageContext [[-StorageAccountName] ] [-UseConnectedAccount] [-B ### SasTokenServiceEndpoint ``` -New-AzStorageContext -SasToken [-BlobEndpoint ] [-FileEndpoint ] - [-QueueEndpoint ] [-TableEndpoint ] [] +New-AzStorageContext -SasToken [-UseConnectedAccount] [-BlobEndpoint ] + [-FileEndpoint ] [-QueueEndpoint ] [-TableEndpoint ] [-EnableFileBackupRequestIntent] + [] ``` ### ConnectionString @@ -293,7 +291,7 @@ Required parameter to use with OAuth (Microsoft Entra ID) Authentication for Fil ```yaml Type: System.Management.Automation.SwitchParameter -Parameter Sets: OAuthAccount, OAuthAccountEnvironment, OAuthAccountServiceEndpoint +Parameter Sets: OAuthAccount, SasToken, SasTokenWithAzureEnvironment, OAuthAccountEnvironment, OAuthAccountServiceEndpoint, SasTokenServiceEndpoint Aliases: Required: False @@ -488,7 +486,7 @@ The cmdlet will use OAuth Authentication by default, when other authentication n ```yaml Type: System.Management.Automation.SwitchParameter -Parameter Sets: OAuthAccount, OAuthAccountEnvironment, OAuthAccountServiceEndpoint +Parameter Sets: OAuthAccount, SasToken, SasTokenWithAzureEnvironment, OAuthAccountEnvironment, OAuthAccountServiceEndpoint, SasTokenServiceEndpoint Aliases: Required: False diff --git a/src/Storage/Storage.Management/help/New-AzStorageFileSASToken.md b/src/Storage/Storage.Management/help/New-AzStorageFileSASToken.md index 6c6b59f9314b..599fc62b5266 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageFileSASToken.md +++ b/src/Storage/Storage.Management/help/New-AzStorageFileSASToken.md @@ -15,34 +15,33 @@ Generates a shared access signature token for a Storage file. ### NameSasPermission ``` -New-AzStorageFileSASToken [-ShareName] [-Path] [-Permission ] [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] +New-AzStorageFileSASToken [-ShareName] [-Path] [-DelegatedUserObjectId ] + [-Permission ] [-Protocol ] [-IPAddressOrRange ] [-StartTime ] + [-ExpiryTime ] [-FullUri] [-Context ] [-DefaultProfile ] [] ``` ### NameSasPolicy ``` -New-AzStorageFileSASToken [-ShareName] [-Path] -Policy [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] +New-AzStorageFileSASToken [-ShareName] [-Path] -Policy + [-DelegatedUserObjectId ] [-Protocol ] [-IPAddressOrRange ] [-StartTime ] + [-ExpiryTime ] [-FullUri] [-Context ] [-DefaultProfile ] [] ``` ### FileSasPermission ``` -New-AzStorageFileSASToken -ShareFileClient [-Permission ] [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] +New-AzStorageFileSASToken -ShareFileClient [-DelegatedUserObjectId ] + [-Permission ] [-Protocol ] [-IPAddressOrRange ] [-StartTime ] + [-ExpiryTime ] [-FullUri] [-Context ] [-DefaultProfile ] [] ``` ### FileSasPolicy ``` -New-AzStorageFileSASToken -ShareFileClient -Policy [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] - [] +New-AzStorageFileSASToken -ShareFileClient -Policy [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ## DESCRIPTION @@ -105,6 +104,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ExpiryTime Specifies the time at which the shared access signature becomes invalid. diff --git a/src/Storage/Storage.Management/help/New-AzStorageQueueSASToken.md b/src/Storage/Storage.Management/help/New-AzStorageQueueSASToken.md index 44bbf36fd21a..967593fbfd7c 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageQueueSASToken.md +++ b/src/Storage/Storage.Management/help/New-AzStorageQueueSASToken.md @@ -15,17 +15,16 @@ Generates a shared access signature token for an Azure storage queue. ### SasPolicy ``` -New-AzStorageQueueSASToken [-Name] -Policy [-Protocol ] [-IPAddressOrRange ] - [-StartTime ] [-ExpiryTime ] [-FullUri] [-Context ] - [-DefaultProfile ] [] +New-AzStorageQueueSASToken [-Name] -Policy [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ### SasPermission ``` -New-AzStorageQueueSASToken [-Name] [-Permission ] [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] - [] +New-AzStorageQueueSASToken [-Name] [-Permission ] [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ## DESCRIPTION @@ -73,6 +72,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ExpiryTime Specifies when the shared access signature is no longer valid. diff --git a/src/Storage/Storage.Management/help/New-AzStorageShareSASToken.md b/src/Storage/Storage.Management/help/New-AzStorageShareSASToken.md index 33b7f3a1f512..7a43cff33957 100644 --- a/src/Storage/Storage.Management/help/New-AzStorageShareSASToken.md +++ b/src/Storage/Storage.Management/help/New-AzStorageShareSASToken.md @@ -15,18 +15,16 @@ Generate Shared Access Signature token for Azure Storage share. ### SasPolicy ``` -New-AzStorageShareSASToken [-ShareName] -Policy [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] - [] +New-AzStorageShareSASToken [-ShareName] -Policy [-DelegatedUserObjectId ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ### SasPermission ``` -New-AzStorageShareSASToken [-ShareName] [-Permission ] [-Protocol ] - [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] - [-Context ] [-DefaultProfile ] - [] +New-AzStorageShareSASToken [-ShareName] [-DelegatedUserObjectId ] [-Permission ] + [-Protocol ] [-IPAddressOrRange ] [-StartTime ] [-ExpiryTime ] [-FullUri] + [-Context ] [-DefaultProfile ] [] ``` ## DESCRIPTION @@ -90,6 +88,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DelegatedUserObjectId +This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ExpiryTime Specifies the time at which the shared access signature becomes invalid. diff --git a/src/Storage/Storage.Test/Blob/StorageCloudBlobCmdletBaseTest.cs b/src/Storage/Storage.Test/Blob/StorageCloudBlobCmdletBaseTest.cs index 2cedf4ea7f9e..32721add73b2 100644 --- a/src/Storage/Storage.Test/Blob/StorageCloudBlobCmdletBaseTest.cs +++ b/src/Storage/Storage.Test/Blob/StorageCloudBlobCmdletBaseTest.cs @@ -33,7 +33,7 @@ public void InitCommand() { command = new StorageCloudBlobCmdletBase(BlobMock) { - Context = new AzureStorageContext(CloudStorageAccount.DevelopmentStorageAccount), + Context = new AzureStorageContext(CloudStorageAccount.DevelopmentStorageAccount, null, null, null), CommandRuntime = MockCmdRunTime }; } diff --git a/src/Storage/Storage.Test/Common/Cmdlet/NewAzureStorageContextTest.cs b/src/Storage/Storage.Test/Common/Cmdlet/NewAzureStorageContextTest.cs index e4bb308bf2bd..90a6802f3f87 100644 --- a/src/Storage/Storage.Test/Common/Cmdlet/NewAzureStorageContextTest.cs +++ b/src/Storage/Storage.Test/Common/Cmdlet/NewAzureStorageContextTest.cs @@ -114,10 +114,10 @@ public void GetStorageAccountByConnectionStringAndSasToken() string endpoint = "http://storageaccountname.blob.core.windows.net"; string connectionString = String.Format("BlobEndpoint={0};QueueEndpoint={0};TableEndpoint={0};SharedAccessSignature={1}", endpoint, sasToken); CloudStorageAccount account = command.GetStorageAccountByConnectionString(connectionString); - AzureStorageContext context = new AzureStorageContext(account); + AzureStorageContext context = new AzureStorageContext(account, null, null, null); connectionString = String.Format("BlobEndpoint={0};SharedAccessSignature={1}", endpoint, sasToken); account = command.GetStorageAccountByConnectionString(connectionString); - context = new AzureStorageContext(account); + context = new AzureStorageContext(account, null, null, null); } } } diff --git a/src/Storage/Storage.Test/Common/StorageCloudCmdletBaseTest.cs b/src/Storage/Storage.Test/Common/StorageCloudCmdletBaseTest.cs index 36a909494da6..581f6f00025c 100644 --- a/src/Storage/Storage.Test/Common/StorageCloudCmdletBaseTest.cs +++ b/src/Storage/Storage.Test/Common/StorageCloudCmdletBaseTest.cs @@ -54,7 +54,7 @@ public void CleanCommand() public void GetCloudStorageAccountFromContextTest() { CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount; - command.Context = new AzureStorageContext(account); + command.Context = new AzureStorageContext(account, null, null, null); Assert.AreEqual(command.Context, command.GetCmdletStorageContext()); } @@ -73,7 +73,7 @@ public void WriteObjectWithStorageContextWithNullContextTest() public void WriteObjectWithStorageContextWithContextTest() { CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount; - command.Context = new AzureStorageContext(account); + command.Context = new AzureStorageContext(account, null, null, null); AzureStorageBase item = new AzureStorageBase(); command.WriteObjectWithStorageContext(item); @@ -105,7 +105,7 @@ public void WriteObjectWithStorageContextWihtEnumerableList() public void ShouldInitServiceChannelTest() { CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount; - command.Context = new AzureStorageContext(account); + command.Context = new AzureStorageContext(account, null, null, null); string toss; Assert.IsFalse(command.TryGetStorageAccount(command.SMProfile, out toss)); } diff --git a/src/Storage/Storage.Test/Service/MockStorageBlobManagement.cs b/src/Storage/Storage.Test/Service/MockStorageBlobManagement.cs index a6a25232d605..23b1831be804 100644 --- a/src/Storage/Storage.Test/Service/MockStorageBlobManagement.cs +++ b/src/Storage/Storage.Test/Service/MockStorageBlobManagement.cs @@ -744,6 +744,11 @@ public BlobServiceClient GetBlobServiceClient(BlobClientOptions options = null) throw new NotImplementedException(); } + public bool IsSasWithOAuthCredential() + { + throw new NotImplementedException(); + } + /// /// The storage context /// diff --git a/src/Storage/Storage.common/Common/AzureContextAdapterExtensions.cs b/src/Storage/Storage.common/Common/AzureContextAdapterExtensions.cs index 2724ede3b406..4fd0a190df96 100644 --- a/src/Storage/Storage.common/Common/AzureContextAdapterExtensions.cs +++ b/src/Storage/Storage.common/Common/AzureContextAdapterExtensions.cs @@ -84,7 +84,9 @@ public static IStorageContext GetStorageContext(this IStorageService service) { return new AzureStorageContext(new CloudStorageAccount(new StorageCredentials(service.Name, service.AuthenticationKeys.First()), new StorageUri(service.BlobEndpoint), new StorageUri(service.QueueEndpoint), - new StorageUri(service.TableEndpoint), new StorageUri(service.FileEndpoint))); + new StorageUri(service.TableEndpoint), new StorageUri(service.FileEndpoint)), + null, + false); } diff --git a/src/Storage/Storage.common/Common/AzureStorageContext.cs b/src/Storage/Storage.common/Common/AzureStorageContext.cs index ebe02864c1cb..855915ecf70c 100644 --- a/src/Storage/Storage.common/Common/AzureStorageContext.cs +++ b/src/Storage/Storage.common/Common/AzureStorageContext.cs @@ -149,7 +149,20 @@ public string ConnectionString { /// Storage account name /// /// - public AzureStorageContext(CloudStorageAccount account, string accountName = null, IAzureContext DefaultContext = null, DebugLogWriter logWriter = null) + public AzureStorageContext(CloudStorageAccount account, string accountName = null, IAzureContext DefaultContext = null, DebugLogWriter logWriter = null) : + this(account, accountName, false, DefaultContext, logWriter) + { + } + + /// + /// Create a storage context using cloud storage account + /// + /// cloud storage account + /// Storage account name + /// + /// + /// + public AzureStorageContext(CloudStorageAccount account, string accountName = null, bool isOAuthToken = false, IAzureContext DefaultContext = null, DebugLogWriter logWriter = null) { StorageAccount = account; TableStorageAccount = XTable.CloudStorageAccount.Parse(StorageAccount.ToString(true)); @@ -195,7 +208,8 @@ public AzureStorageContext(CloudStorageAccount account, string accountName = nul StorageAccountName = "[Anonymous]"; } } - if (account.Credentials != null && account.Credentials.IsToken) + if ((account.Credentials != null && account.Credentials.IsToken) + || isOAuthToken) { Track2OauthToken = new AzureSessionCredential(DefaultContext, logWriter); } diff --git a/src/Storage/Storage.common/Storage.common.csproj b/src/Storage/Storage.common/Storage.common.csproj index 4add4bf55085..f26900ce6db5 100644 --- a/src/Storage/Storage.common/Storage.common.csproj +++ b/src/Storage/Storage.common/Storage.common.csproj @@ -17,10 +17,11 @@ - + + \ No newline at end of file diff --git a/src/Storage/Storage/Blob/Cmdlet/CopyAzureStorageBlob.cs b/src/Storage/Storage/Blob/Cmdlet/CopyAzureStorageBlob.cs index 432f17597d07..14c6e3dcb379 100644 --- a/src/Storage/Storage/Blob/Cmdlet/CopyAzureStorageBlob.cs +++ b/src/Storage/Storage/Blob/Cmdlet/CopyAzureStorageBlob.cs @@ -15,8 +15,15 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Blob.Cmdlet { + using System; + using System.Collections.Generic; + using System.Management.Automation; + using System.Security.Permissions; + using System.Threading.Tasks; using Azure.Commands.Common.Authentication.Abstractions; using Commands.Common.Storage.ResourceModel; + using global::Azure; + using global::Azure.Core; using global::Azure.Storage.Blobs; using global::Azure.Storage.Blobs.Models; using global::Azure.Storage.Blobs.Specialized; @@ -24,11 +31,6 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Blob.Cmdlet using Microsoft.Azure.Storage.Blob; using Microsoft.WindowsAzure.Commands.Storage.Common; using Microsoft.WindowsAzure.Commands.Storage.Model.Contract; - using System; - using System.Collections.Generic; - using System.Management.Automation; - using System.Security.Permissions; - using System.Threading.Tasks; using Track2Models = global::Azure.Storage.Blobs.Models; [Cmdlet("Copy", Azure.Commands.ResourceManager.Common.AzureRMConstants.AzurePrefix + "StorageBlob", SupportsShouldProcess = true, DefaultParameterSetName = ContainerNameParameterSet),OutputType(typeof(AzureStorageBlob))] @@ -324,7 +326,7 @@ private void CopyBlobSync(IStorageBlobManagement destChannel, BlobBaseClient src destCloudBlob = Util.GetTrack2BlobClientWithType(destCloudBlob, destChannel.StorageContext, srcBlobType, ClientOptions); } - Func taskGenerator = (taskId) => CopyFromUri(taskId, destChannel, srcCloudBlob.GenerateUriWithCredentials(Channel.StorageContext), destCloudBlob); + Func taskGenerator = (taskId) => CopyFromUri(taskId, destChannel, srcCloudBlob.GenerateUriWithCredentials(Channel.StorageContext), Channel, destCloudBlob); RunTask(taskGenerator); } @@ -333,15 +335,24 @@ private void CopyBlobSync(IStorageBlobManagement destChannel, string srcUri, str Track2Models.BlobType srcBlobType = Util.GetBlobType(new BlobBaseClient(new Uri(srcUri), ClientOptions), true).Value; BlobBaseClient destBlob = this.GetDestBlob(destChannel, destContainer, destBlobName, srcBlobType); - Func taskGenerator = (taskId) => CopyFromUri(taskId, destChannel, new Uri(srcUri), destBlob); + Func taskGenerator = (taskId) => CopyFromUri(taskId, destChannel, new Uri(srcUri), Channel, destBlob); RunTask(taskGenerator); } - private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, Uri srcUri, BlobBaseClient destBlob) + private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, Uri srcUri, IStorageBlobManagement sourceChannel, BlobBaseClient destBlob) { bool destExist = true; - Track2Models.BlobType? srcBlobType = Util.GetBlobType(new BlobBaseClient(srcUri, ClientOptions), true).Value; - Track2Models.BlobType? destBlobType = Util.GetBlobType(new BlobBaseClient(srcUri, ClientOptions), true).Value; + BlobBaseClient srcBlobClient; + if (sourceChannel.StorageContext != null && sourceChannel.StorageContext.Track2OauthToken != null) + { + srcBlobClient = new BlobBaseClient(srcUri, sourceChannel.StorageContext.Track2OauthToken, ClientOptions); + } + else + { + srcBlobClient = new BlobBaseClient(srcUri, ClientOptions); + } + Track2Models.BlobType? srcBlobType = Util.GetBlobType(srcBlobClient, true).Value; + Track2Models.BlobType? destBlobType = srcBlobType; Track2Models.BlobProperties properties = null; try @@ -399,8 +410,6 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, if (!destExist || this.ConfirmOverwrite(srcUri.AbsoluteUri.ToString(), destBlob.Uri.ToString())) { - - BlobBaseClient srcBlobClient= new BlobBaseClient(srcUri, ClientOptions); Track2Models.BlobProperties srcProperties = srcBlobClient.GetProperties(cancellationToken: this.CmdletCancellationToken).Value; Track2Models.BlobHttpHeaders httpHeaders = new Track2Models.BlobHttpHeaders @@ -449,11 +458,17 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, Track2Models.PageBlobCreateOptions pageBlobCreateOptions = new Track2Models.PageBlobCreateOptions(); pageBlobCreateOptions.HttpHeaders = httpHeaders; pageBlobCreateOptions.Metadata = srcProperties.Metadata; - pageBlobCreateOptions.Tags = blobTags ?? null; + pageBlobCreateOptions.Tags = (blobTags is null || blobTags.Count == 0) ? null : blobTags; destPageBlob.Create(srcProperties.ContentLength, pageBlobCreateOptions, this.CmdletCancellationToken); Track2Models.PageBlobUploadPagesFromUriOptions pageBlobUploadPagesFromUriOptions = new Track2Models.PageBlobUploadPagesFromUriOptions(); + if (sourceChannel.StorageContext != null && sourceChannel.StorageContext.Track2OauthToken != null) + { + string oauthToken = sourceChannel.StorageContext.Track2OauthToken.GetToken(new TokenRequestContext(), this.CmdletCancellationToken).Token; + pageBlobUploadPagesFromUriOptions.SourceAuthentication = new HttpAuthorization("Bearer", oauthToken); + } + long pageCopyOffset = 0; progressHandler.Report(pageCopyOffset); long contentLenLeft = srcProperties.ContentLength; @@ -473,7 +488,7 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, Track2Models.AppendBlobCreateOptions appendBlobCreateOptions = new Track2Models.AppendBlobCreateOptions(); appendBlobCreateOptions.HttpHeaders = httpHeaders; appendBlobCreateOptions.Metadata = srcProperties.Metadata; - appendBlobCreateOptions.Tags = blobTags ?? null; + appendBlobCreateOptions.Tags = (blobTags is null || blobTags.Count == 0) ? null : blobTags; destAppendBlob.Create(appendBlobCreateOptions, this.CmdletCancellationToken); @@ -490,6 +505,12 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, SourceRange = new global::Azure.HttpRange(appendCopyOffset, appendContentSize) }; + if (sourceChannel.StorageContext != null && sourceChannel.StorageContext.Track2OauthToken != null) + { + string oauthToken = sourceChannel.StorageContext.Track2OauthToken.GetToken(new TokenRequestContext(), this.CmdletCancellationToken).Token; + appendBlobAppendBlockFromUriOptions.SourceAuthentication = new HttpAuthorization("Bearer", oauthToken); + } + destAppendBlob.AppendBlockFromUri(srcUri, appendBlobAppendBlockFromUriOptions, this.CmdletCancellationToken); appendCopyOffset += appendContentSize; progressHandler.Report(appendContentSize); @@ -513,7 +534,13 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, } options.SourceConditions = this.BlobRequestConditions; options.Metadata = srcProperties.Metadata; - options.Tags = blobTags ?? null; + options.Tags = (blobTags is null || blobTags.Count == 0) ? null : blobTags; + + if (sourceChannel.StorageContext != null && sourceChannel.StorageContext.Track2OauthToken != null) + { + string oauthToken = sourceChannel.StorageContext.Track2OauthToken.GetToken(new TokenRequestContext(), this.CmdletCancellationToken).Token; + options.SourceAuthentication = new HttpAuthorization("Bearer", oauthToken); + } destBlobClient.SyncCopyFromUri(srcUri, options, this.CmdletCancellationToken); @@ -531,7 +558,7 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, Track2Models.CommitBlockListOptions commitBlockListOptions = new Track2Models.CommitBlockListOptions(); commitBlockListOptions.HttpHeaders = httpHeaders; commitBlockListOptions.Metadata = srcProperties.Metadata; - commitBlockListOptions.Tags = blobTags ?? null; + commitBlockListOptions.Tags = (blobTags is null || blobTags.Count == 0) ? null : blobTags; if (accesstier != null) { @@ -544,12 +571,19 @@ private async Task CopyFromUri(long taskId, IStorageBlobManagement destChannel, progressHandler.Report(copyoffset); foreach (string id in blockIDs) { + Track2Models.StageBlockFromUriOptions stageBlockOptions = new Track2Models.StageBlockFromUriOptions(); long blocksize = blockLength; if (copyoffset + blocksize > srcProperties.ContentLength) { blocksize = srcProperties.ContentLength - copyoffset; } - destBlockBlob.StageBlockFromUri(srcUri, id, new global::Azure.HttpRange(copyoffset, blocksize), null, null, null, cancellationToken: this.CmdletCancellationToken); + stageBlockOptions.SourceRange = new global::Azure.HttpRange(copyoffset, blocksize); + if (sourceChannel.StorageContext != null && sourceChannel.StorageContext.Track2OauthToken != null) + { + string oauthToken = sourceChannel.StorageContext.Track2OauthToken.GetToken(new TokenRequestContext(), this.CmdletCancellationToken).Token; + stageBlockOptions.SourceAuthentication = new HttpAuthorization("Bearer", oauthToken); + } + destBlockBlob.StageBlockFromUri(srcUri, id, stageBlockOptions, cancellationToken: this.CmdletCancellationToken); copyoffset += blocksize; progressHandler.Report(copyoffset); diff --git a/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobContent.cs b/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobContent.cs index b85ece2ae851..380c62b1e46d 100644 --- a/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobContent.cs +++ b/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobContent.cs @@ -216,13 +216,13 @@ internal virtual async Task DownloadBlob(long taskId, IStorageBlobManagement loc || !System.IO.File.Exists(filePath) || ShouldContinue(string.Format(Resources.OverwriteConfirmation, filePath), null)) { - StorageTransferOptions trasnferOption = new StorageTransferOptions() + StorageTransferOptions transferOption = new StorageTransferOptions() { MaximumConcurrency = this.GetCmdletConcurrency(), MaximumTransferSize = size4MB, InitialTransferSize = size4MB }; - await blob.DownloadToAsync(filePath, BlobRequestConditions, trasnferOption, CmdletCancellationToken).ConfigureAwait(false); + await blob.DownloadToAsync(filePath, BlobRequestConditions, transferOption, CmdletCancellationToken).ConfigureAwait(false); OutputStream.WriteObject(taskId, new AzureStorageBlob(blob, localChannel is null? null : localChannel.StorageContext, blobProperties, options: ClientOptions)); } } @@ -263,7 +263,7 @@ internal void GetBlobContent(CloudBlobContainer container, string blobName, stri ValidatePipelineCloudBlobContainer(container); - if (UseTrack2Sdk()) + if (UseTrack2Sdk() || IsSasTokenWithOAuth(container)) { BlobContainerClient track2container = AzureStorageContainer.GetTrack2BlobContainerClient(container, Channel.StorageContext, ClientOptions); BlobBaseClient blobClient = track2container.GetBlobBaseClient(blobName); @@ -279,6 +279,19 @@ internal void GetBlobContent(CloudBlobContainer container, string blobName, stri } } + private bool IsSasTokenWithOAuth(CloudBlobContainer container) + { + if (container != null && container.ServiceClient != null && container.ServiceClient.Credentials != null && container.ServiceClient.Credentials.IsSAS) //SAS + { + if (Channel.StorageContext != null && Channel.StorageContext.Track2OauthToken != null) + { + return true; + } + } + + return false; + } + /// /// get blob content /// @@ -544,7 +557,7 @@ public override void ExecuteCmdlet() case BlobParameterSet: if (ShouldProcess(CloudBlob.Name, "Download")) { - if (!(CloudBlob is InvalidCloudBlob) && !UseTrack2Sdk()) + if (!(CloudBlob is InvalidCloudBlob) && !UseTrack2Sdk() && !IsSasTokenWithOAuth(CloudBlob.Container)) { GetBlobContent(CloudBlob, FileName, true); } diff --git a/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobCopyState.cs b/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobCopyState.cs index 4fb497af3528..1b877a8bd178 100644 --- a/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobCopyState.cs +++ b/src/Storage/Storage/Blob/Cmdlet/GetAzureStorageBlobCopyState.cs @@ -113,6 +113,11 @@ protected bool IsTaskCompleted(long taskId) [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] public override void ExecuteCmdlet() { + if (Channel.IsSasWithOAuthCredential()) + { + throw new InvalidOperationException("Get Blob Async copy status doesn't support user delegation SAS with OAuth credential."); + } + CloudBlob blob = null; switch (ParameterSetName) diff --git a/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageBlobSasToken.cs b/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageBlobSasToken.cs index 4fcb24f2a020..1f7d41d0a6c9 100644 --- a/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageBlobSasToken.cs +++ b/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageBlobSasToken.cs @@ -109,6 +109,10 @@ public string Policy [ValidateNotNullOrEmpty] public string Permission { get; set; } + [Parameter(Mandatory = false, HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Protocol can be used in the request with this SAS token.")] [ValidateNotNull] public SharedAccessProtocol? Protocol { get; set; } @@ -191,6 +195,14 @@ public override void ExecuteCmdlet() return; } } + else + { + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } + } + //Get blob instance BlobBaseClient blobClient; if (this.BlobBaseClient != null) @@ -211,7 +223,7 @@ public override void ExecuteCmdlet() } //Create SAS builder - BlobSasBuilder sasBuilder = SasTokenHelper.SetBlobSasBuilder_FromBlob(blobClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.EncryptionScope); + BlobSasBuilder sasBuilder = SasTokenHelper.SetBlobSasBuilder_FromBlob(blobClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.EncryptionScope, this.DelegatedUserObjectId); //Create SAS and output string sasToken = SasTokenHelper.GetBlobSharedAccessSignature(Channel.StorageContext, sasBuilder, generateUserDelegationSas, ClientOptions, CmdletCancellationToken); diff --git a/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageContainerSasToken.cs b/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageContainerSasToken.cs index 7dbcf4c4c7a9..2ca1441ad0c3 100644 --- a/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageContainerSasToken.cs +++ b/src/Storage/Storage/Blob/Cmdlet/NewAzureStorageContainerSasToken.cs @@ -67,6 +67,10 @@ public string Policy [ValidateNotNullOrEmpty] public string Permission { get; set; } + [Parameter(Mandatory = false, HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Protocol can be used in the request with this SAS token.")] [ValidateNotNull] public SharedAccessProtocol? Protocol { get; set; } @@ -139,6 +143,14 @@ public override void ExecuteCmdlet() return; } } + else + { + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } + } + //Get container instance CloudBlobContainer container_Track1 = Channel.GetContainerReference(Name); BlobContainerClient container = AzureStorageContainer.GetTrack2BlobContainerClient(container_Track1, Channel.StorageContext, ClientOptions); @@ -151,7 +163,7 @@ public override void ExecuteCmdlet() } //Create SAS builder - BlobSasBuilder sasBuilder = SasTokenHelper.SetBlobSasBuilder_FromContainer(container, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.EncryptionScope); + BlobSasBuilder sasBuilder = SasTokenHelper.SetBlobSasBuilder_FromContainer(container, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.EncryptionScope, this.DelegatedUserObjectId); //Create SAS and output it string sasToken = SasTokenHelper.GetBlobSharedAccessSignature(Channel.StorageContext, sasBuilder, generateUserDelegationSas, ClientOptions, CmdletCancellationToken); diff --git a/src/Storage/Storage/Blob/Cmdlet/RemoveAzureStorageBlob.cs b/src/Storage/Storage/Blob/Cmdlet/RemoveAzureStorageBlob.cs index c472a4056e9c..4d793bae97f0 100644 --- a/src/Storage/Storage/Blob/Cmdlet/RemoveAzureStorageBlob.cs +++ b/src/Storage/Storage/Blob/Cmdlet/RemoveAzureStorageBlob.cs @@ -344,7 +344,7 @@ internal async Task RemoveAzureBlob(long taskId, IStorageBlobManagement localCha ValidatePipelineCloudBlobContainer(container); - if (!UseTrack2Sdk()) + if (!UseTrack2Sdk() && !Channel.IsSasWithOAuthCredential()) { AccessCondition accessCondition = null; BlobRequestOptions requestOptions = null; diff --git a/src/Storage/Storage/Blob/Cmdlet/SetAzureStorageBlobContent.cs b/src/Storage/Storage/Blob/Cmdlet/SetAzureStorageBlobContent.cs index 345b72dccbf1..ef30cd6ff3d6 100644 --- a/src/Storage/Storage/Blob/Cmdlet/SetAzureStorageBlobContent.cs +++ b/src/Storage/Storage/Blob/Cmdlet/SetAzureStorageBlobContent.cs @@ -397,7 +397,7 @@ protected override void DoEndProcessing() Func taskGenerator; long fileSize = new FileInfo(ResolvedFileName).Length; - if (!UseTrack2Sdk() && (this.BlobType.ToLower() != AppendBlobType.ToLower() || fileSize <= (long)size4MB * maxBlockCount)) + if (!(UseTrack2Sdk() || localChannel.IsSasWithOAuthCredential()) && (this.BlobType.ToLower() != AppendBlobType.ToLower() || fileSize <= (long)size4MB * maxBlockCount)) { //Upload with DMlib taskGenerator = (taskId) => Upload2Blob(taskId, localChannel, uploadRequest.Item1, uploadRequest.Item2); @@ -449,9 +449,10 @@ internal virtual async Task UploadBlobwithSdk(long taskId, IStorageBlobManagemen { options = SetClientOptionsWithEncryptionScope(this.EncryptionScope); } + BlobClient blobClient = GetTrack2BlobClient(blob, localChannel.StorageContext, options); if (this.Force.IsPresent - || !blob.Exists() + || !blobClient.Exists() || ShouldContinue(string.Format(Resources.OverwriteConfirmation, blob.Uri), null)) { // Prepare blob Properties, MetaData, accessTier @@ -486,9 +487,8 @@ internal virtual async Task UploadBlobwithSdk(long taskId, IStorageBlobManagemen //block blob if (string.Equals(blobType, BlockBlobType, StringComparison.InvariantCultureIgnoreCase)) { - BlobClient blobClient = GetTrack2BlobClient(blob, localChannel.StorageContext, options); outputBlobClient = blobClient; - StorageTransferOptions trasnferOption = new StorageTransferOptions() { MaximumConcurrency = this.GetCmdletConcurrency() }; + StorageTransferOptions transferOption = new StorageTransferOptions() { MaximumConcurrency = this.GetCmdletConcurrency() }; BlobUploadOptions uploadOptions = new BlobUploadOptions(); if (this.BlobTag != null) { @@ -499,7 +499,7 @@ internal virtual async Task UploadBlobwithSdk(long taskId, IStorageBlobManagemen uploadOptions.Conditions = this.BlobRequestConditions; uploadOptions.AccessTier = accesstierToSet; uploadOptions.ProgressHandler = progressHandler; - uploadOptions.TransferOptions = trasnferOption; + uploadOptions.TransferOptions = transferOption; await blobClient.UploadAsync(stream, uploadOptions, CmdletCancellationToken).ConfigureAwait(false); } diff --git a/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobCopy.cs b/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobCopy.cs index 70b532efaade..66035f2f1803 100644 --- a/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobCopy.cs +++ b/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobCopy.cs @@ -387,6 +387,11 @@ public override void ExecuteCmdlet() IStorageBlobManagement destChannel = GetDestinationChannel(); IStorageBlobManagement srcChannel = Channel; + if (srcChannel.IsSasWithOAuthCredential() || destChannel.IsSasWithOAuthCredential()) + { + throw new InvalidOperationException("Blob Async copy doesn't support user delegation SAS with OAuth credential."); + } + string target = string.Empty; Action copyAction = null; switch (ParameterSetName) diff --git a/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobIncrementalCopy.cs b/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobIncrementalCopy.cs index 44f67f2fd1d3..480518aad50f 100644 --- a/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobIncrementalCopy.cs +++ b/src/Storage/Storage/Blob/Cmdlet/StartAzureStorageBlobIncrementalCopy.cs @@ -206,6 +206,11 @@ public override void ExecuteCmdlet() IStorageBlobManagement destChannel = GetDestinationChannel(); IStorageBlobManagement srcChannel = Channel; + if (srcChannel.IsSasWithOAuthCredential() || destChannel.IsSasWithOAuthCredential()) + { + throw new InvalidOperationException("Blob Async copy doesn't support user delegation SAS with OAuth credential."); + } + string target = string.Empty; Action copyAction = null; switch (ParameterSetName) diff --git a/src/Storage/Storage/Blob/Cmdlet/StopAzureStorageBlobCopy.cs b/src/Storage/Storage/Blob/Cmdlet/StopAzureStorageBlobCopy.cs index 0d8dff8d86d3..53a299a0bc09 100644 --- a/src/Storage/Storage/Blob/Cmdlet/StopAzureStorageBlobCopy.cs +++ b/src/Storage/Storage/Blob/Cmdlet/StopAzureStorageBlobCopy.cs @@ -105,6 +105,10 @@ protected override void BeginProcessing() [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] public override void ExecuteCmdlet() { + if (Channel.IsSasWithOAuthCredential()) + { + throw new InvalidOperationException("Stop Blob Async copy doesn't support user delegation SAS with OAuth credential."); + } Func taskGenerator = null; IStorageBlobManagement localChannel = Channel; string target = string.Empty; @@ -186,7 +190,7 @@ private async Task StopCopyBlob(long taskId, IStorageBlobManagement localChannel { ValidateBlobType(blob); - if (UseTrack2Sdk()) // Use Track2 + if (UseTrack2Sdk() || Channel.IsSasWithOAuthCredential()) // Use Track2 { if (null == blob) { diff --git a/src/Storage/Storage/Blob/StorageCloudBlobCmdletBase.cs b/src/Storage/Storage/Blob/StorageCloudBlobCmdletBase.cs index 43f0e54ad9c5..740281e54f57 100644 --- a/src/Storage/Storage/Blob/StorageCloudBlobCmdletBase.cs +++ b/src/Storage/Storage/Blob/StorageCloudBlobCmdletBase.cs @@ -719,7 +719,17 @@ internal DataLakeFileSystemClient GetFileSystemClientByName(IStorageBlobManageme } else if (localChannel.StorageContext.StorageAccount.Credentials != null && localChannel.StorageContext.StorageAccount.Credentials.IsSAS) //SAS { - fileSystem = new DataLakeFileSystemClient(new Uri (fileSystemUri.ToString() + "?" + Util.GetSASStringWithoutQuestionMark(localChannel.StorageContext.StorageAccount.Credentials.SASToken)), this.DataLakeClientOptions); + if (localChannel.StorageContext.Track2OauthToken != null) + { + fileSystem = new DataLakeFileSystemClient(new Uri(fileSystemUri.ToString() + "?" + Util.GetSASStringWithoutQuestionMark(localChannel.StorageContext.StorageAccount.Credentials.SASToken)), + localChannel.StorageContext.Track2OauthToken, + this.DataLakeClientOptions); + } + else + { + fileSystem = new DataLakeFileSystemClient(new Uri(fileSystemUri.ToString() + "?" + Util.GetSASStringWithoutQuestionMark(localChannel.StorageContext.StorageAccount.Credentials.SASToken)), + this.DataLakeClientOptions); + } } else if (localChannel.StorageContext.StorageAccount.Credentials != null && localChannel.StorageContext.StorageAccount.Credentials.IsSharedKey) //Shared Key { @@ -921,7 +931,14 @@ public static BlobClient GetTrack2BlobClient(CloudBlob cloubBlob, AzureStorageCo { fullUri = fullUri + "?" + sas; } - blobClient = new BlobClient(new Uri(fullUri), options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new BlobClient(new Uri(fullUri), context.Track2OauthToken, options); + } + else + { + blobClient = new BlobClient(new Uri(fullUri), options); + } } else if (cloubBlob.ServiceClient.Credentials.IsSharedKey) //Shared Key { diff --git a/src/Storage/Storage/Common/AzureStorageBlob.cs b/src/Storage/Storage/Common/AzureStorageBlob.cs index ed0ebe64dac3..0bdfb57093df 100644 --- a/src/Storage/Storage/Common/AzureStorageBlob.cs +++ b/src/Storage/Storage/Common/AzureStorageBlob.cs @@ -408,7 +408,8 @@ private void SetProperties(BlobBaseClient track2BlobClient, AzureStorageContext public static CloudBlob GetTrack1Blob(BlobBaseClient track2BlobClient, StorageCredentials credentials, global::Azure.Storage.Blobs.Models.BlobType? blobType = null) { if ((Util.GetVersionIdFromBlobUri(track2BlobClient.Uri) != null) - || (track2BlobClient.Uri.Query.Contains("sig=") && (credentials == null || !credentials.IsSAS))) + || (track2BlobClient.Uri.Query.Contains("sig=") && (credentials == null || !credentials.IsSAS)) + || track2BlobClient.Uri.Query.Contains("sduoid=")) // UD sas + bearer token { // Track1 SDK don't support blob VersionId return new InvalidCloudBlob(track2BlobClient.Uri, credentials); @@ -451,7 +452,8 @@ public void FetchAttributes() public static BlobClient GetTrack2BlobClient(CloudBlob cloubBlob, AzureStorageContext context, BlobClientOptions options = null) { BlobClient blobClient; - if (cloubBlob.ServiceClient.Credentials.IsToken) //Oauth + if (cloubBlob.ServiceClient.Credentials.IsToken + || (context != null && context.Track2OauthToken != null && !cloubBlob.ServiceClient.Credentials.IsSAS && !cloubBlob.ServiceClient.Credentials.IsSharedKey)) //Oauth { if (context == null) { @@ -474,7 +476,14 @@ public static BlobClient GetTrack2BlobClient(CloudBlob cloubBlob, AzureStorageCo { fullUri = fullUri + "?" + sas; } - blobClient = new BlobClient(new Uri(fullUri), options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new BlobClient(new Uri(fullUri), context.Track2OauthToken, options); + } + else + { + blobClient = new BlobClient(new Uri(fullUri), options); + } } else if (cloubBlob.ServiceClient.Credentials.IsSharedKey) //Shared Key { @@ -509,7 +518,14 @@ public static BlobClient GetTrack2BlobClient(BlobBaseClient blobBaseClient, Azur } else //Anonymous or SAS { - blobClient = new BlobClient(blobBaseClient.Uri, options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new BlobClient(blobBaseClient.Uri, context.Track2OauthToken, options); + } + else + { + blobClient = new BlobClient(blobBaseClient.Uri, options); + } } return blobClient; diff --git a/src/Storage/Storage/Common/AzureStorageContainer.cs b/src/Storage/Storage/Common/AzureStorageContainer.cs index eee568794b2a..9f0a6b866bbf 100644 --- a/src/Storage/Storage/Common/AzureStorageContainer.cs +++ b/src/Storage/Storage/Common/AzureStorageContainer.cs @@ -262,7 +262,14 @@ public static BlobContainerClient GetTrack2BlobContainerClient(CloudBlobContaine string fullUri = cloubContainer.Uri.ToString(); string sas = Util.GetSASStringWithoutQuestionMark(cloubContainer.ServiceClient.Credentials.SASToken); fullUri = fullUri + "?" + sas; - blobContainerClient = new BlobContainerClient(new Uri(fullUri), options); + if (context != null && context.Track2OauthToken != null) + { + blobContainerClient = new BlobContainerClient(new Uri(fullUri), context.Track2OauthToken, options); + } + else + { + blobContainerClient = new BlobContainerClient(new Uri(fullUri), options); + } } else if (cloubContainer.ServiceClient.Credentials.IsSharedKey) //Shared Key { diff --git a/src/Storage/Storage/Common/Cmdlet/NewAzureStorageContext.cs b/src/Storage/Storage/Common/Cmdlet/NewAzureStorageContext.cs index 859d9acdbfe7..4f492f40b5e5 100644 --- a/src/Storage/Storage/Common/Cmdlet/NewAzureStorageContext.cs +++ b/src/Storage/Storage/Common/Cmdlet/NewAzureStorageContext.cs @@ -181,13 +181,16 @@ public SwitchParameter Anonymous [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = OAuthParameterSet)] [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = OAuthEnvironmentParameterSet)] [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = OAuthServiceEndpointParameterSet)] + [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = SasTokenParameterSet)] + [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = SasTokenEnvironmentParameterSet)] + [Parameter(HelpMessage = "Use OAuth storage account", Mandatory = false, ParameterSetName = SasTokenServiceEndpointParameterSet)] public SwitchParameter UseConnectedAccount { get { return isOAuth; } set { isOAuth = value; } } - private bool isOAuth = true; + private bool isOAuth = false; private const string ProtocolHelpMessage = "Protocol specification (HTTP or HTTPS), default is HTTPS"; [Parameter(HelpMessage = ProtocolHelpMessage, @@ -270,9 +273,13 @@ public string Environment [Parameter(HelpMessage = TableServiceEndPointHelpMessage, ParameterSetName = OAuthServiceEndpointParameterSet)] public string TableEndpoint { get; set; } - [Parameter(Mandatory = false, ParameterSetName = OAuthParameterSet, HelpMessage = "Required parameter to use with OAuth (Microsoft Entra ID) Authentication for Files. This will bypass any file/directory level permission checks and allow access, based on the allowed data actions, even if there are ACLs in place for those files/directories.")] - [Parameter(Mandatory = false, ParameterSetName = OAuthEnvironmentParameterSet, HelpMessage = "Required parameter to use with OAuth (Microsoft Entra ID) Authentication for Files. This will bypass any file/directory level permission checks and allow access, based on the allowed data actions, even if there are ACLs in place for those files/directories.")] - [Parameter(Mandatory = false, ParameterSetName = OAuthServiceEndpointParameterSet, HelpMessage = "Required parameter to use with OAuth (Microsoft Entra ID) Authentication for Files. This will bypass any file/directory level permission checks and allow access, based on the allowed data actions, even if there are ACLs in place for those files/directories.")] + private const string EnableFileBackupRequestIntentHelpText = "Required parameter to use with OAuth (Microsoft Entra ID) Authentication for Files. This will bypass any file/directory level permission checks and allow access, based on the allowed data actions, even if there are ACLs in place for those files/directories."; + [Parameter(Mandatory = false, ParameterSetName = OAuthParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] + [Parameter(Mandatory = false, ParameterSetName = OAuthEnvironmentParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] + [Parameter(Mandatory = false, ParameterSetName = OAuthServiceEndpointParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] + [Parameter(Mandatory = false, ParameterSetName = SasTokenParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] + [Parameter(Mandatory = false, ParameterSetName = SasTokenEnvironmentParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] + [Parameter(Mandatory = false, ParameterSetName = SasTokenServiceEndpointParameterSet, HelpMessage = EnableFileBackupRequestIntentHelpText)] public SwitchParameter EnableFileBackupRequestIntent { get; set; } /// @@ -627,19 +634,22 @@ public override void ExecuteCmdlet() account = GetAnonymousStorageAccountFromAzureEnvironment(StorageAccountName, useHttps, environmentName); break; case OAuthParameterSet: + this.isOAuth = true; account = GetStorageAccountByOAuth(StorageAccountName, useHttps, storageEndpoint); break; case OAuthServiceEndpointParameterSet: + this.isOAuth = true; account = GetStorageAccountByOAuth(this.BlobEndpoint, this.QueueEndpoint, this.FileEndpoint, this.TableEndpoint); break; case OAuthEnvironmentParameterSet: + this.isOAuth = true; account = GetStorageAccountByOAuthFromAzureEnvironment(StorageAccountName, useHttps, environmentName); break; default: throw new ArgumentException(Resources.DefaultStorageCredentialsNotFound); } - AzureStorageContext context = new AzureStorageContext(account, GetRealAccountName(StorageAccountName), DefaultContext, WriteDebug); + AzureStorageContext context = new AzureStorageContext(account, GetRealAccountName(StorageAccountName), this.isOAuth, DefaultContext, WriteDebug); if (this.EnableFileBackupRequestIntent.IsPresent) { context.ShareTokenIntent = ShareTokenIntent.Backup; diff --git a/src/Storage/Storage/Common/SasTokenHelper.cs b/src/Storage/Storage/Common/SasTokenHelper.cs index bea108f6fff0..6b4f4882c8fd 100644 --- a/src/Storage/Storage/Common/SasTokenHelper.cs +++ b/src/Storage/Storage/Common/SasTokenHelper.cs @@ -14,23 +14,24 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Common { - using Microsoft.WindowsAzure.Commands.Storage.Model.Contract; - using Microsoft.Azure.Storage; - using Microsoft.Azure.Storage.Blob; - using XTable = Microsoft.Azure.Cosmos.Table; using System; using System.Collections.Generic; - using global::Azure.Storage.Sas; - using global::Azure.Storage.Blobs.Specialized; - using global::Azure.Storage.Blobs.Models; using System.Threading; - using global::Azure.Storage.Blobs; + using global::Azure.Core; using global::Azure.Storage; + using global::Azure.Storage.Blobs; + using global::Azure.Storage.Blobs.Models; + using global::Azure.Storage.Blobs.Specialized; using global::Azure.Storage.Files.DataLake; using global::Azure.Storage.Files.Shares; using global::Azure.Storage.Files.Shares.Models; - using global::Azure.Storage.Queues.Models; using global::Azure.Storage.Queues; + using global::Azure.Storage.Queues.Models; + using global::Azure.Storage.Sas; + using Microsoft.Azure.Storage; + using Microsoft.Azure.Storage.Blob; + using Microsoft.WindowsAzure.Commands.Storage.Model.Contract; + using XTable = Microsoft.Azure.Cosmos.Table; internal class SasTokenHelper { @@ -231,7 +232,8 @@ public static ShareSasBuilder SetShareSasBuilder_FromFile(ShareFileClient file, DateTime? StartTime = null, DateTime? ExpiryTime = null, string iPAddressOrRange = null, - string Protocol = null) + string Protocol = null, + string delegatedUserObjectId = null) { ShareSasBuilder sasBuilder = SetShareSasBuilder(file.ShareName, file.Path, @@ -240,7 +242,8 @@ public static ShareSasBuilder SetShareSasBuilder_FromFile(ShareFileClient file, StartTime, ExpiryTime, iPAddressOrRange, - Protocol); + Protocol, + delegatedUserObjectId); return sasBuilder; } @@ -253,7 +256,8 @@ public static ShareSasBuilder SetShareSasBuilder_FromShare(ShareClient share, DateTime? StartTime = null, DateTime? ExpiryTime = null, string iPAddressOrRange = null, - string Protocol = null) + string Protocol = null, + string delegatedUserObjectId = null) { ShareSasBuilder sasBuilder = SetShareSasBuilder(share.Name, null, @@ -262,7 +266,9 @@ public static ShareSasBuilder SetShareSasBuilder_FromShare(ShareClient share, StartTime, ExpiryTime, iPAddressOrRange, - Protocol); + Protocol, + null, + delegatedUserObjectId); return sasBuilder; } @@ -275,7 +281,8 @@ public static QueueSasBuilder SetQueueSasbuilder(QueueClient queue, DateTime? startTime = null, DateTime? expiryTime = null, string iPAddressOrRange = null, - string protocol = null) + string protocol = null, + string delegatedUserObjectId = null) { QueueSasBuilder sasBuilder = new QueueSasBuilder { @@ -371,6 +378,10 @@ public static QueueSasBuilder SetQueueSasbuilder(QueueClient queue, sasBuilder.Protocol = SasProtocol.Https; } } + if (delegatedUserObjectId != null) + { + sasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + } return sasBuilder; } @@ -385,7 +396,8 @@ public static ShareSasBuilder SetShareSasBuilder(string shareName, DateTime? ExpiryTime = null, string iPAddressOrRange = null, string Protocol = null, - string EncryptionScope = null) + string EncryptionScope = null, + string delegatedUserObjectId = null) { ShareSasBuilder sasBuilder; if (signedIdentifier != null) // Use save access policy @@ -487,37 +499,75 @@ public static ShareSasBuilder SetShareSasBuilder(string shareName, sasBuilder.Protocol = SasProtocol.Https; } } + if (delegatedUserObjectId != null) + { + sasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + } return sasBuilder; } /// /// Get SAS string /// - public static string GetFileSharedAccessSignature(AzureStorageContext context, ShareSasBuilder sasBuilder, CancellationToken cancelToken) + public static string GetFileSharedAccessSignature(AzureStorageContext context, ShareSasBuilder sasBuilder, bool generateUserDelegationSas, CancellationToken cancelToken) { if (context != null && context.StorageAccount != null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsSharedKey) { return sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential(context.StorageAccountName, context.StorageAccount.Credentials.ExportBase64EncodedKey())).ToString(); } - else + + if (generateUserDelegationSas) { - throw new InvalidOperationException("Create File service SAS only supported with SharedKey credential."); + if (context.StorageAccountName.StartsWith("[")) + { + throw new InvalidOperationException("Please provide '-Context' as a storage context created by cmdlet `New-AzStorageContext` with parameters include '-StorageAccountName'."); + } + global::Azure.Storage.Files.Shares.Models.UserDelegationKey userDelegationKey = null; + ShareServiceClient oauthService = new ShareServiceClient(context.StorageAccount.FileEndpoint, context.Track2OauthToken); + + Util.ValidateUserDelegationKeyStartEndTime(sasBuilder.StartsOn, sasBuilder.ExpiresOn); + + userDelegationKey = oauthService.GetUserDelegationKey( + startsOn: sasBuilder.StartsOn == DateTimeOffset.MinValue || sasBuilder.StartsOn == null ? DateTimeOffset.UtcNow : sasBuilder.StartsOn.ToUniversalTime(), + expiresOn: sasBuilder.ExpiresOn.ToUniversalTime(), + cancellationToken: cancelToken); + + return sasBuilder.ToSasQueryParameters(userDelegationKey, context.StorageAccountName).ToString(); } + + throw new InvalidOperationException("Create File service SAS only supported with SharedKey or OAuth token credential without using SAS token."); } /// /// Get Queue SAS string /// - public static string GetQueueSharedAccessSignature(AzureStorageContext context, QueueSasBuilder sasBuilder, CancellationToken cancellationToken) + public static string GetQueueSharedAccessSignature(AzureStorageContext context, QueueSasBuilder sasBuilder, bool generateUserDelegationSas, CancellationToken cancellationToken) { if (context != null && context.StorageAccount != null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsSharedKey) { return sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential(context.StorageAccountName, context.StorageAccount.Credentials.ExportBase64EncodedKey())).ToString(); } - else + + if (generateUserDelegationSas) { - throw new InvalidOperationException("Create Queue service SAS only supported with SharedKey credential."); + if (context.StorageAccountName.StartsWith("[")) + { + throw new InvalidOperationException("Please provide '-Context' as a storage context created by cmdlet `New-AzStorageContext` with parameters include '-StorageAccountName'."); + } + global::Azure.Storage.Queues.Models.UserDelegationKey userDelegationKey = null; + QueueServiceClient oauthService = new QueueServiceClient(context.StorageAccount.QueueEndpoint, context.Track2OauthToken); + + Util.ValidateUserDelegationKeyStartEndTime(sasBuilder.StartsOn, sasBuilder.ExpiresOn); + + userDelegationKey = oauthService.GetUserDelegationKey( + startsOn: sasBuilder.StartsOn == DateTimeOffset.MinValue || sasBuilder.StartsOn == null ? DateTimeOffset.UtcNow : sasBuilder.StartsOn.ToUniversalTime(), + expiresOn: sasBuilder.ExpiresOn.ToUniversalTime(), + cancellationToken: cancellationToken); + + return sasBuilder.ToSasQueryParameters(userDelegationKey, context.StorageAccountName).ToString(); } + + throw new InvalidOperationException("Create Queue service SAS only supported with SharedKey or OAuth token credential without using SAS token."); } @@ -531,7 +581,8 @@ public static BlobSasBuilder SetBlobSasBuilder_FromBlob(BlobBaseClient blobClien DateTime? ExpiryTime = null, string iPAddressOrRange = null, SharedAccessProtocol? Protocol = null, - string EncryptionScope = null) + string EncryptionScope = null, + string DelegatedUserObjectId = null) { BlobSasBuilder sasBuilder = SetBlobSasBuilder(blobClient.BlobContainerName, blobClient.Name, @@ -541,7 +592,8 @@ public static BlobSasBuilder SetBlobSasBuilder_FromBlob(BlobBaseClient blobClien ExpiryTime, iPAddressOrRange, Protocol, - EncryptionScope); + EncryptionScope, + DelegatedUserObjectId); if (Util.GetVersionIdFromBlobUri(blobClient.Uri) != null) { sasBuilder.BlobVersionId = Util.GetVersionIdFromBlobUri(blobClient.Uri); @@ -563,7 +615,8 @@ public static BlobSasBuilder SetBlobSasBuilder_FromContainer(BlobContainerClient DateTime? ExpiryTime = null, string iPAddressOrRange = null, SharedAccessProtocol? Protocol = null, - string EncryptionScope = null) + string EncryptionScope = null, + string DelegatedUserObjectId = null) { BlobSasBuilder sasBuilder = SetBlobSasBuilder(container.Name, null, @@ -573,7 +626,8 @@ public static BlobSasBuilder SetBlobSasBuilder_FromContainer(BlobContainerClient ExpiryTime, iPAddressOrRange, Protocol, - EncryptionScope); + EncryptionScope, + DelegatedUserObjectId); return sasBuilder; } @@ -588,7 +642,8 @@ public static BlobSasBuilder SetBlobSasBuilder(string containerName, DateTime? ExpiryTime = null, string iPAddressOrRange = null, SharedAccessProtocol? Protocol = null, - string EncryptionScope = null) + string EncryptionScope = null, + string DelegatedUserObjectId = null) { BlobSasBuilder sasBuilder; if (signedIdentifier != null) // Use save access policy @@ -643,7 +698,7 @@ public static BlobSasBuilder SetBlobSasBuilder(string containerName, } else { - sasBuilder = SetBlobPermission(sasBuilder, Permission); + sasBuilder.SetPermissions(Permission, true); } } } @@ -654,7 +709,7 @@ public static BlobSasBuilder SetBlobSasBuilder(string containerName, BlobContainerName = containerName, BlobName = blobName, }; - sasBuilder = SetBlobPermission(sasBuilder, Permission); + sasBuilder.SetPermissions(Permission, true); if (StartTime != null) { sasBuilder.StartsOn = StartTime.Value.ToUniversalTime(); @@ -694,53 +749,10 @@ public static BlobSasBuilder SetBlobSasBuilder(string containerName, { sasBuilder.EncryptionScope = EncryptionScope; } - return sasBuilder; - } - - /// - /// Set blob permission to SAS builder - /// - public static BlobSasBuilder SetBlobPermission(BlobSasBuilder sasBuilder, string rawPermission) - { - BlobContainerSasPermissions permission = 0; - foreach (char c in rawPermission) - { - switch (c) - { - case 'r': - permission = permission | BlobContainerSasPermissions.Read; - break; - case 'a': - permission = permission | BlobContainerSasPermissions.Add; - break; - case 'c': - permission = permission | BlobContainerSasPermissions.Create; - break; - case 'w': - permission = permission | BlobContainerSasPermissions.Write; - break; - case 'd': - permission = permission | BlobContainerSasPermissions.Delete; - break; - case 'l': - permission = permission | BlobContainerSasPermissions.List; - break; - case 't': - permission = permission | BlobContainerSasPermissions.Tag; - break; - case 'x': - permission = permission | BlobContainerSasPermissions.DeleteBlobVersion; - break; - case 'i': - permission = permission | BlobContainerSasPermissions.SetImmutabilityPolicy; - break; - default: - // Can't convert to permission supported by XSCL, so use raw permission string - sasBuilder.SetPermissions(rawPermission); - return sasBuilder; - } + if (DelegatedUserObjectId != null) + { + sasBuilder.DelegatedUserObjectId = DelegatedUserObjectId; } - sasBuilder.SetPermissions(permission); return sasBuilder; } @@ -773,7 +785,7 @@ public static string GetBlobSharedAccessSignature(AzureStorageContext context, B } else { - throw new InvalidOperationException("Create SAS only supported with SharedKey or Oauth credential."); + throw new InvalidOperationException("Create SAS only supported with SharedKey or OAuth token credential without using SAS token."); } } @@ -806,7 +818,7 @@ public static string GetDatalakeGen2SharedAccessSignature(AzureStorageContext co } else { - throw new InvalidOperationException("Create SAS only supported with SharedKey or Oauth credential."); + throw new InvalidOperationException("Create SAS only supported with SharedKey or OAuth token credential without using SAS token."); } } @@ -919,55 +931,11 @@ public static AccountSasResourceTypes GetAccountSasResourceTypes(SharedAccessAcc /// public static AccountSasBuilder SetAccountPermission(AccountSasBuilder sasBuilder, string rawPermission) { - AccountSasPermissions permission = 0; - foreach (char c in rawPermission) - { - switch (c) - { - case 'r': - permission = permission | AccountSasPermissions.Read; - break; - case 'a': - permission = permission | AccountSasPermissions.Add; - break; - case 'c': - permission = permission | AccountSasPermissions.Create; - break; - case 'w': - permission = permission | AccountSasPermissions.Write; - break; - case 'd': - permission = permission | AccountSasPermissions.Delete; - break; - case 'l': - permission = permission | AccountSasPermissions.List; - break; - case 'u': - permission = permission | AccountSasPermissions.Update; - break; - case 'p': - permission = permission | AccountSasPermissions.Process; - break; - case 't': - permission = permission | AccountSasPermissions.Tag; - break; - case 'f': - permission = permission | AccountSasPermissions.Filter; - break; - case 'x': - permission = permission | AccountSasPermissions.DeleteVersion; - break; - case 'i': - permission = permission | AccountSasPermissions.SetImmutabilityPolicy; - break; - case 'y': - permission = permission | AccountSasPermissions.PermanentDelete; - break; - default: - // Can't convert to permission supported by XSCL, so use raw permission string - sasBuilder.SetPermissions(rawPermission); - return sasBuilder; - } + if (!AccountSasPermissions.TryParse(rawPermission, out AccountSasPermissions permission)) + { + // Can't convert to permission supported by XSCL, so use raw permission string + sasBuilder.SetPermissions(rawPermission); + return sasBuilder; } sasBuilder.SetPermissions(permission); return sasBuilder; diff --git a/src/Storage/Storage/Common/Util.cs b/src/Storage/Storage/Common/Util.cs index d4140c0d464a..49e13a14bee8 100644 --- a/src/Storage/Storage/Common/Util.cs +++ b/src/Storage/Storage/Common/Util.cs @@ -487,20 +487,48 @@ public static BlobBaseClient GetTrack2BlobClient(Uri blobUri, AzureStorageContex { if (blobType == null) { - blobClient = new BlobBaseClient(blobUri, options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new BlobBaseClient(blobUri, context.Track2OauthToken, options); + } + else + { + blobClient = new BlobBaseClient(blobUri, options); + } } else { switch (blobType.Value) { case global::Azure.Storage.Blobs.Models.BlobType.Page: - blobClient = new PageBlobClient(blobUri, options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new PageBlobClient(blobUri, context.Track2OauthToken, options); + } + else + { + blobClient = new PageBlobClient(blobUri, options); + } break; case global::Azure.Storage.Blobs.Models.BlobType.Append: - blobClient = new AppendBlobClient(blobUri, options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new AppendBlobClient(blobUri, context.Track2OauthToken, options); + } + else + { + blobClient = new AppendBlobClient(blobUri, options); + } break; default: //Block - blobClient = new BlockBlobClient(blobUri, options); + if (context != null && context.Track2OauthToken != null) + { + blobClient = new BlockBlobClient(blobUri, context.Track2OauthToken, options); + } + else + { + blobClient = new BlockBlobClient(blobUri, options); + } break; } } @@ -525,11 +553,19 @@ public static BlobServiceClient GetTrack2BlobServiceClient(AzureStorageContext c string connectionString = context.ConnectionString; // remove the "?" at the begin of SAS if any + bool withSasToken = false; if (context != null && context.StorageAccount != null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsSAS) { + withSasToken = true; connectionString = connectionString.Replace("SharedAccessSignature=?", "SharedAccessSignature="); } + blobServiceClient = new BlobServiceClient(connectionString, options); + + if (withSasToken && context.Track2OauthToken != null) + { + blobServiceClient = new BlobServiceClient(blobServiceClient.Uri, context.Track2OauthToken, options); + } } return blobServiceClient; } @@ -731,13 +767,14 @@ public static ShareServiceClient GetTrack2FileServiceClient(AzureStorageContext throw new ArgumentException(Resources.DefaultStorageCredentialsNotFound); } + if (context.ShareTokenIntent != null) + { + options.ShareTokenIntent = context.ShareTokenIntent.Value; + } + ShareServiceClient shareServiceClient; - if (context.StorageAccount!= null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsToken) //Oauth + if (context.StorageAccount!= null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsToken) //Oauth { - if (context.ShareTokenIntent != null) - { - options.ShareTokenIntent = context.ShareTokenIntent.Value; - } shareServiceClient = new ShareServiceClient(context.StorageAccount.FileEndpoint, context.Track2OauthToken, options); } else //sas , key or Anonymous, use connection string @@ -748,8 +785,21 @@ public static ShareServiceClient GetTrack2FileServiceClient(AzureStorageContext if (context != null && context.StorageAccount != null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsSAS) { connectionString = connectionString.Replace("SharedAccessSignature=?", "SharedAccessSignature="); + shareServiceClient = new ShareServiceClient(connectionString, options); + + if (context != null && context.Track2OauthToken != null) + { + if (context.ShareTokenIntent != null) + { + options.ShareTokenIntent = context.ShareTokenIntent.Value; + } + shareServiceClient = new ShareServiceClient(shareServiceClient.Uri, context.Track2OauthToken, options); + } + } + else + { + shareServiceClient = new ShareServiceClient(connectionString, options); } - shareServiceClient = new ShareServiceClient(connectionString, options); } return shareServiceClient; } @@ -786,8 +836,17 @@ public static QueueServiceClient GetTrack2QueueServiceClient(AzureStorageContext if (context != null && context.StorageAccount != null && context.StorageAccount.Credentials != null && context.StorageAccount.Credentials.IsSAS) { connectionString = connectionString.Replace("SharedAccessSignature=?", "SharedAccessSignature="); + queueServiceClient = new QueueServiceClient(connectionString, options); + + if (context != null && context.Track2OauthToken != null) + { + queueServiceClient = new QueueServiceClient(queueServiceClient.Uri, context.Track2OauthToken, options); + } + } + else + { + queueServiceClient = new QueueServiceClient(connectionString, options); } - queueServiceClient = new QueueServiceClient(connectionString, options); } return queueServiceClient; } diff --git a/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2ChildItem.cs b/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2ChildItem.cs index d5a1f4ca6cc0..bbf5e6e72bbd 100644 --- a/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2ChildItem.cs +++ b/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2ChildItem.cs @@ -123,7 +123,13 @@ public override void ExecuteCmdlet() Page page; do { - IEnumerator> enumerator = fileSystem.GetPaths(this.Path, this.Recurse, this.OutputUserPrincipalName.IsPresent) + DataLakeGetPathsOptions options = new DataLakeGetPathsOptions() + { + Path = this.Path, + Recursive = this.Recurse, + UserPrincipalName = this.OutputUserPrincipalName.IsPresent + }; + IEnumerator> enumerator = fileSystem.GetPaths(options) .AsPages(this.ContinuationToken, listCount) .GetEnumerator(); diff --git a/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2FileContent.cs b/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2FileContent.cs index ce85ba371d59..70b76dfb3bb5 100644 --- a/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2FileContent.cs +++ b/src/Storage/Storage/DatalakeGen2/Cmdlet/GetAzDataLakeGen2FileContent.cs @@ -12,20 +12,22 @@ // limitations under the License. // ---------------------------------------------------------------------------------- -using Microsoft.WindowsAzure.Commands.Common; -using Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel; -using Microsoft.WindowsAzure.Commands.Storage.Common; -using Microsoft.WindowsAzure.Commands.Storage.Model.Contract; -using Microsoft.WindowsAzure.Commands.Utilities.Common; -using Microsoft.Azure.Storage; -using Microsoft.Azure.Storage.Blob; -using Microsoft.Azure.Storage.DataMovement; using System; using System.IO; using System.Management.Automation; using System.Security.Permissions; using System.Threading.Tasks; +using Azure.Storage; +using Azure.Storage.Blobs.Specialized; using Azure.Storage.Files.DataLake; +using Microsoft.Azure.Storage; +using Microsoft.Azure.Storage.Blob; +using Microsoft.Azure.Storage.DataMovement; +using Microsoft.WindowsAzure.Commands.Common; +using Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel; +using Microsoft.WindowsAzure.Commands.Storage.Common; +using Microsoft.WindowsAzure.Commands.Storage.Model.Contract; +using Microsoft.WindowsAzure.Commands.Utilities.Common; namespace Microsoft.WindowsAzure.Commands.Storage.Blob.Cmdlet { @@ -146,6 +148,35 @@ await DataMovementTransferHelper.DoTransfer(() => WriteDataLakeGen2Item(localChannel, fileClient, taskId: data.TaskId); } + /// + /// Download blob to local file + /// + /// Task id + /// IStorageBlobManagement channel object + /// Source blob object + /// Destination file path + internal virtual async Task DownloadBlob(long taskId, IStorageBlobManagement localChannel, BlobBaseClient blob, string filePath) + { + string activity = String.Format(Resources.ReceiveAzureBlobActivity, blob.Name, filePath); + string status = Resources.PrepareDownloadingBlob; + var blobProperties = await blob.GetPropertiesAsync(cancellationToken: CmdletCancellationToken).ConfigureAwait(false); + + if (this.Force.IsPresent + || !System.IO.File.Exists(filePath) + || ShouldContinue(string.Format(Resources.OverwriteConfirmation, filePath), null)) + { + StorageTransferOptions transferOption = new StorageTransferOptions() + { + MaximumConcurrency = this.GetCmdletConcurrency(), + MaximumTransferSize = size4MB, + InitialTransferSize = size4MB + }; + await blob.DownloadToAsync(filePath, BlobRequestConditions, transferOption, CmdletCancellationToken).ConfigureAwait(false); + } + + WriteDataLakeGen2Item(localChannel, fileClient, taskId); + } + /// /// get blob content /// @@ -183,6 +214,41 @@ internal void GetBlobContent(CloudBlob blob, string fileName, bool isValidBlob = RunTask(taskGenerator); } + /// + /// get blob content + /// + /// source CloudBlob object + /// destination file path + /// whether the source FileSystem validated + /// the downloaded blob object + internal void GetBlobContent(BlobBaseClient blob, string fileName, bool isValidBlob = false) + { + if (null == blob) + { + throw new ArgumentNullException(typeof(BlobBaseClient).Name, String.Format(Resources.ObjectCannotBeNull, typeof(BlobBaseClient).Name)); + } + + string filePath = GetFullReceiveFilePath(fileName, blob.Name); + + if (!isValidBlob) + { + ValidatePipelineCloudBlobTrack2(blob); + } + + //create the destination directory if not exists. + String dirPath = System.IO.Path.GetDirectoryName(filePath); + + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + + IStorageBlobManagement localChannel = Channel; + + Func taskGenerator = (taskId) => DownloadBlob(taskId, localChannel, blob, filePath); + RunTask(taskGenerator); + } + /// /// get full file path according to the specified file name /// @@ -214,7 +280,6 @@ internal string GetFullReceiveFilePath(string fileName, string blobName) } //there is no need to check the read/write permission on the specified file path, the data movement library will do that - return filePath; } @@ -247,7 +312,8 @@ public override void ExecuteCmdlet() DoBeginProcessing(); } - CloudBlockBlob blob = null; + CloudBlockBlob track1Blob = null; + BlockBlobClient track2Blob = null; if (ParameterSetName == ManualParameterSet) { DataLakeFileSystemClient fileSystem = GetFileSystemClientByName(localChannel, this.FileSystem); @@ -257,8 +323,16 @@ public override void ExecuteCmdlet() throw new ArgumentException(String.Format("The input FileSystem '{0}', path '{1}' point to a Directory, can't download it.", this.FileSystem, this.Path)); } - CloudBlobContainer container = GetCloudBlobContainerByName(Channel, this.FileSystem).ConfigureAwait(false).GetAwaiter().GetResult(); - blob = container.GetBlockBlobReference(this.Path); + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.Track2OauthToken != null) + { + var blobServiceClient = Channel.GetBlobServiceClient(); + track2Blob = blobServiceClient.GetBlobContainerClient(this.FileSystem).GetBlockBlobClient(this.Path); + } + else + { + CloudBlobContainer container = GetCloudBlobContainerByName(Channel, this.FileSystem).ConfigureAwait(false).GetAwaiter().GetResult(); + track1Blob = container.GetBlockBlobReference(this.Path); + } } else //BlobParameterSet { @@ -268,11 +342,18 @@ public override void ExecuteCmdlet() Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsSAS) { // For SAS, the Uri already contains the sas token, so can't repeatedly inout the credential - blob = new CloudBlockBlob(InputObject.File.Uri); + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.Track2OauthToken != null) + { + track2Blob = new BlockBlobClient(InputObject.File.Uri, Channel.StorageContext.Track2OauthToken); + } + else + { + track1Blob = new CloudBlockBlob(InputObject.File.Uri); + } } else { - blob = new CloudBlockBlob(InputObject.File.Uri, Channel.StorageContext.StorageAccount.Credentials); + track1Blob = new CloudBlockBlob(InputObject.File.Uri, Channel.StorageContext.StorageAccount.Credentials); } fileClient = InputObject.File; } @@ -282,7 +363,14 @@ public override void ExecuteCmdlet() } } - GetBlobContent(blob, FileName, true); + if (track1Blob != null) + { + GetBlobContent(track1Blob, FileName, true); + } + else + { + GetBlobContent(track2Blob, FileName, true); + } if (AsJob.IsPresent) { diff --git a/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2Item.cs b/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2Item.cs index af921b6a1100..ef561750f556 100644 --- a/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2Item.cs +++ b/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2Item.cs @@ -222,7 +222,7 @@ public override void ExecuteCmdlet() // 2. file encryption context is set by user if ((Channel.StorageContext.StorageAccount != null && Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsSAS && (!string.IsNullOrEmpty(this.Permission) || !string.IsNullOrEmpty(this.Umask))) || - this.EncryptionContext != null) + this.EncryptionContext != null || Channel.StorageContext.Track2OauthToken != null) { Func taskGenerator = (taskId) => UploadDataLakeFile(taskId, fileClient, ResolvedFileName); RunTask(taskGenerator); diff --git a/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2SasToken.cs b/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2SasToken.cs index c611f1de4c06..3ed63f1a4923 100644 --- a/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2SasToken.cs +++ b/src/Storage/Storage/DatalakeGen2/Cmdlet/NewAzDataLakeGen2SasToken.cs @@ -62,6 +62,10 @@ public class NewDataLakeGen2SasTokenCommand : StorageCloudBlobCmdletBase [ValidateNotNullOrEmpty] public string Permission { get; set; } + [Parameter(Mandatory = false, HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Protocol can be used in the request with this SAS token.")] [ValidateNotNull] public SasProtocol? Protocol { get; set; } @@ -116,10 +120,9 @@ public override void ExecuteCmdlet() { IStorageBlobManagement localChannel = Channel; - // When the input context is Oauth bases, can't generate normal SAS, but UserDelegationSas + // When the input context is OAuth bases, can't generate normal SAS, but UserDelegationSas bool generateUserDelegationSas = false; - if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.StorageAccount != null && - Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsToken) + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsToken) { if (ShouldProcess(this.Path, "Generate User Delegation SAS, since input Storage Context is OAuth based.")) { @@ -130,6 +133,13 @@ public override void ExecuteCmdlet() return; } } + else + { + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } + } if (this.ParameterSetName == ItemParameterSet) { @@ -180,6 +190,10 @@ public override void ExecuteCmdlet() { sasBuilder.EncryptionScope = this.EncryptionScope; } + if (this.DelegatedUserObjectId != null) + { + sasBuilder.DelegatedUserObjectId = this.DelegatedUserObjectId; + } DataLakeFileSystemClient fileSystem = GetFileSystemClientByName(localChannel, this.FileSystem); diff --git a/src/Storage/Storage/File/AzureStorageFileCmdletBase.cs b/src/Storage/Storage/File/AzureStorageFileCmdletBase.cs index fee296c470cd..563b839fedc8 100644 --- a/src/Storage/Storage/File/AzureStorageFileCmdletBase.cs +++ b/src/Storage/Storage/File/AzureStorageFileCmdletBase.cs @@ -128,7 +128,9 @@ public ShareClientOptions createClientOptions() protected bool WithOauthCredential() { - if(this.Channel != null && this.Channel.StorageContext != null && this.Channel.StorageContext.StorageAccount != null && this.Channel.StorageContext.StorageAccount.Credentials.IsToken) + if (this.Channel != null && this.Channel.StorageContext != null && + ((this.Channel.StorageContext.StorageAccount != null && this.Channel.StorageContext.StorageAccount.Credentials.IsToken) + || (this.Channel.StorageContext.Track2OauthToken != null))) { return true; } diff --git a/src/Storage/Storage/File/Cmdlet/NewAzureStorageFileSasToken.cs b/src/Storage/Storage/File/Cmdlet/NewAzureStorageFileSasToken.cs index fca9f40168af..97e7295235f4 100644 --- a/src/Storage/Storage/File/Cmdlet/NewAzureStorageFileSasToken.cs +++ b/src/Storage/Storage/File/Cmdlet/NewAzureStorageFileSasToken.cs @@ -89,6 +89,12 @@ public string Policy } private string accessPolicyIdentifier; + [Parameter( + Mandatory = false, + HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter( Mandatory = false, HelpMessage = "Permissions for a file. Permissions can be any subset of \"rwd\".", @@ -152,10 +158,29 @@ public override void ExecuteCmdlet() fileClient = shareClient.GetRootDirectoryClient().GetFileClient(this.Path); } - - if (this.Context != null && this.Context is AzureStorageContext && ((AzureStorageContext)this.Context).StorageAccount != null && !((AzureStorageContext)this.Context).StorageAccount.Credentials.IsSharedKey) + // When the input context is OAuth bases, can't generate normal SAS, but UserDelegationSas + bool generateUserDelegationSas = false; + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.StorageAccount != null && Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsToken) + { + if (ShouldProcess(Path, "Generate User Delegation SAS, since input Storage Context is OAuth based.")) + { + generateUserDelegationSas = true; + if (!string.IsNullOrEmpty(this.Policy)) + { + throw new ArgumentException("When input Storage Context is OAuth based, Saved Policy is not supported.", "Policy"); + } + } + else + { + return; + } + } + else { - throw new InvalidOperationException("Create File service SAS only supported with SharedKey credential."); + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } } // Get share saved policy if any @@ -166,10 +191,10 @@ public override void ExecuteCmdlet() } //Create SAS builder - ShareSasBuilder sasBuilder = SasTokenHelper.SetShareSasBuilder_FromFile(fileClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol); + ShareSasBuilder sasBuilder = SasTokenHelper.SetShareSasBuilder_FromFile(fileClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.DelegatedUserObjectId); //Create SAS and output it - string sasToken = SasTokenHelper.GetFileSharedAccessSignature((AzureStorageContext)this.Context, sasBuilder, CmdletCancellationToken); + string sasToken = SasTokenHelper.GetFileSharedAccessSignature((AzureStorageContext)this.Context, sasBuilder, generateUserDelegationSas, CmdletCancellationToken); // remove prefix "?" of SAS if any sasToken = Util.GetSASStringWithoutQuestionMark(sasToken); diff --git a/src/Storage/Storage/File/Cmdlet/NewAzureStorageShareSasToken.cs b/src/Storage/Storage/File/Cmdlet/NewAzureStorageShareSasToken.cs index 1d17a7c13f5e..425040a2a3c7 100644 --- a/src/Storage/Storage/File/Cmdlet/NewAzureStorageShareSasToken.cs +++ b/src/Storage/Storage/File/Cmdlet/NewAzureStorageShareSasToken.cs @@ -14,15 +14,16 @@ namespace Microsoft.WindowsAzure.Commands.Storage.File.Cmdlet { - using Azure.Commands.Common.Authentication.Abstractions; - using Microsoft.WindowsAzure.Commands.Storage.Common; using System; + using System.IO; using System.Management.Automation; using System.Security.Permissions; + using Azure.Commands.Common.Authentication.Abstractions; + using global::Azure.Storage; using global::Azure.Storage.Files.Shares; - using global::Azure.Storage.Sas; using global::Azure.Storage.Files.Shares.Models; - using global::Azure.Storage; + using global::Azure.Storage.Sas; + using Microsoft.WindowsAzure.Commands.Storage.Common; [Cmdlet("New", Azure.Commands.ResourceManager.Common.AzureRMConstants.AzurePrefix + "StorageShareSASToken"), OutputType(typeof(String))] public class NewAzureStorageShareSasToken : AzureStorageFileCmdletBase @@ -54,6 +55,10 @@ public string Policy } private string accessPolicyIdentifier; + [Parameter(Mandatory = false, HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Permissions for a share. Permissions can be any subset of \"rwdl\".", ParameterSetName = SasPermissionParameterSet)] [ValidateNotNullOrEmpty] @@ -100,16 +105,36 @@ public override void ExecuteCmdlet() { if (String.IsNullOrEmpty(ShareName)) return; - if (Channel.StorageContext != null && Channel.StorageContext.StorageAccount != null && !Channel.StorageContext.StorageAccount.Credentials.IsSharedKey) - { - throw new InvalidOperationException("Create File service SAS only supported with SharedKey credential."); - } - ShareClient share = Util.GetTrack2ShareReference(this.ShareName, (AzureStorageContext)this.Context, snapshotTime: null, ClientOptions); + // When the input context is OAuth bases, can't generate normal SAS, but UserDelegationSas + bool generateUserDelegationSas = false; + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsToken) + { + if (ShouldProcess(ShareName, "Generate User Delegation SAS, since input Storage Context is OAuth based.")) + { + generateUserDelegationSas = true; + if (!string.IsNullOrEmpty(this.Policy)) + { + throw new ArgumentException("When input Storage Context is OAuth based, Saved Policy is not supported.", "Policy"); + } + } + else + { + return; + } + } + else + { + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } + } + // Get share saved policy if any ShareSignedIdentifier identifier = null; if (ParameterSetName == SasPolicyParmeterSet) @@ -118,10 +143,10 @@ public override void ExecuteCmdlet() } //Create SAS builder - ShareSasBuilder sasBuilder = SasTokenHelper.SetShareSasBuilder_FromShare(share, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol); + ShareSasBuilder sasBuilder = SasTokenHelper.SetShareSasBuilder_FromShare(share, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.DelegatedUserObjectId); //Create SAS and output it - string sasToken = SasTokenHelper.GetFileSharedAccessSignature(Channel.StorageContext, sasBuilder, CmdletCancellationToken); + string sasToken = SasTokenHelper.GetFileSharedAccessSignature(Channel.StorageContext, sasBuilder, generateUserDelegationSas, CmdletCancellationToken); // remove prefix "?" of SAS if any sasToken = Util.GetSASStringWithoutQuestionMark(sasToken); diff --git a/src/Storage/Storage/File/Cmdlet/SetAzureStorageFileContent.cs b/src/Storage/Storage/File/Cmdlet/SetAzureStorageFileContent.cs index 32143e94996a..0fbf501bbb62 100644 --- a/src/Storage/Storage/File/Cmdlet/SetAzureStorageFileContent.cs +++ b/src/Storage/Storage/File/Cmdlet/SetAzureStorageFileContent.cs @@ -136,8 +136,6 @@ public override void ExecuteCmdlet() bool isDirectory; string[] path = NamingUtil.ValidatePath(this.Path, out isDirectory); - var cloudFileToBeUploaded = - BuildCloudFileInstanceFromPathAsync(localFile.Name, path, isDirectory).ConfigureAwait(false).GetAwaiter().GetResult(); var fileClientToBeUploaded = BuildShareFileClientInstanceFromPathAsync(localFile.Name, path, isDirectory).ConfigureAwait(false).GetAwaiter().GetResult(); @@ -147,7 +145,9 @@ public override void ExecuteCmdlet() && !WithOauthCredential() && (this.DisAllowTrailingDot.IsPresent || !Util.PathContainsTrailingDot(fileClientToBeUploaded.Path)) && this.FileMode == null && this.Owner == null && this.Group == null) - { + { + var cloudFileToBeUploaded = + BuildCloudFileInstanceFromPathAsync(localFile.Name, path, isDirectory).ConfigureAwait(false).GetAwaiter().GetResult(); if (ShouldProcess(cloudFileToBeUploaded.Name, "Set file content")) { var progressRecord = new ProgressRecord( @@ -179,7 +179,6 @@ await DataMovementTransferHelper.DoTransfer(() => } else // use Track2 SDK { - if (ShouldProcess(fileClientToBeUploaded.Path, "Set file content")) { var progressRecord = new ProgressRecord( diff --git a/src/Storage/Storage/File/Cmdlet/StartAzureStorageFileCopy.cs b/src/Storage/Storage/File/Cmdlet/StartAzureStorageFileCopy.cs index 1f88746460eb..31266593f5c5 100644 --- a/src/Storage/Storage/File/Cmdlet/StartAzureStorageFileCopy.cs +++ b/src/Storage/Storage/File/Cmdlet/StartAzureStorageFileCopy.cs @@ -311,6 +311,12 @@ public override void ExecuteCmdlet() blobChannel = this.GetBlobChannel(); destChannel = GetDestinationChannel(); IStorageFileManagement srcChannel = Channel; + + if (srcChannel.IsSasWithOAuthCredential() || destChannel.IsSasWithOAuthCredential()) + { + throw new InvalidOperationException("File Async copy doesn't support user delegation SAS with OAuth credential."); + } + Action copyAction = null; string target = this.DestShareFileClient != null ? this.DestShareFileClient.Name : DestFilePath; switch (ParameterSetName) diff --git a/src/Storage/Storage/Model/Contract/IStorageBlobManagement.cs b/src/Storage/Storage/Model/Contract/IStorageBlobManagement.cs index 9a8ba47a6650..828e9f45482c 100644 --- a/src/Storage/Storage/Model/Contract/IStorageBlobManagement.cs +++ b/src/Storage/Storage/Model/Contract/IStorageBlobManagement.cs @@ -30,6 +30,8 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Model.Contract /// public interface IStorageBlobManagement : IStorageManagement { + bool IsSasWithOAuthCredential(); + /// /// Get a list of cloudblobcontainer in azure /// diff --git a/src/Storage/Storage/Model/Contract/IStorageFileManagement.cs b/src/Storage/Storage/Model/Contract/IStorageFileManagement.cs index 3257f0e3dbcd..69106f8743a2 100644 --- a/src/Storage/Storage/Model/Contract/IStorageFileManagement.cs +++ b/src/Storage/Storage/Model/Contract/IStorageFileManagement.cs @@ -25,6 +25,8 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Model.Contract /// public interface IStorageFileManagement : IStorageManagement { + bool IsSasWithOAuthCredential(); + /// /// Returns a reference to a Microsoft.Azure.Storage.File.CloudFileShare /// object with the specified name. diff --git a/src/Storage/Storage/Model/Contract/StorageBlobManagement.cs b/src/Storage/Storage/Model/Contract/StorageBlobManagement.cs index 9261b69c2e3b..5e07cfa25c8f 100644 --- a/src/Storage/Storage/Model/Contract/StorageBlobManagement.cs +++ b/src/Storage/Storage/Model/Contract/StorageBlobManagement.cs @@ -14,23 +14,24 @@ namespace Microsoft.WindowsAzure.Commands.Storage.Model.Contract { - using Microsoft.WindowsAzure.Commands.Common.Storage; - using Microsoft.WindowsAzure.Commands.Storage.Common; + using global::Azure.Storage; + using global::Azure.Storage.Blobs; + using Microsoft.Azure.Cosmos.Table; using Microsoft.Azure.Storage; - using XSCL = Microsoft.Azure.Storage; using Microsoft.Azure.Storage.Blob; using Microsoft.Azure.Storage.File; using Microsoft.Azure.Storage.File.Protocol; using Microsoft.Azure.Storage.Queue; - using XSCLProtocol = Microsoft.Azure.Storage.Shared.Protocol; - using XTable = Microsoft.Azure.Cosmos.Table; - using Microsoft.Azure.Cosmos.Table; + using Microsoft.OData.UriParser; + using Microsoft.WindowsAzure.Commands.Common.Storage; + using Microsoft.WindowsAzure.Commands.Storage.Common; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - using global::Azure.Storage.Blobs; - using global::Azure.Storage; + using XSCL = Microsoft.Azure.Storage; + using XSCLProtocol = Microsoft.Azure.Storage.Shared.Protocol; + using XTable = Microsoft.Azure.Cosmos.Table; /// /// Blob management @@ -66,6 +67,10 @@ private CloudBlobClient BlobClient return this.blobClient; } } + public bool IsSasWithOAuthCredential() + { + return this.BlobClient != null && this.BlobClient.Credentials.IsSAS && this.StorageContext != null && this.StorageContext.Track2OauthToken != null; + } /// /// The azure storage context associated with this IStorageBlobManagement @@ -179,7 +184,25 @@ public BlobServiceClient GetBlobServiceClient(BlobClientOptions options = null) } else //sas, Anonymous { - blobServiceClient = new BlobServiceClient(this.StorageContext.StorageAccount.BlobEndpoint, options); + string blobEndpointWithSas = this.StorageContext.StorageAccount.BlobEndpoint.ToString(); + if (this.StorageContext.StorageAccount.Credentials.SASToken != null) + { + string sasToken = this.StorageContext.StorageAccount.Credentials.SASToken; + if (!string.IsNullOrEmpty(sasToken) && !sasToken.StartsWith("?")) + { + sasToken = "?" + sasToken; + } + blobEndpointWithSas += sasToken; + } + + if (this.StorageContext != null && this.StorageContext.Track2OauthToken != null) + { + blobServiceClient = new BlobServiceClient(new Uri(blobEndpointWithSas), this.StorageContext.Track2OauthToken, options); + } + else + { + blobServiceClient = new BlobServiceClient(new Uri(blobEndpointWithSas), options); + } } } return blobServiceClient; diff --git a/src/Storage/Storage/Model/Contract/StorageFileManagement.cs b/src/Storage/Storage/Model/Contract/StorageFileManagement.cs index 534d8799f740..1cdf8cfbf951 100644 --- a/src/Storage/Storage/Model/Contract/StorageFileManagement.cs +++ b/src/Storage/Storage/Model/Contract/StorageFileManagement.cs @@ -68,5 +68,9 @@ public Task DirectoryExistsAsync(CloudFileDirectory directory, FileRequest { return directory.ExistsAsync(options, operationContext, cancellationToken); } + public bool IsSasWithOAuthCredential() + { + return this.Client != null && this.Client.Credentials.IsSAS && this.StorageContext != null && this.StorageContext.Track2OauthToken != null; + } } } diff --git a/src/Storage/Storage/Queue/Cmdlet/NewAzureStorageQueueSasToken.cs b/src/Storage/Storage/Queue/Cmdlet/NewAzureStorageQueueSasToken.cs index 3817d0cf5310..4c8e4aa9a0c0 100644 --- a/src/Storage/Storage/Queue/Cmdlet/NewAzureStorageQueueSasToken.cs +++ b/src/Storage/Storage/Queue/Cmdlet/NewAzureStorageQueueSasToken.cs @@ -61,6 +61,10 @@ public string Policy [ValidateNotNullOrEmpty] public string Permission { get; set; } + [Parameter(Mandatory = false, HelpMessage = "This value specifies the Entra ID of the user who is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. This parameter can only be specified when input Storage Context is OAuth based.")] + [ValidateNotNullOrEmpty] + public string DelegatedUserObjectId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Protocol can be used in the request with this SAS token.")] [ValidateSet("HttpsOnly", "HttpsOrHttp", IgnoreCase = true),] public string Protocol { get; set; } @@ -110,6 +114,31 @@ public override void ExecuteCmdlet() { if (String.IsNullOrEmpty(Name)) return; + // When the input context is OAuth based, can't generate normal SAS, but UserDelegationSas + bool generateUserDelegationSas = false; + if (Channel != null && Channel.StorageContext != null && Channel.StorageContext.StorageAccount.Credentials != null && Channel.StorageContext.StorageAccount.Credentials.IsToken) + { + if (ShouldProcess(Name, "Generate User Delegation SAS, since input Storage Context is OAuth based.")) + { + generateUserDelegationSas = true; + if (!string.IsNullOrEmpty(accessPolicyIdentifier) || !string.IsNullOrEmpty(this.Policy)) + { + throw new ArgumentException("When input Storage Context is OAuth based, Saved Policy is not supported.", "Policy"); + } + } + else + { + return; + } + } + else + { + if (this.DelegatedUserObjectId != null) + { + throw new ArgumentException("DelegatedUserObjectId can only be specified when input Storage Context is OAuth based without using SAS token.", "DelegatedUserObjectId"); + } + } + QueueClient queueClient = Util.GetTrack2QueueClient(this.Name, (AzureStorageContext)this.Context, this.ClientOptions); QueueSignedIdentifier identifier = null; if (!string.IsNullOrEmpty(this.Policy)) @@ -117,8 +146,8 @@ public override void ExecuteCmdlet() identifier = SasTokenHelper.GetQueueSignedIdentifier(queueClient, this.Policy, CmdletCancellationToken); } - QueueSasBuilder sasBuilder = SasTokenHelper.SetQueueSasbuilder(queueClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol); - string sasToken = SasTokenHelper.GetQueueSharedAccessSignature((AzureStorageContext)this.Context, sasBuilder, CmdletCancellationToken); + QueueSasBuilder sasBuilder = SasTokenHelper.SetQueueSasbuilder(queueClient, identifier, this.Permission, this.StartTime, this.ExpiryTime, this.IPAddressOrRange, this.Protocol, this.DelegatedUserObjectId); + string sasToken = SasTokenHelper.GetQueueSharedAccessSignature((AzureStorageContext)this.Context, sasBuilder, generateUserDelegationSas, CmdletCancellationToken); // remove prefix "?" of SAS if any sasToken = Util.GetSASStringWithoutQuestionMark(sasToken); diff --git a/src/Storage/Storage/Storage.csproj b/src/Storage/Storage/Storage.csproj index 17e50e790bcc..2f92ff798243 100644 --- a/src/Storage/Storage/Storage.csproj +++ b/src/Storage/Storage/Storage.csproj @@ -13,10 +13,10 @@ - - - - + + + +