Public/Grant-NTFSPermission.ps1

function Grant-NTFSPermission {
    <#
        .SYNOPSIS
            Grants NTFS permissions to files and folders with enhanced security controls.
 
        .DESCRIPTION
            Adds or modifies NTFS permissions on files and folders with:
            - Comprehensive error handling for specific scenarios
            - Support for all standard NTFS rights
            - Inheritance and propagation control options
            - Security principal validation against Active Directory
            - Path existence verification with detailed errors
            - Progress tracking for batch operations
            - Detailed logging with verbose and debug options
            - Support for -WhatIf and -Confirm parameters
            - Pipeline input support for batch processing
            - Performance optimizations for large environments
 
        .PARAMETER Path
            [String] Full path to the file or folder.
            Must exist and be accessible.
            Supports pipeline input.
 
        .PARAMETER Object
            [String] Security principal (user/group) receiving permissions.
            Must be resolvable in current domain/forest.
            Use format "Domain\Username" or "Domain\GroupName".
 
        .PARAMETER Permission
            [String] NTFS permission to grant. Valid values:
            - ReadAndExecute : Grants rights to read and execute files
            - AppendData : Grants rights to append data to files
            - CreateFiles : Grants rights to create new files within a folder
            - Read : Grants basic read access
            - Write : Grants basic write access
            - Modify : Grants read, write, and delete access
            - FullControl : Grants complete control over files and folders
 
        .PARAMETER NoInheritance
            [Switch] When specified, permissions are not inherited by child objects.
            By default, permissions are inherited by child objects.
 
        .PARAMETER ClearExisting
            [Switch] When specified, removes all existing permissions before applying new ones.
            Use with caution - can remove critical system permissions.
 
        .PARAMETER PassThru
            [Switch] Returns an object representing the modified ACL.
            By default, the function doesn't return any output.
 
        .EXAMPLE
            Grant-NTFSPermission -Path 'D:\Shares\Finance' -Object 'EguibarIT\Finance_RO' -Permission 'Read'
 
            Grants read access to Finance_RO group on Finance share.
 
        .EXAMPLE
            $params = @{
                Path = 'E:\Data'
                Object = 'EguibarIT\Backup_Operators'
                Permission = 'Modify'
            }
            Grant-NTFSPermission @params -Verbose
 
            Grants modify rights with verbose logging.
 
        .EXAMPLE
            Get-ChildItem -Path 'D:\Projects' -Directory | Grant-NTFSPermission -Object 'EguibarIT\Developers' -Permission 'Modify' -WhatIf
 
            Shows what would happen if modify permissions were granted to the Developers group on all subdirectories of Projects.
 
        .EXAMPLE
            Grant-NTFSPermission -Path 'D:\Confidential' -Object 'EguibarIT\Executives' -Permission 'FullControl' -NoInheritance -PassThru
 
            Grants full control to the Executives group without inheritance and returns the modified ACL.
 
        .INPUTS
            System.String
            You can pipe path strings to this function, allowing batch processing of multiple files and folders.
 
        .OUTPUTS
            [System.Security.AccessControl.FileSecurity] when -PassThru is specified
            [void] by default
 
        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ═══════════════════════════════════════╬════════════════════════
                Get-Acl ║ Microsoft.PowerShell.Security
                Set-Acl ║ Microsoft.PowerShell.Security
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility
                Write-Debug ║ Microsoft.PowerShell.Utility
                Write-Progress ║ Microsoft.PowerShell.Utility
                Get-ADObject ║ ActiveDirectory
                Get-FunctionDisplay ║ EguibarIT
 
        .NOTES
            Version: 1.3
            DateModified: 22/May/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                            vicente@eguibar.com
                            Eguibar IT
                            http://www.eguibarit.com
 
        .LINK
            https://github.com/vreguibar/EguibarIT/blob/main/Public/Grant-NTFSPermission.ps1
 
        .LINK
            https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.filesystemrights
 
        .LINK
            https://docs.microsoft.com/en-us/windows/win32/secauthz/access-control-lists
 
        .COMPONENT
            File System
 
        .ROLE
            Security Administration
 
        .FUNCTIONALITY
            NTFS Permissions Management
 
    #>


    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'Default'
    )]
    [OutputType([void])]
    [OutputType([System.Security.AccessControl.FileSecurity], ParameterSetName = 'PassThru')]

    Param (
        # Param1 path to the resource|folder
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'Absolute path to the file or folder',
            Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            { Test-Path $_ -PathType Any },
            ErrorMessage = 'Path does not exist or is not accessible: {0}'
        )]
        [Alias('FullName', 'FilePath', 'FolderPath')]
        [string]
        $path,

        # Param2 object or SecurityPrincipal
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'Name of the Identity getting the permission.',
            Position = 1)]
        [ValidateNotNullOrEmpty()]
        [Alias('GroupName', 'GroupID', 'Identity', 'SamAccountName')]
        [string]
        $object,

        # Param3 permission
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'NTFS permission to grant: ReadAndExecute, AppendData, CreateFiles, Read, Write, Modify, or FullControl',
            Position = 2)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('ReadAndExecute', 'AppendData', 'CreateFiles',
            'Read', 'Write', 'Modify', 'FullControl')]
        [string]
        $permission,

        # Disable inheritance
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Disable inheritance for this permission')]
        [switch]
        $NoInheritance,

        # Clear existing permissions
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Clear all existing permissions before applying new ones')]
        [switch]
        $ClearExisting,

        # Return the modified ACL
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Return the modified ACL',
            ParameterSetName = 'PassThru')]
        [switch]
        $PassThru
    )

    Begin {
        Set-StrictMode -Version Latest

        if ($null -ne $Variables -and
            $null -ne $Variables.Header) {

            $txt = ($Variables.Header -f
                (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
                (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end If

        ##############################
        # Module imports

        ##############################
        # Variables Definition

        [hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase)
        [int]$TotalItems = 0
        [int]$ProcessedItems = 0
        [bool]$ValidatePrincipal = $true  # Set to $false to skip AD validation for better performance
        [System.Collections.Generic.List[string]]$ProcessedPaths = [System.Collections.Generic.List[string]]::new()

        # Possible values for FileSystemRights are:
        # ReadAndExecute, AppendData, CreateFiles, read, write, Modify, FullControl
        # Initialize security flags
        $FileSystemRights = [Security.AccessControl.FileSystemRights]$Permission

        # Set inheritance flags based on NoInheritance parameter
        if ($PSBoundParameters['NoInheritance']) {

            $InheritanceFlag = [Security.AccessControl.InheritanceFlags]::None
            Write-Debug -Message 'Inheritance disabled'

        } else {

            $InheritanceFlag = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
            [Security.AccessControl.InheritanceFlags]::ObjectInherit
            Write-Debug -Message 'Inheritance enabled for container and object'

        } #end If-Else

        $PropagationFlag = [Security.AccessControl.PropagationFlags]::None
        $AccessControlType = [Security.AccessControl.AccessControlType]::Allow

        try {
            # Validate security principal
            Write-Debug -Message ('Validating security principal: {0}' -f $Object)
            $Account = [System.Security.Principal.NTAccount]::new($PSBoundParameters['Object'])

            # Validate the account only if validation is enabled
            if ($ValidatePrincipal) {
                # For performance in large environments, we could cache validated principals
                # or skip validation entirely if needed

                # Optional: Validate against AD
                # Uncomment this section if strict validation is required
                <#
                try {
                    # Try to translate to SID to validate the account
                    $null = $Account.Translate([System.Security.Principal.SecurityIdentifier])
                    Write-Debug -Message ('Security principal validated: {0}' -f $PSBoundParameters['Object'])
                } catch {
                    throw ('Invalid security principal: {0}. Error: {1}' -f $PSBoundParameters['Object'], $_.Exception.Message)
                }
                #>

            } #end If
        } catch {
            $ErrorMsg = ('Error creating security principal object for {0}: {1}' -f
                $PSBoundParameters['Object'], $_.Exception.Message)
            Write-Error -Message $ErrorMsg -Category InvalidArgument
            throw
        } #end Try-Catch

        Write-Verbose -Message ('
            Beginning NTFS permission change for {0}
            with {1} rights'
 -f $PSBoundParameters['Object'], $PSBoundParameters['Permission']
        )

    } #end Begin

    Process {
        # Count total items for progress bar
        $TotalItems = $PSBoundParameters['Path'].Count
        $ProcessedItems = 0

        # Process each path in the array
        foreach ($CurrentPath in $PSBoundParameters['Path']) {
            $ProcessedItems++

            # Skip if already processed (in case of duplicates)
            if ($ProcessedPaths.Contains($CurrentPath)) {

                Write-Debug -Message ('Skipping duplicate path: {0}' -f $CurrentPath)
                continue

            } #end If

            $ProcessedPaths.Add($CurrentPath)

            # Show progress
            $Splat = @{
                Activity        = 'Granting NTFS Permissions'
                Status          = ('Processing {0}' -f $CurrentPath)
                PercentComplete = (($ProcessedItems / $TotalItems) * 100)
            }
            Write-Progress @Splat

            Write-Debug -Message ('Processing path: {0}' -f $CurrentPath)

            # Create the FileSystemAccessRule object
            $FileSystemAccessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList (
                $Account,
                $FileSystemRights,
                $InheritanceFlag,
                $PropagationFlag,
                $AccessControlType
            )

            try {
                # Get current ACL
                $DirectorySecurity = Get-Acl -Path $CurrentPath

                # Create descriptive action for ShouldProcess
                $ShouldProcessDescription = ('Grant {0} permissions to {1} on {2}' -f
                    $PSBoundParameters['Permission'],
                    $PSBoundParameters['Object'],
                    $CurrentPath)

                # Process only if ShouldProcess approves
                if ($PSCmdlet.ShouldProcess($CurrentPath, $ShouldProcessDescription)) {

                    # Clear existing permissions if requested
                    if ($PSBoundParameters['ClearExisting']) {

                        if ($PSCmdlet.ShouldContinue(
                            ('WARNING: About to remove ALL existing permissions on {0}. Continue?' -f $CurrentPath),
                                'Confirm Permission Removal')) {

                            $DirectorySecurity.SetAccessRuleProtection($true, $false)
                            Write-Debug -Message ('Cleared existing permissions on {0}' -f $CurrentPath)

                        } else {

                            Write-Verbose -Message ('User cancelled clearing permissions on {0}' -f $CurrentPath)
                            continue

                        } #end If-Else
                    } #end If

                    # Add the new access rule
                    $DirectorySecurity.AddAccessRule($FileSystemAccessRule)

                    # Apply the modified ACL
                    Set-Acl -Path $CurrentPath -AclObject $DirectorySecurity

                    Write-Verbose -Message ('Successfully granted {0} permissions to {1} on {2}' -f
                        $PSBoundParameters['Permission'],
                        $PSBoundParameters['Object'],
                        $CurrentPath)

                    # Return the ACL if PassThru is specified
                    if ($PSBoundParameters['PassThru']) {

                        Get-Acl -Path $CurrentPath
                    } #end If

                } #end If

            } catch [System.UnauthorizedAccessException] {

                $ErrorMsg = ('Access denied. Cannot modify permissions on {0}. Error: {1}' -f
                    $CurrentPath, $_.Exception.Message)
                Write-Error -Message $ErrorMsg -Category PermissionDenied
                continue

            } catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {

                $ErrorMsg = ('Path no longer exists: {0}. Error: {1}' -f
                    $CurrentPath, $_.Exception.Message)
                Write-Error -Message $ErrorMsg -Category ObjectNotFound
                continue

            } catch [System.Security.Principal.IdentityNotMappedException] {

                $ErrorMsg = ('Security principal cannot be mapped: {0}. Error: {1}' -f
                    $PSBoundParameters['Object'], $_.Exception.Message)
                Write-Error -Message $ErrorMsg -Category InvalidData
                throw

            } catch {

                $ErrorMsg = ('Error granting NTFS permissions on {0}. Error: {1}' -f
                    $CurrentPath, $_.Exception.Message)
                Write-Error -Message $ErrorMsg
                continue

            } #end Try-Catch
        } #end Foreach

        # Complete progress bar
        Write-Progress -Activity 'Granting NTFS Permissions' -Completed

    } #end Process

    End {
        if ($null -ne $Variables -and
            $null -ne $Variables.Footer) {

            $txt = ($Variables.Footer -f $MyInvocation.InvocationName,
                'changing NTFS permissions.'
            )
            Write-Verbose -Message $txt
        } #end If
    } #end End

} #end Function Grant-NTFSPermission