Carbon.FileSystem.psm1
using namespace System.Security.AccessControl using namespace System.Collections # Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $moduleRoot as the relative root from which to find # things. A published module has its function appended to this file, while a # module in development has its functions in the Functions directory. $moduleRoot = $PSScriptRoot Import-Module -Name (Join-Path -Path $moduleRoot -ChildPath 'Modules\Carbon.Security' -Resolve) ` -Function @('Get-CPermission', 'Grant-CPermission', 'Revoke-CPermission', 'Test-CPermission') ` -Verbose:$false Add-Type @' using System; using System.Text; using System.Collections.Generic; using System.Runtime.InteropServices; namespace Carbon.FileSystem { public static class Kernel32 { #region WinAPI P/Invoke declarations [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr FindFirstFileNameW(string lpFileName, uint dwFlags, ref uint StringLength, StringBuilder LinkName); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool FindNextFileNameW(IntPtr hFindStream, ref uint StringLength, StringBuilder LinkName); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool FindClose(IntPtr hFindFile); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool GetVolumePathName(string lpszFileName, [Out] StringBuilder lpszVolumePathName, uint cchBufferLength); public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); // 0xffffffff; public const int MAX_PATH = 65535; // Max. NTFS path length. #endregion } } '@ # Store each of your module's functions in its own file in the Functions # directory. On the build server, your module's functions will be appended to # this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the # functions that are in their own files. $functionsPath = Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function ConvertTo-CarbonSecurityApplyTo { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [AllowNull()] [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles', 'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')] [String] $ApplyTo ) process { $map = @{ 'FolderOnly' = 'ContainerOnly'; 'FolderSubfoldersAndFiles' = 'ContainerSubcontainersAndLeaves'; 'FolderAndSubfolders' = 'ContainerAndSubcontainers'; 'FolderAndFiles' = 'ContainerAndLeaves'; 'SubfoldersAndFilesOnly' = 'SubcontainersAndLeavesOnly'; 'SubfoldersOnly' = 'SubcontainersOnly'; 'FilesOnly' = 'LeavesOnly'; } if (-not $ApplyTo) { return } return $map[$ApplyTo] } } function Get-CNtfsHardLink { <# .SYNOPSIS Retrieves hard link targets from a file. .DESCRIPTION Get-CNtfsHardLink retrieves hard link targets from a file given a file path. This fixes compatibility issues between Windows PowerShell and PowerShell Core when retrieving targets from a hard link. .EXAMPLE Get-CNtfsHardLink -Path $Path Demonstrates how to retrieve a hard link given a file path. #> [CmdletBinding()] param( # The path whose hard links to get/return. Must exist. [Parameter(Mandatory)] [String] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Resolve-Path -LiteralPath $Path) ) { return } try { $sbPath = [Text.StringBuilder]::New([Carbon.FileSystem.Kernel32]::MAX_PATH) $charCount = [uint32]$sbPath.Capacity; # in/out character-count variable for the WinAPI calls. # Get the volume (drive) part of the target file's full path (e.g., @"C:\") [void][Carbon.FileSystem.Kernel32]::GetVolumePathName($Path, $sbPath, $charCount) $volume = $sbPath.ToString(); # Trim the trailing "\" from the volume path, to enable simple concatenation # with the volume-relative paths returned by the FindFirstFileNameW() and FindFirstFileNameW() functions, # which have a leading "\" $volume = $volume.Substring(0, $volume.Length - 1); # Loop over and collect all hard links as their full paths. [IntPtr]$findHandle = [IntPtr]::Zero $findHandle = [Carbon.FileSystem.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$charCount, $sbPath) if( [Carbon.FileSystem.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle) { $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() $msg = "Failed to find hard links to path ""$($Path | Split-Path -Relative)"": the system error code is ""$($errorCode)""." Write-Error $msg -ErrorAction $ErrorActionPreference return } do { Join-Path -Path $volume -ChildPath $sbPath.ToString() | Write-Output # Add the full path to the result list. $charCount = [uint32]$sbPath.Capacity; # Prepare for the next FindNextFileNameW() call. } while( [Carbon.FileSystem.Kernel32]::FindNextFileNameW($findHandle, [ref]$charCount, $sbPath) ) [void][Carbon.FileSystem.Kernel32]::FindClose($findHandle); } catch { Write-Error -Message $_ -ErrorAction $ErrorActionPreference } } function Get-FileHardLink { <# .SYNOPSIS ***OBSOLETE.*** Use Get-CNtfsHardLink instead. .DESCRIPTION ***OBSOLETE.*** Use Get-CNtfsHardLink instead. .EXAMPLE Get-CNtfsHardLink -Path $Path Demonstrates that you should use `Get-CNtfsHardLink` instead. #> [CmdletBinding()] param( # The path whose hard links to get/return. Must exist. [Parameter(Mandatory)] [String] $Path ) $msg = 'The Get-FileHardLink function is obsolete and will removed in the next major version of ' + 'Carbon.FileSystem. Please use Get-CNtfsHardLink instead.' Write-Warning -Message $msg Get-CNtfsHardLink @PSBoundParameters } function Get-CNtfsPermission { <# .SYNOPSIS Gets the permissions (access control rules) for a file or directory. .DESCRIPTION The `Get-CNtfsPermission` function gets permissions on a file or directory. Permissions returned are the `[Security.AccessControl.FileSystemAccessRule]` objects from the file/directory's ACL. By default, all non-inherited permissions are returned. Pass the path to the file/directory whose permissions to get to the `Path` parameter. To also get inherited permissions, use the `Inherited` switch. To get the permissions a specific identity has on the file/directory, pass that username/group name to the `Identity` parameter. If the identity doesn't exist, or it doesn't have any permissions, no error is written and nothing is returned. .OUTPUTS System.Security.AccessControl.FileSystemAccessRule. .LINK Get-CNtfsPermission .LINK Grant-CNtfsPermission .LINK Revoke-CNtfsPermission .LINK Test-CNtfsPermission .EXAMPLE Get-CNtfsPermission -Path 'C:\Windows' Returns `System.Security.AccessControl.FileSystemAccessRule` objects for all the non-inherited rules on `C:\windows`. .EXAMPLE Get-CNtfsPermission -Path 'C:\Windows' -Inherited Returns `System.Security.AccessControl.RegistryAccessRule` objects for all the inherited and non-inherited rules on `hklm:\software`. .EXAMPLE Get-CNtfsPermission -Path 'C:\Windows' -Idenity Administrators Returns `System.Security.AccessControl.FileSystemAccessRule` objects for all the `Administrators'` rules on `C:\windows`. #> [CmdletBinding()] [OutputType([Security.AccessControl.FileSystemAccessRule])] param( # The path to the file/directory whose permissions (i.e. access control rules) to return. Wildcards supported. [Parameter(Mandatory, ValueFromPipeline)] [String] $Path, # The identity whose permissiosn (i.e. access control rules) to return. By default, all non-inherited # permissions are returned. [String] $Identity, # Return inherited permissions in addition to explicit permissions. [switch] $Inherited ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Get-CPermission @PSBoundParameters } } function Grant-CNtfsPermission { <# .SYNOPSIS Grants permission on folders and files. .DESCRIPTION The `Grant-CNtfsPermission` functions grants permissions to folders and files. Pass the folder/file path to the `Path` parameter, the user/group name to the `Identity` parameter, and the permissions to the `Permission` parameter. By default, the permissions are applied to the folder and inherited to all its subfolders and files. To control how the permissions are applied, use the `ApplyTo` parameter. If you want permissions to only apply to child files and folders, use the `OnlyApplyToChildFilesAndFolders` switch. By default, an "Allow" permission is granted. To add a "Deny" permission, set the value of the `Type` parameter to `Deny`. All existing, non-inherited permissions for the given identity are removed first. If you want to preserve a user/group's existing permissions, use the `Append` switch. To remove *all* non-inherited permissions except the permission being granted, use the `Clear` switch. The permission is only granted if it doesn't exist. To always grant the permission, use the `Force` switch. To get the permission back as a `[System.Security.AccessControl.FileSystemAccessRule]` object, use the `PassThru` switch. .OUTPUTS System.Security.AccessControl.FileSystemAccessRule. .LINK Get-CNtfsPermission .LINK Revoke-CNtfsPermission .LINK Test-CNtfsPermission .LINK http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx .LINK http://msdn.microsoft.com/en-us/magazine/cc163885.aspx#S3 .EXAMPLE Grant-CNtfsPermission -Identity ENTERPRISE\Engineers -Permission FullControl -Path C:\EngineRoom Grants the Enterprise's engineering group full control on the engine room. Very important if you want to get anywhere. .EXAMPLE Grant-CNtfsPermission -Identity ENTERPRISE\Engineers -Permission FullControl -Path C:\EngineRoom -Clear Grants the Enterprise's engineering group full control on the engine room. Any non-inherited, existing access rules are removed from `C:\EngineRoom`. .EXAMPLE Grant-CNtfsPermission -Identity BORG\Locutus -Permission FullControl -Path 'C:\EngineRoom' -Type Deny Demonstrates how to grant deny permissions on an objecy with the `Type` parameter. .EXAMPLE Grant-CNtfsPermission -Path C:\Bridge -Identity ENTERPRISE\Wesley -Permission 'Read' -ApplyTo ContainerAndSubContainersAndLeaves -Append Grant-CNtfsPermission -Path C:\Bridge -Identity ENTERPRISE\Wesley -Permission 'Write' -ApplyTo ContainerAndLeaves -Append Demonstrates how to grant multiple access rules to a single identity with the `Append` switch. In this case, `ENTERPRISE\Wesley` will be able to read everything in `C:\Bridge` and write only in the `C:\Bridge` directory, not to any sub-directory. #> [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='DefaultAppliesToFlags')] [OutputType([Security.AccessControl.FileSystemAccessRule])] param( # The folder/file path on which the permissions should be granted. [Parameter(Mandatory)] [String] $Path, # The user or group getting the permissions. [Parameter(Mandatory)] [String] $Identity, # The permissions to grant. See # [System.Security.AccessControl.FileSystemRights](http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx) # for the list of rights with descriptions. [Parameter(Mandatory)] [FileSystemRights[]] $Permission, # How to apply the permissions. The default is `FolderSubfoldersAndFiles`. Valid values are: # # * FolderOnly # * FolderSubfoldersAndFiles # * FolderAndSubfolders # * FolderAndFiles # * SubfoldersAndFilesOnly # * SubfoldersOnly # * FilesOnly [Parameter(Mandatory, ParameterSetName='SetAppliesToFlags')] [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles', 'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')] [String] $ApplyTo, # Only apply the permissions to files and/or folders within the folder. Don't set this if the Path parameter is # to a file. [Parameter(ParameterSetName='SetAppliesToFlags')] [switch] $OnlyApplyToChildFilesAndFolders, # The type of rule to apply, either `Allow` or `Deny`. The default is `Allow`, which will allow access to the # item. The other option is `Deny`, which will deny access to the item. [AccessControlType] $Type = [AccessControlType]::Allow, # Removes all non-inherited permissions on the item. [switch] $Clear, # Returns an object representing the permission created or set on the `Path`. The returned object will have a # `Path` propery added to it so it can be piped to any cmdlet that uses a path. [switch] $PassThru, # Grants permissions, even if they are already present. [switch] $Force, # When set, adds the permissions as a new access rule instead of replacing any existing access rules. [switch] $Append ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if (-not $ApplyTo -and (Test-Path -Path $Path -PathType Container)) { $ApplyTo = 'FolderSubfoldersAndFiles' } if ($ApplyTo) { $PSBoundParameters['ApplyTo'] = $ApplyTo | ConvertTo-CarbonSecurityApplyTo } if ($PSBoundParameters.ContainsKey('OnlyApplyToChildFilesAndFolders')) { $PSBoundParameters.Remove('OnlyApplyToChildFilesAndFolders') $PSBoundParameters['OnlyApplyToChildren'] = $OnlyApplyToChildFilesAndFolders } Grant-CPermission @PSBoundParameters } function Revoke-CNtfsPermission { <# .SYNOPSIS Revokes *explicit* permissions on folders and files. .DESCRIPTION Revokes all of user/group's *explicit* permissions on a folder or file. Only explicit permissions are considered; inherited permissions are ignored. If the identity doesn't have permission, nothing happens, not even errors written out. .LINK Get-CNtfsPermission .LINK Grant-CNtfsPermission .LINK Test-CNtfsPermission .EXAMPLE Revoke-CNtfsPermission -Identity ENTERPRISE\Engineers -Path 'C:\EngineRoom' Demonstrates how to revoke all of the 'Engineers' permissions on the `C:\EngineRoom` directory. #> [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')] [CmdletBinding(SupportsShouldProcess)] param( # The folder or file path on which the permissions should be revoked. [Parameter(Mandatory)] [String] $Path, # The identity losing permissions. [Parameter(Mandatory)] [String] $Identity ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Revoke-CPermission @PSBoundParameters } function Test-CNtfsPermission { <# .SYNOPSIS Tests if permissions are set on a folder or file. .DESCRIPTION The `Test-CNtfsPermission` function tests if an identity has a permission on a file/folder. Pass the path to check to the `Path` parameter, the user/group name to the `Identity` parameter, and the permission to check for to the `Permission` parameter. If the user/group has the given permission on the given path, the function returns `$true`, otherwise it returns `$false`. Inherited permissions are *not* checked by default. To check inherited permission, use the `-Inherited` switch. By default, the permission check is not exact, i.e. the user may have additional permissions to what you're checking. If you want to make sure the user has *exactly* the permission you want, use the `-Strict` switch. Please note that by default, NTFS will automatically add/grant `Synchronize` permission on an item, which is handled by this function. You can also test how the permission is inherited by using the `ApplyTo` and `OnlyApplyToChildFilesAndFolders` parameters. .OUTPUTS System.Boolean. .LINK Get-CNtfsPermission .LINK Grant-CNtfsPermission .LINK Revoke-CNtfsPermission .LINK http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx .EXAMPLE Test-CNtfsPermission -Identity 'STARFLEET\JLPicard' -Permission 'FullControl' -Path 'C:\Enterprise\Bridge' Demonstrates how to check that Jean-Luc Picard has `FullControl` permission on the `C:\Enterprise\Bridge`. .EXAMPLE Test-CNtfsPermission -Identity 'STARFLEET\Worf' -Permission 'Write' -ApplyTo 'FolderOnly' -Path 'C:\Enterprise\Brig' Demonstrates how to test for inheritance/propogation flags, in addition to permissions. #> [CmdletBinding(DefaultParameterSetName='SkipAppliesToFlags')] param( # The path to a folder/file on which the permissions should be checked. [Parameter(Mandatory)] [String] $Path, # The user or group name whose permissions to check. [Parameter(Mandatory)] [String] $Identity, # The permission to test for: e.g. FullControl, Read, etc. See # [System.Security.AccessControl.FileSystemRights](http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx) # for the list of rights with descriptions. [Parameter(Mandatory)] [FileSystemRights[]] $Permission, # Checks how the permission is inherited. By default, the permission's inheritance is ignored. # # Valid values are: # # * FolderOnly # * FolderSubfoldersAndFiles # * FolderAndSubfolders # * FolderAndFiles # * SubfoldersAndFilesOnly # * SubfoldersOnly # * FilesOnly [Parameter(Mandatory, ParameterSetName='TestAppliesToFlags')] [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles', 'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')] [String] $ApplyTo, # Checks that the permissions are only applied to child files and folders. By default, the permission's # inheritnace is ignored. [Parameter(ParameterSetName='TestAppliesToFlags')] [switch] $OnlyApplyToChildFilesAndFolders, # Include inherited permissions in the check. [switch] $Inherited, # Check for the exact permissions and how the permission is applied, i.e. make sure the identity has # *only* the permissions you specify. [switch] $Strict ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if ($PSCmdlet.ParameterSetName -eq 'TestAppliesToFlags') { if ($ApplyTo) { $PSBoundParameters['ApplyTo'] = $ApplyTo | ConvertTo-CarbonSecurityApplyTo } $PSBoundParameters.Remove('OnlyApplyToChildFilesAndFolders') | Out-Null $PSBoundParameters['OnlyApplyToChildren'] = $OnlyApplyToChildFilesAndFolders } Test-CPermission @PSBoundParameters } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } |