RestSetAcls.psm1
. $PSScriptRoot/Convert.ps1 . $PSScriptRoot/SddlUtils.ps1 . $PSScriptRoot/PrintUtils.ps1 Import-Module $PSScriptRoot/Interop.psm1 function Write-LiveFilesAndFoldersProcessingStatus { [OutputType([int])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "FileOrFolder", Justification = "We don't print `$FileOrFolder but we do want to iterate over it")] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Object[]]$FileOrFolder, [Parameter(Mandatory = $true)] [datetime]$StartTime, [Parameter(Mandatory = $false)] [int]$RefreshRateHertz = 10 ) begin { $i = 0 $failures = 0 $msBetweenPrints = 1000 / $RefreshRateHertz $lastPrint = (Get-Date).AddMilliseconds(-$msBetweenPrints) $overwriteLine = -not (Get-IsPowerShellIse) } process { $i++ $timeSinceLastPrint = (Get-Date) - $lastPrint if (-not $_.Success) { $failures++ } # To avoid overloading gui, only print at most every $msBetweenPrints # On a test with 6K files, printing at 60Hz saved ~20% perf compared to printing every update if ($timeSinceLastPrint.TotalMilliseconds -gt $msBetweenPrints) { $now = Get-Date $timeSinceStart = $now - $StartTime $itemsPerSec = [math]::Round($i / $timeSinceStart.TotalSeconds, 1) if ($overwriteLine) { Write-Host "`r" -NoNewline } Write-Host "Set " -ForegroundColor DarkGray -NoNewline Write-Host ($i - $failures) -ForegroundColor Blue -NoNewline Write-Host " permissions" -ForegroundColor DarkGray -NoNewline if ($failures -gt 0) { Write-Host ", " -ForegroundColor DarkGray -NoNewline Write-Host $failures -ForegroundColor Red -NoNewline Write-Host " failures" -ForegroundColor DarkGray -NoNewline } Write-Host " (" -ForegroundColor DarkGray -NoNewline Write-Host $itemsPerSec -ForegroundColor Blue -NoNewline Write-Host " items/s)" -ForegroundColor DarkGray -NoNewline if (-not $overwriteLine) { Write-Host } $lastPrint = Get-Date } Write-Output $_ } } function Write-FinalFilesAndFoldersProcessed { [OutputType([System.Void])] param ( [Parameter(Mandatory = $true)] [int]$ProcessedCount, [Parameter(Mandatory = $true)] [hashtable]$Errors, [Parameter(Mandatory = $true)] [timespan]$TotalTime, [Parameter(Mandatory = $false)] [int]$MaxErrorsToShow = 10 ) $successCount = $ProcessedCount - $Errors.Count $errorCount = $Errors.Count $seconds = [math]::Round($TotalTime.TotalSeconds, 2) if ($errorCount -gt 0) { if ($errorCount -eq $processedCount) { # Setting ACLs failed for all files; report it. Write-FailedHeader Write-Host "Failed to set " -NoNewline Write-Host $errorCount -ForegroundColor Red -NoNewline Write-Host " permissions. Total time " -NoNewline Write-Host $seconds -ForegroundColor Blue -NoNewline Write-Host " seconds. Errors:" } else { Write-PartialHeader Write-Host "Set " -NoNewline Write-Host $successCount -ForegroundColor Blue -NoNewline Write-Host " permissions, " -NoNewline Write-Host $errorCount -ForegroundColor Red -NoNewline Write-Host " failures. Total time " -NoNewline Write-Host $seconds -ForegroundColor Blue -NoNewline Write-Host " seconds. Errors:" } Write-Host # Print first $maxErrorsToShow errors $Errors.GetEnumerator() | Select-Object -First $MaxErrorsToShow | ForEach-Object { Write-Host " $($_.Key): " -NoNewline Write-Host $_.Value -ForegroundColor Red } # Add a note if there are more errors if ($errorCount -gt $MaxErrorsToShow) { Write-Host " ... and " -NoNewline Write-Host ($errorCount - $MaxErrorsToShow) -ForegroundColor Red -NoNewline Write-Host " more errors" Write-Host } # Save all errors to a JSON file ConvertTo-Json $Errors | Out-File "errors.json" Write-Host " Full list of errors has been saved in " -NoNewline Write-Host "errors.json" -ForegroundColor Blue Write-Host } else { $itemsPerSec = [math]::Round($successCount / $TotalTime.TotalSeconds, 1) Write-DoneHeader Write-Host "Set " -NoNewline Write-Host $successCount -ForegroundColor Blue -NoNewline Write-Host " permissions in " -NoNewline Write-Host $seconds -ForegroundColor Blue -NoNewline Write-Host " seconds" -NoNewline Write-Host " (" -NoNewline Write-Host $itemsPerSec -ForegroundColor Blue -NoNewline Write-Host " items/s)" } } function Write-SddlWarning { param ( [Parameter(Mandatory = $true)] [string]$Sddl, [Parameter(Mandatory = $true)] [string]$NewSddl ) Write-WarningHeader Write-Host "The SDDL string has non-standard inheritance rules." Write-Host "It is recommended to set OI (Object Inherit) and CI (Container Inherit) on every permission. " -ForegroundColor DarkGray Write-Host "This ensures that the permissions are inherited by files and folders created in the future." -ForegroundColor DarkGray Write-Host Write-Host " Current: " -NoNewline -ForegroundColor Yellow Write-Host $Sddl Write-Host " Recommended: " -NoNewline -ForegroundColor Green Write-Host $NewSddl Write-Host Write-Host "Do you want to continue with the " -NoNewline Write-Host "current" -ForegroundColor Yellow -NoNewline Write-Host " SDDL?" -NoNewline return Ask "" } function Get-ShareName { param ( [Parameter(Mandatory = $true)] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File ) if ($File.GetType().Name -eq "AzureStorageFileDirectory") { return $File.ShareDirectoryClient.ShareName } else { return $File.ShareFileClient.ShareName } } function Get-ClientFromFile { param ( [Parameter(Mandatory = $true)] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File ) if ($File -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFileDirectory]) { return $File.ShareDirectoryClient } elseif ($File -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFile]) { return $File.ShareFileClient } else { throw "Invalid parameter File. Expected AzureStorageFileDirectory or AzureStorageFile." } } function Get-IsDirectoryClient { param ( [Parameter(Mandatory = $true, Position = 0)] [object]$Client ) if ($Client -is [Azure.Storage.Files.Shares.ShareDirectoryClient]) { return $true } elseif ($Client -is [Azure.Storage.Files.Shares.ShareFileClient]) { return $false } else { throw "Invalid parameter Client. Expected ShareDirectoryClient or ShareFileClient." } } function Get-ShareClientFromFileOrDirectoryClient { param ( [Parameter(Mandatory = $true, Position = 0)] [object]$Client ) if ($Client -is [Azure.Storage.Files.Shares.ShareDirectoryClient] -or $Client -is [Azure.Storage.Files.Shares.ShareFileClient]) { return [Azure.Storage.Files.Shares.Specialized.SpecializedShareExtensions]::GetParentShareClient($Client) } else { throw "Invalid parameter Client. Expected ShareDirectoryClient or ShareFileClient." } } function Get-AzureFilesRecursive { param ( [Parameter(Mandatory = $true)] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase[]]$DirectoryContents, [Parameter(Mandatory = $true)] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $false)] [string]$DirectoryPath = "", [Parameter(Mandatory = $false)] [switch]$SkipFiles = $false, [Parameter(Mandatory = $false)] [switch]$SkipDirectories = $false ) foreach ($file in $DirectoryContents) { $fullPath = "${DirectoryPath}$($file.Name)" $isDirectory = $file.GetType().Name -eq "AzureStorageFileDirectory" $file.Context = $Context if ($isDirectory) { $fullPath += "/" # Get the contents of the directory. # Calling Get-AzStorageFile with this parameter set returns an Object[], # where items are either AzureStorageFile or AzureStorageFileDirectory. # Therefore, when recursing, we can cast Object[] to AzureStorageBase[]. $subdirectoryContents = Get-AzStorageFile -Context $Context -ShareDirectoryClient $file.ShareDirectoryClient if ($null -ne $subdirectoryContents) { Get-AzureFilesRecursive ` -Context $Context ` -DirectoryContents $subdirectoryContents ` -DirectoryPath $fullPath ` -SkipFiles:$SkipFiles ` -SkipDirectories:$SkipDirectories } } if (($isDirectory -and !$SkipDirectories) -or (!$isDirectory -and !$SkipFiles)) { Write-Output @{ FullPath = $fullPath File = $file } } } } function New-AzFileAcl { <# .SYNOPSIS Creates a new Azure File ACL (Access Control List) for a specified file share. .DESCRIPTION The `New-AzFileAcl` function creates a new ACL for an Azure file share. It supports both SDDL (Security Descriptor Definition Language) and binary ACL formats. The function determines the ACL format if not explicitly provided and uploads the ACL to the specified file share. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share where the ACL will be applied. .PARAMETER ShareClient Specifies the Azure storage file share client with which the ACL will be applied. .PARAMETER Acl Specifies the ACL to be applied. This can be in SDDL format, base64-encoded binary, binary array, or RawSecurityDescriptor. .PARAMETER AclFormat Specifies the format of the ACL. If not provided, the function will infer the format automatically. .OUTPUTS System.String Returns the file permission key associated with the created ACL. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $acl = "O:BAG:SYD:(A;;FA;;;SY)" PS> New-AzFileAcl -Context $context -FileShareName "myfileshare" -Acl $acl -AclFormat Sddl Creates a new ACL in SDDL format for the specified file share and returns the file permission key. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $acl = "<base64-encoded ACL>" PS> New-AzFileAcl -Context $context -FileShareName "myfileshare" -Acl $acl -AclFormat Base64 Creates a new ACL for the specified file share, inferring the ACL format automatically, and returns the file permission key. .LINK Set-AzFileAclKey #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([string])] param ( [Parameter(Mandatory = $true, ParameterSetName = "FileShareName")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FileShareName")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FileShareClient")] [Azure.Storage.Files.Shares.ShareClient]$ShareClient, [Parameter(Mandatory = $true, ParameterSetName = "FileShareName")] [Parameter(Mandatory = $true, ParameterSetName = "FileShareClient")] [object]$Acl, [Parameter(Mandatory = $false, ParameterSetName = "FileShareName")] [Parameter(Mandatory = $false, ParameterSetName = "FileShareClient")] [SecurityDescriptorFormat]$AclFormat ) begin { # Get a ShareClient instance from the parameters if ($PSCmdlet.ParameterSetName -eq "FileShareName") { $share = Get-AzStorageShare -Name $FileShareName -Context $Context $ShareClient = $share.ShareClient } # Infer AclFormat if not provided if (-not $AclFormat) { $AclFormat = Get-InferredAclFormat $Acl Write-Verbose "Inferred ACL format: $AclFormat. To override, use -AclFormat." } } process { # If it's SDDL, then upload the SDDL directly if ($AclFormat -eq "Sddl") { [string]$sddl = $Acl if ($PSCmdlet.ShouldProcess("File share '$($ShareClient.Name)'", "Create SDDL permission '$sddl'")) { $permissionInfo = $ShareClient.CreatePermission($sddl, [System.Threading.CancellationToken]::None) return $permissionInfo.Value.FilePermissionKey } } # All other formats should use the binary API else { $base64 = Convert-SecurityDescriptor $Acl -From $AclFormat -To Base64 if ($PSCmdlet.ShouldProcess("File share '$($ShareClient.Name)'", "Create binary permission '$base64'")) { $permission = [Azure.Storage.Files.Shares.Models.ShareFilePermission]::new() $permission.Permission = $base64 $permission.PermissionFormat = [Azure.Storage.Files.Shares.Models.FilePermissionFormat]::Binary $permissionInfo = $ShareClient.CreatePermission($permission, [System.Threading.CancellationToken]::None) return $permissionInfo.Value.FilePermissionKey } } } } function Set-AzFileAclKey { <# .SYNOPSIS Sets the Azure File ACL key on a specified file or directory. .DESCRIPTION The `Set-AzFileAclKey` takes an ACL key, and sets it on a specified file or directory in Azure Files. .PARAMETER File Specifies the Azure storage file or directory on which to set the ACL key. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share where the ACL will be applied. .PARAMETER Client Specifies the Azure storage file or directory client with which the ACL will be applied. .PARAMETER Key Specifies the ACL key to be applied. This is the key returned from the `New-AzFileAcl` function. .OUTPUTS System.String Returns the file permission key associated with the created ACL. Note that this may differ from the key that was passed in, as the permission applied to the file may be different, due to inheritance rules defined on the parent directory. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $key = New-AzFileAcl -Context $context -FileShareName "myfileshare" -Acl "O:BAG:SYD:(A;;FA;;;SY)" -AclFormat Sddl PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> Set-AzFileAclKey -File $file -Key $key Sets the specified ACL key on the given file. .LINK New-AzFileAcl #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([string])] param ( [Parameter(Mandatory = $true, ParameterSetName = "File")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Path to the file or directory on which to set the permission key")] [string]$FilePath, [Parameter(Mandatory = $true, ParameterSetName = "Client")] [object]$Client, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Key ) begin { # Ensure $Client and $isDirectory are initialized for all parameter sets if ($PSCmdlet.ParameterSetName -eq "FilePath") { $File = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath $Client = Get-ClientFromFile $File } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Client = Get-ClientFromFile $File } $isDirectory = Get-IsDirectoryClient $Client } process { # Set the ACL $smbProperties = [Azure.Storage.Files.Shares.Models.FileSmbProperties]::new() $smbProperties.FilePermissionKey = $Key if ($isDirectory) { if ($PSCmdlet.ShouldProcess("Directory '$($Client.Path)'", "Set permission key '$Key'")) { $options = [Azure.Storage.Files.Shares.Models.ShareDirectorySetHttpHeadersOptions]::new() $options.SmbProperties = $smbProperties $response = $client.SetHttpHeaders($options) return $response.Value.SmbProperties.FilePermissionKey } } else { if ($PSCmdlet.ShouldProcess("File '$($Client.Path))'", "Set permission key '$Key'")) { $options = [Azure.Storage.Files.Shares.Models.ShareFileSetHttpHeadersOptions]::new() $options.SmbProperties = $smbProperties $response = $client.SetHttpHeaders($options) return $response.Value.SmbProperties.FilePermissionKey } } } } function Set-AzFileAcl { <# .SYNOPSIS Sets the Access Control List (ACL) for a specified Azure file or directory. .DESCRIPTION The `Set-AzFileAcl` function applies an ACL to a specified Azure file or directory. It supports both SDDL (Security Descriptor Definition Language) and binary ACL formats. The function determines the ACL format if not explicitly provided and applies the ACL directly or via a permission key, depending on the size of the ACL. .PARAMETER File Specifies the Azure storage file or directory on which to set the ACL. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share where the ACL will be applied. .PARAMETER Client Specifies the Azure storage file or directory client with which the ACL will be applied. .PARAMETER Client Specifies the Azure storage file or directory client with which to set the ACL. .PARAMETER Acl Specifies the ACL to be applied. This can be in SDDL format, base64-encoded binary, binary array, or RawSecurityDescriptor. .PARAMETER AclFormat Specifies the format of the ACL. If not provided, the function will infer the format automatically. Supported formats include SDDL, Base64, and Binary. .OUTPUTS System.String Returns the file permission key associated with the applied ACL. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> Set-AzFileAcl -File $file -Acl "O:BAG:SYD:(A;;FA;;;SY)" -AclFormat Sddl Sets the specified SDDL ACL on the given file. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> $binaryAcl = [byte[]](0x01, 0x02, 0x03, 0x04, ...) PS> Set-AzFileAcl -File $file -Acl $binaryAcl -AclFormat Binary Sets the specified binary ACL on the given file. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([string])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "File")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Path to the file or directory on which to set the permission key")] [string]$FilePath, [Parameter(Mandatory = $true, ParameterSetName = "Client")] [Object]$Client, [Parameter(Mandatory = $true, ParameterSetName = "File")] [Parameter(Mandatory = $true, ParameterSetName = "FilePath")] [Parameter(Mandatory = $true, ParameterSetName = "Client")] [object]$Acl, [Parameter(Mandatory = $false, ParameterSetName = "File")] [Parameter(Mandatory = $false, ParameterSetName = "FilePath")] [Parameter(Mandatory = $false, ParameterSetName = "Client")] [SecurityDescriptorFormat]$AclFormat ) begin { # Convert parameters to a $Client, and determine if $isDirectory if ($PSCmdlet.ParameterSetName -eq "FilePath") { $File = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath $Client = Get-ClientFromFile $File } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Client = Get-ClientFromFile $File } $isDirectory = Get-IsDirectoryClient $Client # Get the permission value to set from the parameters if (-not $AclFormat) { $AclFormat = Get-InferredAclFormat $Acl Write-Verbose "Inferred ACL format: $AclFormat. To override, use -AclFormat." } $permission = [Azure.Storage.Files.Shares.Models.ShareFilePermission]::new() if ($AclFormat -eq "Sddl") { $permission.Permission = $Acl $permission.PermissionFormat = [Azure.Storage.Files.Shares.Models.FilePermissionFormat]::Sddl $aclCreationFormat = [SecurityDescriptorFormat]::Sddl } else { $permission.Permission = Convert-SecurityDescriptor $Acl -From $AclFormat -To Base64 $permission.PermissionFormat = [Azure.Storage.Files.Shares.Models.FilePermissionFormat]::Binary $aclCreationFormat = [SecurityDescriptorFormat]::Base64 } } process { # If it's < 8 KiB, update directly via HTTP headers in a single request # If it's >= 8 KiB, create a new permission key and set it (two requests) if ($permission.Permission.Length -lt 8192) { if ($IsDirectory) { $options = [Azure.Storage.Files.Shares.Models.ShareDirectorySetHttpHeadersOptions]::new() $options.FilePermission = $permission if ($PSCmdlet.ShouldProcess("Directory '$($Client.Path)'", "Set permission '$($permission.Permission)' in format '$($permission.PermissionFormat)'")) { $response = $Client.SetHttpHeaders($options) return $response.Value.SmbProperties.FilePermissionKey } } else { $options = [Azure.Storage.Files.Shares.Models.ShareFileSetHttpHeadersOptions]::new() $options.FilePermission = $permission if ($PSCmdlet.ShouldProcess("File '$($Client.Path)'", "Set permission '$($permission.Permission)' in format '$($permission.PermissionFormat)'")) { $response = $Client.SetHttpHeaders($options) return $response.Value.SmbProperties.FilePermissionKey } } } else { $ShareClient = Get-ShareClientFromFileOrDirectoryClient $Client # Create a new permission key $key = New-AzFileAcl ` -ShareClient $shareClient ` -Acl $permission.Permission ` -AclFormat $aclCreationFormat ` -WhatIf:$WhatIfPreference if ([string]::IsNullOrEmpty($key)) { Write-Error "Failed to create file permission" -ErrorAction Stop } # Set the permission key return Set-AzFileAclKey -Client $Client -Key $key -WhatIf:$WhatIfPreference } } } function Get-AzFileAclKey { <# .SYNOPSIS Retrieves the permission key from a file or directory in an Azure file share. .DESCRIPTION The `Get-AzFileAclKey` function retrieves the ACL key for a given file or directory in an Azure file share. The ACL can be returned in various formats, including SDDL (Security Descriptor Definition Language) or binary formats. The function supports retrieving the ACL from a file share specified either directly or by its name and context. .PARAMETER File Specifies the Azure storage file or directory from which to retrieve the ACL key. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share from which to retrieve the ACL key. .PARAMETER FilePath Specifies the path to the file or directory from which to retrieve the ACL key. .PARAMETER Client Specifies the Azure storage file or directory client with which to retrieve the ACL key. .OUTPUTS System.String Returns the file permission key associated with the specified file or directory. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> Get-AzFileAclKey -File $file Retrieves the permission key for the specified file. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Azure storage file or directory")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Path to the file or directory on which to set the permission key")] [string]$FilePath, [Parameter(Mandatory = $false, ParameterSetName = "Client")] [object]$Client ) begin { # Get a $Client from the parameters if ($PSCmdlet.ParameterSetName -eq "FilePath") { $File = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath $Client = Get-ClientFromFile $File } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Client = Get-ClientFromFile $File } } process { return $Client.GetProperties([System.Threading.CancellationToken]::None).Value.SmbProperties.FilePermissionKey } } function Get-AzFileAclFromKey { <# .SYNOPSIS Retrieves the ACL (Access Control List) for a specified ACL key. .DESCRIPTION The `Get-AzFileAclFromKey` function retrieves the ACL for a specified ACL key. It supports retrieving the ACL in various formats, including SDDL (Security Descriptor Definition Language) or binary formats. The function supports retrieving the ACL from a file share specified either directly or its name and context. .PARAMETER Key Specifies the ACL key to be retrieved. This is the key returned from the `New-AzFileAcl`, `Set-AzFileAclKey`, or `Get-AzFileAclKey` functions. .PARAMETER Share Specifies the Azure storage file share from which to retrieve the ACL key. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share from which to retrieve the ACL key. .PARAMETER ShareClient Specifies the Azure storage file share client from which to retrieve the ACL key. .PARAMETER OutputFormat Specifies the output format of the security descriptor. Supported formats include SDDL, Base64, and Binary. .OUTPUTS System.String Returns the ACL in the specified format. The default format is SDDL. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> $key = Get-AzFileAclKey -File $file PS> Get-AzFileAclFromKey -Key $key -Share $file.Share -OutputFormat Sddl Retrieves the SDDL ACL for the specified file using the permission key. .LINK New-AzFileAcl .LINK Set-AzFileAcl .LINK Set-AzFileAclKey #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Security.AccessControl.RawSecurityDescriptor], [string], [byte[]])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Key, [Parameter(Mandatory = $true, ParameterSetName = "Share")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFileShare]$Share, [Parameter(Mandatory = $true, ParameterSetName = "FileShareName", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FileShareName", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $false, ParameterSetName = "ShareClient", HelpMessage = "Azure storage file share client")] [Azure.Storage.Files.Shares.ShareClient]$ShareClient, [Parameter(Mandatory = $false, HelpMessage = "Output format of the security descriptor")] [SecurityDescriptorFormat]$OutputFormat = [SecurityDescriptorFormat]::Sddl ) begin { # Get a $ShareClient from the parameters if ($PSCmdlet.ParameterSetName -eq "FileShareName") { $ShareClient = (Get-AzStorageShare -Name $FileShareName -Context $Context).ShareClient } elseif ($PSCmdlet.ParameterSetName -eq "Share") { $ShareClient = $Share.ShareClient } } process { if ($PSCmdlet.ShouldProcess("File share '$($ShareClient.Name)'", "Get permission key '$Key'")) { if ($OutputFormat -eq [SecurityDescriptorFormat]::Sddl) { $format = [Azure.Storage.Files.Shares.Models.FilePermissionFormat]::Sddl $permissionInfo = $ShareClient.GetPermission($Key, $format, [System.Threading.CancellationToken]::None) $sddl = $permissionInfo.Value.Permission return $sddl } else { $format = [Azure.Storage.Files.Shares.Models.FilePermissionFormat]::Binary $permissionInfo = $ShareClient.GetPermission($Key, $format, [System.Threading.CancellationToken]::None) $base64 = $permissionInfo.Value.Permission return Convert-SecurityDescriptor $base64 -From Base64 -To $OutputFormat } } } } function Get-AzFileAcl { <# .SYNOPSIS Retrieves the ACL (Access Control List) for a specified file or directory. .DESCRIPTION The `Get-AzFileAcl` function retrieves the ACL for a specified file or directory. It supports retrieving the ACL in various formats, including SDDL (Security Descriptor Definition Language) or binary formats. The function supports retrieving the ACL from a file share specified either directly or its name and context. .PARAMETER File Specifies the Azure storage file or directory from which to retrieve the ACL key. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share from which to retrieve the ACL key. .PARAMETER FilePath Specifies the path to the file or directory from which to retrieve the ACL key. .PARAMETER Client Specifies the Azure storage file share client with which to retrieve the ACL key. .PARAMETER OutputFormat Specifies the output format of the security descriptor. Supported formats include SDDL, Base64, and Binary. .OUTPUTS System.String Returns the ACL in the specified format. The default format is SDDL. .EXAMPLE PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey" PS> $file = Get-AzStorageFile -Context $context -ShareName "myfileshare" -Path "myfolder/myfile.txt" PS> Get-AzFileAcl -File $file Retrieves the SDDL ACL for the specified file using the permission key. .LINK New-AzFileAcl .LINK Set-AzFileAcl .LINK Set-AzFileAclKey #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Azure storage file or directory")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Path to the file or directory on which to set the permission key")] [string]$FilePath, [Parameter(Mandatory = $false, ParameterSetName = "Client")] [object]$Client, [Parameter(Mandatory = $false, HelpMessage = "Output format of the security descriptor")] [SecurityDescriptorFormat]$OutputFormat = [SecurityDescriptorFormat]::Sddl ) begin { # Get a $Client from the parameters if ($PSCmdlet.ParameterSetName -eq "FilePath") { $File = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath $Client = Get-ClientFromFile $File } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Client = Get-ClientFromFile $File } } process { $key = Get-AzFileAclKey -Client $Client if ([string]::IsNullOrEmpty($key)) { Write-Error "Failed to get file permission key" -ErrorAction Stop } $shareClient = Get-ShareClientFromFileOrDirectoryClient $Client return Get-AzFileAclFromKey -Key $key -ShareClient $shareClient -OutputFormat $OutputFormat } } function Set-AzFileAclRecursive { [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'ForEach-Object/Parallel', Justification = "We are guarding the usage of -Parallel with a PowerShell version check")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'ForEach-Object/ThrottleLimit', Justification = "We are guarding the usage of -ThrottleLimit with a PowerShell version check")] param ( [Parameter(Mandatory = $true)] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true)] [string]$FileShareName, [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string]$SddlPermission, [Parameter(Mandatory = $false)] [bool]$Parallel = $true, [Parameter(Mandatory = $false)] [int]$ThrottleLimit = 10, [Parameter(Mandatory = $false)] [switch]$SkipFiles = $false, [Parameter(Mandatory = $false)] [switch]$SkipDirectories = $false, [Parameter(Mandatory = $false)] [switch]$WriteToPipeline = $false ) if ($SkipFiles -and $SkipDirectories) { Write-Warning "Both -SkipFiles and -SkipDirectories are set. Nothing to do." return } # Check if parallel mode is supported if ($Parallel -and $PSVersionTable.PSVersion.Major -lt 7) { Write-Warning "-Parallel is only supported on PowerShell 7+. Falling back to single-threaded mode." $Parallel = $false } # Try to parse SDDL permission, check for common issues try { $securityDescriptor = ConvertTo-SecurityDescriptor $SddlPermission -InputFormat Sddl } catch { Write-Failure "SDDL permission is invalid" return } # Check if inheritance flags are okay $shouldBeEnabled = "ContainerInherit, ObjectInherit" $shouldBeDisabled = "NoPropagateInherit, InheritOnly" $matchesTarget = Get-AllAceFlagsMatch ` -SecurityDescriptor $securityDescriptor ` -EnabledFlags $shouldBeEnabled ` -DisabledFlags $shouldBeDisabled if (-not ($matchesTarget)) { Set-AceFlags ` -SecurityDescriptor $securityDescriptor ` -EnableFlags $shouldBeEnabled ` -DisableFlags $shouldBeDisabled $newSddl = ConvertFrom-SecurityDescriptor $securityDescriptor -OutputFormat Sddl $continue = Write-SddlWarning -Sddl $SddlPermission -NewSddl $newSddl if (-not $continue) { return } } # Try to create permission on Azure Files # The idea is to create the permission early. If this fails (e.g. due to invalid SDDL), we can fail early. # Setting permission key should in theory also be slightly faster than setting SDDL directly (though this may not be noticeable in practice). try { $filePermissionKey = New-AzFileAcl -Context $Context -FileShareName $FileShareName -Sddl $SddlPermission -WhatIf:$WhatIfPreference if ([string]::IsNullOrEmpty($filePermissionKey)) { Write-Failure "Failed to create file permission" return } } catch { Write-Failure "Failed to create file permission" -Details $_.Exception.Message return } # Get root directory # Calling Get-AzStorageFile with this parameter set returns a AzureStorageFileDirectory # (if the path is a directory) or AzureStorageFile (if the path is a file). # If it's a directory, Get-AzureFilesRecursive will get its contents. try { $directory = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath -ErrorAction Stop } catch { Write-Failure "Failed to read root directory" -Details $_.Exception.Message return } $startTime = Get-Date $processedCount = 0 $errors = @{} $ProgressPreference = "SilentlyContinue" if ($Parallel) { $funcDef = ${function:Set-AzFileAclKey}.ToString() Get-AzureFilesRecursive ` -Context $Context ` -DirectoryContents @($directory) ` -SkipFiles:$SkipFiles ` -SkipDirectories:$SkipDirectories ` | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { # Set the ACL ${function:Set-AzFileAclKey} = $using:funcDef $success = $true $errorMessage = "" try { Set-AzFileAclKey -File $_.File -Key $using:filePermissionKey -WhatIf:$using:WhatIfPreference } catch { $success = $false $errorMessage = $_.Exception.Message } # Write full output if requested, otherwise write minimal output if ($using:WriteToPipeline) { Write-Output @{ Time = (Get-Date).ToString("o") FullPath = $_.FullPath Permission = $using:SddlPermission Success = $success ErrorMessage = $errorMessage } } else { Write-Output @{ FullPath = $_.FullPath Success = $success ErrorMessage = $errorMessage } } } ` | ForEach-Object { # Can't write in the parallel block, so we write here if (-not $_.Success) { $errors[$_.FullPath] = $_.ErrorMessage } $processedCount++ Write-Output $_ } ` | Write-LiveFilesAndFoldersProcessingStatus -RefreshRateHertz 10 -StartTime $startTime ` | ForEach-Object { if ($WriteToPipeline) { Write-Output $_ } } } else { Get-AzureFilesRecursive ` -Context $Context ` -DirectoryContents @($directory) ` -SkipFiles:$SkipFiles ` -SkipDirectories:$SkipDirectories ` | ForEach-Object { $fullPath = $_.FullPath $success = $true $errorMessage = "" # Set the ACL try { Set-AzFileAclKey -File $_.File -Key $filePermissionKey -WhatIf:$WhatIfPreference } catch { $success = $false $errorMessage = $_.Exception.Message $errors[$fullPath] = $errorMessage } $processedCount++ # Write full output if requested, otherwise write minimal output if ($WriteToPipeline) { Write-Output @{ Time = (Get-Date).ToString("o") FullPath = $fullPath Permission = $SddlPermission Success = $success ErrorMessage = $errorMessage } } else { Write-Output @{ FullPath = $fullPath Success = $success ErrorMessage = $errorMessage } } } ` | Write-LiveFilesAndFoldersProcessingStatus -RefreshRateHertz 10 -StartTime $startTime ` | ForEach-Object { if ($WriteToPipeline) { Write-Output $_ } } } $ProgressPreference = "Continue" $totalTime = (Get-Date) - $startTime Write-Host "`r" -NoNewline # Clear the line from the live progress reporting Write-FinalFilesAndFoldersProcessed -ProcessedCount $processedCount -Errors $errors -TotalTime $totalTime } function Restore-AzFileAclInheritance { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ParameterSetName = "Single")] [Parameter(Mandatory = $true, ParameterSetName = "Recursive")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "Single")] [Parameter(Mandatory = $true, ParameterSetName = "Recursive")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "Single")] [string]$ParentPath, [Parameter(Mandatory = $true, ParameterSetName = "Single")] [string]$ChildPath, [Parameter(Mandatory = $true, ParameterSetName = "Recursive")] [switch]$Recursive, [Parameter(Mandatory = $true, ParameterSetName = "Recursive")] [string]$Path ) if ($PSCmdlet.ParameterSetName -eq "Recursive") { $ParentPath = $Path } # Check that parent path exists and is a directory $parentFile = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $ParentPath if ($null -eq $parentFile) { Write-Host "The specified parent path '$ParentPath' does not exist." -ForegroundColor Red return } if ($parentFile -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFile]) { Write-Host "The specified parent path '$ParentPath' is a file. Expected it to be a directory." -ForegroundColor Red return } # Dispatch to either recursive or single file processing if ($PSCmdlet.ParameterSetName -eq "Single") { $parentAcl = Get-AzFileAcl -File $parentFile -OutputFormat Raw return Restore-AzFileAclInheritanceSingle ` -Context $Context ` -FileShareName $FileShareName ` -ParentAcl $parentAcl ` -ChildPath $ChildPath ` -WhatIf:$WhatIfPreference } elseif ($PSCmdlet.ParameterSetName -eq "Recursive" -and $Recursive) { $startTime = Get-Date $processedCount = 0 $errors = @{} Restore-AzFileAclInheritanceRecursive ` -DirectoryClient $parentFile.ShareDirectoryClient ` -PassThru ` -WhatIf:$WhatIfPreference ` | ForEach-Object { if (-not $_.Success) { $errors[$_.Path] = "failed" } $processedCount++ Write-Output $_ } | Write-LiveFilesAndFoldersProcessingStatus -RefreshRateHertz 10 -StartTime $startTime ` | ForEach-Object { if ($PassThru) { Write-Output $_ } } Write-Host "`r" -NoNewline # Clear the line from the live progress reporting Write-FinalFilesAndFoldersProcessed ` -ProcessedCount $processedCount ` -Errors $errors ` -TotalTime ((Get-Date) - $startTime) } } function Restore-AzFileAclInheritanceSingle { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Security.AccessControl.GenericSecurityDescriptor])] param ( [Parameter(Mandatory = $true)] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true)] [string]$FileShareName, [Parameter(Mandatory = $true)] [AllowNull()] [System.Security.AccessControl.GenericSecurityDescriptor]$ParentAcl, [Parameter(Mandatory = $true)] [string]$ChildPath ) # Presupposition: the parent path exists and is a directory. It is the responsibility of the caller to check this. # Check that the child path exists $childFile = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $ChildPath if ($null -eq $childFile) { Write-Host "The specified child path '$ChildPath' does not exist." -ForegroundColor Red return } # Get parent and child ACLs $childAcl = Get-AzFileAcl -File $childFile -OutputFormat Raw $childIsFolder = $childFile -is [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageFileDirectory] # Compute inheritance $newChildAcl = CreatePrivateObjectSecurityEx ` -ParentDescriptor $ParentAcl ` -CreatorDescriptor $childAcl ` -IsDirectory $childIsFolder $parentSddl = ConvertFrom-SecurityDescriptor $parentAcl -To Sddl $childSddl = ConvertFrom-SecurityDescriptor $newChildAcl -To Sddl $childNewSddl = ConvertFrom-SecurityDescriptor $childAcl -To Sddl Write-Verbose "Parent SDDL: $parentSddl" Write-Verbose "Child SDDL: $childSddl" Write-Verbose "New child SDDL: $childNewSddl" # Update ACL according to inheritance if ($PSCmdlet.ShouldProcess("File share '$($FileShareName)'", "Apply inheritance from '$ParentPath' to '$ChildPath'")) { Set-AzFileAcl -File $childFile -Acl $newChildAcl -AclFormat Sddl -WhatIf:$WhatIfPreference } return $newChildAcl } function Restore-AzFileAclInheritanceRecursive { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Security.AccessControl.GenericSecurityDescriptor])] param ( [Parameter(Mandatory = $true)] [Azure.Storage.Files.Shares.ShareDirectoryClient]$DirectoryClient, [Parameter(Mandatory = $false)] [switch]$PassThru = $false ) # Presupposition: the parent path exists and is a directory. It is the responsibility of the caller to check this. $directoryPermissionKey = $DirectoryClient.GetProperties().Value.SmbProperties.FilePermissionKey $shareClient = Get-ShareClientFromFileOrDirectoryClient $DirectoryClient $options = [Azure.Storage.Files.Shares.Models.ShareDirectoryGetFilesAndDirectoriesOptions]::new() $options.IncludeExtendedInfo = $true $options.Traits = [Azure.Storage.Files.Shares.Models.ShareFileTraits]::PermissionKey # Recursively visit all subdirectories. This is a breadth-first search. $stack = [System.Collections.Generic.Stack[PSCustomObject]]::new() $stack.Push([PSCustomObject]@{ DirectoryClient = $DirectoryClient DirectoryPermissionKey = $directoryPermissionKey }) while ($stack.Count -gt 0) { $popped = $stack.Pop() $directoryClient = $popped.DirectoryClient $directoryPermissionKey = $popped.DirectoryPermissionKey # Get permission of parent directory $directoryPermission = $null if ($null -eq $directoryPermissionKey) { Write-Verbose "The directory '$($directoryClient.Name)' does not have a permission key..." } else { $directoryPermission = Get-AzFileAclFromKey ` -Key $directoryPermissionKey ` -ShareClient $shareClient ` -OutputFormat FolderAcl } # Iterate over the contents of the directory foreach ($item in $directoryClient.GetFilesAndDirectories($options).GetEnumerator()) { $itemPermissionFormat = if ($item.IsDirectory) { "FolderAcl" } else { "FileAcl" } # Get ACL for the item $itemPermission = Get-AzFileAclFromKey ` -Key $item.PermissionKey ` -ShareClient $shareClient ` -OutputFormat $itemPermissionFormat # Compute inheritance $itemNewPermission = CreatePrivateObjectSecurityEx ` -ParentDescriptor $directoryPermission ` -CreatorDescriptor $itemPermission ` -IsDirectory $item.IsDirectory # Set new ACL on the item $itemClient = if ($item.IsDirectory) { $directoryClient.GetSubdirectoryClient($item.Name) } else { $directoryClient.GetFileClient($item.Name) } $parentSddl = Convert-SecurityDescriptor $directoryPermission -From FolderAcl -To Sddl $creatorSddl = Convert-SecurityDescriptor $itemPermission -From $itemPermissionFormat -To Sddl $newSddl = Convert-SecurityDescriptor $itemNewPermission -From $itemPermissionFormat -To Sddl Write-Verbose "Computed inheritance for item '$($itemClient.Path)'" Write-Verbose "Parent SDDL: $parentSddl" Write-Verbose "Creator SDDL: $creatorSddl" Write-Verbose "New SDDL: $newSddl" $newPermissionKey = Set-AzFileAcl ` -Client $itemClient ` -Acl $itemNewPermission ` -AclFormat $itemPermissionFormat ` -WhatIf:$WhatIfPreference # Write to the pipeline if ($PassThru) { Write-Output @{ Path = $itemClient.Path IsDirectory = $item.IsDirectory NewPermission = (Convert-SecurityDescriptor $itemNewPermission -To Sddl) PermissionKey = $newPermissionKey Success = $true } } # If item is a directory, push it onto the stack if ($item.IsDirectory) { $stack.Push([PSCustomObject]@{ DirectoryClient = $itemClient DirectoryPermissionKey = $newPermissionKey }) } } } } function Connect-MgGraphIfNeeded { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [string[]]$Scopes ) # Determine if we are connected to Microsoft Graph $context = Get-MgContext if ($null -eq $context) { Write-Verbose "Not connected to Microsoft Graph" if ($PSCmdlet.ShouldProcess("Microsoft Graph", "Connect")) { Write-Verbose "Connecting to Microsoft Graph with required scopes '$Scopes'" Connect-MgGraph -Scopes $Scopes -ErrorAction Stop } return } # Determine if we the current connection has the required scopes $missingScopes = $false $currentScopes = [System.Collections.Generic.HashSet[string]]::new($context.Scopes) foreach ($scope in $Scopes) { if (-not $currentScopes.Contains($scope)) { Write-Verbose "Current connection to Microsoft Graph is missing scope '$scope'" $missingScopes = $true } } # Connect with the required scopes if needed if ($missingScopes -and $PSCmdlet.ShouldProcess("Microsoft Graph", "Connect")) { Write-Verbose "Connecting to Microsoft Graph, tenant $($context.TenantId) with required scopes '$Scopes'" Connect-MgGraph -TenantId $context.TenantId -Scopes $Scopes -ErrorAction Stop } else { Write-Verbose "Already connected to Microsoft Graph, tenant $($context.TenantId), with scopes $($currentScopes -join ",")" } } function Get-Sid { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Security.Principal.SecurityIdentifier])] param ( [Parameter(Mandatory = $true)] [string]$Identity ) process { if ($Identity -match "^S-1-(\d+-){1,14}\d+$") { Write-Verbose "Given identity is a SID." return [System.Security.Principal.SecurityIdentifier]::new($Identity) } if ($Identity -match "^[^@]+@[^.]+\..+") { Write-Verbose "Given identity is a UPN." Connect-MgGraphIfNeeded -Scopes @("User.ReadBasic.All") -WhatIf:$WhatIfPreference | Out-Null # Only users have UPNs. Look up the user by UPN. Write-Verbose "Querying Microsoft Graph for user with UPN '$Identity'" $user = Get-MgUserByUserPrincipalName -UserPrincipalName $Identity -Property "OnPremisesSecurityIdentifier","SecurityIdentifier" -ErrorAction Stop if ($user) { if ($user.OnPremisesSecurityIdentifier) { Write-Verbose "Hybrid user found with SID '$($user.OnPremisesSecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.OnPremisesSecurityIdentifier) } elseif ($user.SecurityIdentifier) { Write-Verbose "Cloud-only user found with SID '$($user.SecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.SecurityIdentifier) } else { throw "User with UPN '$Identity' was found, but it did not have a SID." } } else { throw "No user found with UPN '$Identity'" } } $output = [guid]::Empty if ([guid]::TryParse($Identity, [ref]$output)) { Write-Verbose "Given identity is a an object ID." Connect-MgGraphIfNeeded -Scopes @("User.ReadBasic.All", "GroupMember.Read.All") -WhatIf:$WhatIfPreference | Out-Null # Try to look up the user Write-Verbose "Getting user by ID '$Identity' in Microsoft Graph" try { $user = Get-MgUser -UserId $Identity -Property "OnPremisesSecurityIdentifier","SecurityIdentifier" -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId.StartsWith("Request_ResourceNotFound")) { Write-Verbose "User not found by ID '$Identity' in Microsoft Graph" } else { throw $_ } } if ($user) { if ($user.OnPremisesSecurityIdentifier) { Write-Verbose "Hybrid user found with SID '$($user.OnPremisesSecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.OnPremisesSecurityIdentifier) } elseif ($user.SecurityIdentifier) { Write-Verbose "Cloud-only user found with SID '$($user.SecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.SecurityIdentifier) } else { throw "User with object ID '$Identity' was found, but it did not have a SID." } } # Try to look up the group Write-Verbose "Getting group by ID '$Identity' in Microsoft Graph" try { $group = Get-MgGroup -GroupId $Identity -Property "OnPremisesSecurityIdentifier","SecurityIdentifier" -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId.StartsWith("Request_ResourceNotFound")) { Write-Verbose "Group not found by ID '$Identity' in Microsoft Graph" } else { throw $_ } } if ($group) { if ($group.OnPremisesSecurityIdentifier) { Write-Verbose "Hybrid group found with SID '$($group.OnPremisesSecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($group.OnPremisesSecurityIdentifier) } elseif ($group.SecurityIdentifier) { Write-Verbose "Cloud-only group found with SID '$($group.SecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($group.SecurityIdentifier) } else { throw "Group with object ID '$Identity' was found, but it did not have a SID." } } else { throw "No user or group found with ID '$Identity'" } } Write-Verbose "Given identity is a display name." Connect-MgGraphIfNeeded -Scopes @("User.ReadBasic.All", "GroupMember.Read.All") -WhatIf:$WhatIfPreference | Out-Null # Replace single quotes with double quotes for the query # See https://learn.microsoft.com/en-us/graph/query-parameters?tabs=http#escaping-single-quotes $displayName = $Identity -replace "'", "''" $filter = "DisplayName eq '${displayName}'" # Get the user by display name Write-Verbose "Getting user or group by display name '$Identity' in Microsoft Graph" $user = Get-MgUser -Filter $filter -Property "OnPremisesSecurityIdentifier","SecurityIdentifier" -ErrorAction Stop if ($user) { if ($user -is [array] -and $user.Count -gt 1) { throw "$($user.Count) users found with display name '$Identity'. Use SID, object ID or UPN to specify which user is being referred to." } if ($user.OnPremisesSecurityIdentifier) { Write-Verbose "Hybrid user found with SID '$($user.OnPremisesSecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.OnPremisesSecurityIdentifier) } elseif ($user.SecurityIdentifier) { Write-Verbose "Cloud-only user found with SID '$($user.SecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($user.SecurityIdentifier) } else { throw "User with display name '$Identity' was found, but it did not have a SID." } } # If no user was found, try to get the group by display name Write-Verbose "Getting group by display name '$Identity' in Microsoft Graph" $group = Get-MgGroup -Filter $filter -Property "OnPremisesSecurityIdentifier","SecurityIdentifier" -ErrorAction Stop if ($group) { if ($group -is [array] -and $group.Count -gt 1) { throw "$($group.Count) groups found with display name '$Identity'. Use SID or object ID to specify which group is being referred to." } if ($group.OnPremisesSecurityIdentifier) { Write-Verbose "Hybrid group found with SID '$($group.OnPremisesSecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($group.OnPremisesSecurityIdentifier) } elseif ($group.SecurityIdentifier) { Write-Verbose "Cloud-only group found with SID '$($group.SecurityIdentifier)'" return [System.Security.Principal.SecurityIdentifier]::new($group.SecurityIdentifier) } else { throw "Group with display name '$Identity' was found, but it did not have a SID." } } else { throw "No user or group found with display name '$Identity'" } } } function Set-AzFileOwner { <# .SYNOPSIS Sets the owner for a specified Azure file or directory. .DESCRIPTION It supports SIDs, UPNs (User Principal Names), object IDs and display name for specifying the owner. The function can be used to set the owner for a file or directory in an Azure file share. .PARAMETER File Specifies the Azure storage file or directory on which to update the owner. .PARAMETER Context Specifies the Azure storage context. This is required to authenticate and interact with the Azure storage account. .PARAMETER FileShareName Specifies the name of the Azure file share on which to update the owner. .PARAMETER FilePath Specifies the path to the file or directory on which to update the owner. .PARAMETER Client Specifies the Azure storage file or directory client with which to set the ACL. .PARAMETER Owner Specifies the owner that should be set. This can be a SID or a UPN (User Principal Name). .OUTPUTS System.String Returns the file permission key associated with the applied ACL. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([string])] param ( [Parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Azure storage file or directory")] [Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBase]$File, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Azure storage context")] [Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext]$Context, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Name of the file share")] [string]$FileShareName, [Parameter(Mandatory = $true, ParameterSetName = "FilePath", HelpMessage = "Path to the file or directory within the share")] [string]$FilePath, [Parameter(Mandatory = $false, ParameterSetName = "Client")] [object]$Client, [Parameter(Mandatory = $true, ParameterSetName = "File")] [Parameter(Mandatory = $true, ParameterSetName = "FilePath")] [Parameter(Mandatory = $true, ParameterSetName = "Client")] [string]$Owner ) begin { # Get a $Client from the parameters if ($PSCmdlet.ParameterSetName -eq "FilePath") { $File = Get-AzStorageFile -Context $Context -ShareName $FileShareName -Path $FilePath $Client = Get-ClientFromFile $File } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Client = Get-ClientFromFile $File } } process { # Convert the owner to a SID $ownerSid = Get-Sid -Identity $Owner -Verbose:$VerbosePreference -WhatIf:$WhatIfPreference -ErrorAction Stop if ($null -eq $ownerSid) { Write-Error "Failed to get SID for owner '$Owner'" return } # Get the current ACL for the file or directory $acl = Get-AzFileAcl -Client $Client -OutputFormat Raw # Update the owner in the ACL if ($PSCmdlet.ShouldProcess($Client.Path, "Set owner to '$OwnerSid'")) { $acl.Owner = $ownerSid return Set-AzFileAcl -Client $Client -Acl $acl -AclFormat Raw -WhatIf:$WhatIfPreference } } } |