DscResources/NTFSAccessEntry/NTFSAccessEntry.psm1

$resourceRootPath = Split-Path -Path $PSScriptRoot -Parent
$resourceHelperPath = Join-Path -Path $resourceRootPath -ChildPath 'AccessControlResourceHelper'
$resourceHelperPsm1 = Join-Path -Path $resourceHelperPath -ChildPath 'AccessControlResourceHelper.psm1'
Import-Module -Name $resourceHelperPsm1 -Force

try
{
    $importLocalizedDataParams = @{
        BaseDirectory = $resourceHelperPath
        UICulture     = $PSUICulture
        FileName      = 'AccessControlResourceHelper.strings.psd1'
        ErrorAction   = 'Stop'
    }
    $script:localizedData = Import-LocalizedData @importLocalizedDataParams
}
catch
{
    $importLocalizedDataParams.UICulture = 'en-US'
    try
    {
        $script:localizedData = Import-LocalizedData @importLocalizedDataParams
    }
    catch
    {
        throw 'Unable to load localized data'
    }
}


Function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AccessControlList,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $nameSpace = "root/Microsoft/Windows/DesiredStateConfiguration"
    $cimAccessControlList = New-Object -TypeName 'System.Collections.ObjectModel.Collection`1[Microsoft.Management.Infrastructure.CimInstance]'
    $inputPath = Get-InputPath -Path $Path

    if (Test-Path -Path $inputPath)
    {
        $currentAcl = Get-AccessControl -Path $inputPath -AccessControlSection 'Access'

        if ($null -ne $currentAcl)
        {
            $message = $localizedData.AclFound -f $inputPath
            Write-Verbose -Message $message

            foreach ($principal in $AccessControlList)
            {
                $cimAccessControlEntry = New-Object -TypeName 'System.Collections.ObjectModel.Collection`1[Microsoft.Management.Infrastructure.CimInstance]'

                $principalName = $principal.Principal
                $forcePrincipal = $principal.ForcePrincipal

                $identity = Resolve-Identity -Identity $principalName
                $currentPrincipalAccess = $currentAcl.Access.Where({$_.IdentityReference -eq $identity.Name})

                foreach ($access in $currentPrincipalAccess)
                {
                    $accessControlType = $access.AccessControlType.ToString()
                    $fileSystemRights = $access.FileSystemRights.ToString().Split(',').Trim()
                    $Inheritance = Get-NtfsInheritenceName -InheritanceFlag $access.InheritanceFlags.value__ -PropagationFlag $access.PropagationFlags.value__

                    $cimAccessControlEntry += New-CimInstance -ClientOnly -Namespace $nameSpace -ClassName NTFSAccessControlEntry -Property @{
                        AccessControlType = $accessControlType
                        FileSystemRights = @($fileSystemRights)
                        Inheritance = $Inheritance
                        Ensure = ""
                    }
                }

                $cimAccessControlList += New-CimInstance -ClientOnly -Namespace $nameSpace -ClassName NTFSAccessControlList -Property @{
                    Principal = $principalName
                    ForcePrincipal = $forcePrincipal
                    AccessControlEntry = [Microsoft.Management.Infrastructure.CimInstance[]]@($cimAccessControlEntry)
                }
            }

        }
        else
        {
            $message = $localizedData.AclNotFound -f $inputPath
            Write-Verbose -Message $message
        }
    }
    else
    {
        $Message = $localizedData.ErrorPathNotFound -f $inputPath
        Write-Verbose -Message $Message
    }

    $returnValue = @{
        Force = $Force
        Path = $inputPath
        AccessControlList = $cimAccessControlList
    }

    return $returnValue
}

Function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AccessControlList,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $aclRules = @()

    $inputPath = Get-InputPath -Path $Path

    if (Test-Path -Path $inputPath)
    {
        $currentAcl = Get-AccessControl -Path $inputPath -AccessControlSection 'Access'

        if ($null -ne $currentAcl)
        {
            if ($Force)
            {
                # If inheritance is set, disable it and clear inherited access rules
                if (-not $currentAcl.AreAccessRulesProtected)
                {
                    Write-Verbose -Message ($localizedData.ResetDisableInheritance)
                    $currentAcl.SetAccessRuleProtection($true, $false)
                }

                # Removing all access rules to ensure a blank list
                if ($null -ne $currentAcl.Access)
                {
                    foreach ($ace in $currentAcl.Access)
                    {
                        # Added this condition and function to address Win32 API Bug: https://github.com/PowerShell/Win32-OpenSSH/issues/750
                        if ($ace.IdentityReference -match 'APPLICATION PACKAGE AUTHORITY\\.*')
                        {
                            $ace = Update-NtfsAccessControlEntry -AccessControlEntry $ace
                        }

                        $currentAcl.RemoveAccessRuleAll($ace)
                        Write-CustomVerboseMessage -Action 'ActionRemoveAccess' -Path $inputPath -Rule $ace
                    }
                }
            }

            foreach ($accessControlItem in $AccessControlList)
            {
                $principal = $accessControlItem.Principal
                $identity = Resolve-Identity -Identity $principal
                $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                $actualAce = $currentAcl.Access.Where({$_.IdentityReference -eq $identity.Name})
                $aclRules = ConvertTo-FileSystemAccessRule -AccessControlList $accessControlItem -IdentityRef $identityRef
                $results = Compare-NtfsRule -Expected $aclRules -Actual $actualAce -Force $accessControlItem.ForcePrincipal
                $expected += $results.Rules
                $toBeRemoved += $results.Absent

                if ($accessControlItem.ForcePrincipal)
                {
                    $toBeRemoved += $results.ToBeRemoved
                }
            }

            $isInherited = $toBeRemoved.Rule.Where({$_.IsInherited -eq $true}).Count

            if ($isInherited -gt 0)
            {
                $currentAcl.SetAccessRuleProtection($true, $true)
                Set-AccessControl -Path $inputPath -Acl $currentAcl
                $currentAcl = Get-AccessControl -Path $inputPath -AccessControlSection 'Access'
            }

            foreach ($rule in $toBeRemoved.Rule)
            {
                try
                {
                    Write-CustomVerboseMessage -Action 'ActionRemoveAccess' -Path $inputPath -Rule $rule
                    $currentAcl.RemoveAccessRuleSpecific($rule)
                }
                catch
                {
                    try
                    {
                        #If failure due to Idenitity translation issue then create the same rule with the identity as a sid to remove account
                        $sid = ConvertTo-SID -IdentityReference $rule.IdentityReference.Value
                        $sidRule = New-Object System.Security.AccessControl.FileSystemRights($sid, $rule.FileSystemRights.value__, $rule.InheritanceFlags.value__, $rule.PropagationFlags.value__, $rule.AccessControlType.value__)
                        Write-CustomVerboseMessage -Action 'ActionRemoveAccess' -Path $inputPath -Rule $sidRule
                        $currentAcl.RemoveAccessRuleSpecific($sidRule)
                    }
                    catch
                    {
                        Write-Verbose -Message ($localizedData.AclNotFound -f $($rule.IdentityReference.Value))
                    }
                }
            }

            foreach ($rule in $expected.Rule)
            {
                Write-CustomVerboseMessage -Action 'ActionAddAccess' -Path $inputPath -Rule $rule
                $currentAcl.AddAccessRule($rule)
            }

            Set-AccessControl -Path $inputPath -Acl $currentAcl
        }
        else
        {
            Write-Verbose -Message ($localizedData.AclNotFound -f $inputPath)
        }
    }
    else
    {
        Write-Verbose -Message ($localizedData.ErrorPathNotFound -f $inputPath)
    }
}

Function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AccessControlList,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $aclRules = @()
    $inDesiredState = $true
    $inputPath = Get-InputPath -Path $Path

    if (Test-Path -Path $inputPath)
    {
        $currentAcl = Get-AccessControl -Path $inputPath -AccessControlSection 'Access'
        $mappedAcl = Update-FileSystemRightsMapping($currentAcl)

        if ($null -ne $currentAcl)
        {
            if ($Force)
            {
                if ($currentAcl.AreAccessRulesProtected -eq $false)
                {
                    Write-Verbose -Message ($localizedData.InheritanceDetectedForce -f $Force, $inputPath)
                    return $false
                }

                foreach ($accessControlItem in $AccessControlList)
                {
                    $principal = $accessControlItem.Principal
                    $identity = Resolve-Identity -Identity $principal
                    $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                    $aclRules += ConvertTo-FileSystemAccessRule -AccessControlList $accessControlItem -IdentityRef $identityRef
                }

                $actualAce = $mappedAcl.Access
                $results = Compare-NtfsRule -Expected $aclRules -Actual $actualAce -Force $accessControlItem.ForcePrincipal
                $expected = $results.Rules
                $absentToBeRemoved = $results.Absent
                $toBeRemoved = $results.ToBeRemoved
            }
            else
            {
                foreach ($accessControlItem in $AccessControlList)
                {
                    $principal = $accessControlItem.Principal
                    $identity = Resolve-Identity -Identity $principal
                    $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                    $aclRules = ConvertTo-FileSystemAccessRule -AccessControlList $accessControlItem -IdentityRef $identityRef
                    $actualAce = $mappedAcl.Access.Where({$_.IdentityReference -eq $identity.Name})
                    $results = Compare-NtfsRule -Expected $aclRules -Actual $actualAce -Force $accessControlItem.ForcePrincipal
                    $expected += $results.Rules
                    $absentToBeRemoved += $results.Absent

                    if ($accessControlItem.ForcePrincipal)
                    {
                        $toBeRemoved += $results.ToBeRemoved
                    }
                }
            }

            foreach ($rule in $expected)
            {
                if ($rule.Match -eq $false)
                {
                    Write-CustomVerboseMessage -Action 'ActionMissPresentPerm' -Path $inputPath -Rule $rule.rule
                    $inDesiredState = $false
                }
            }

            if ($absentToBeRemoved.Count -gt 0)
            {
                foreach ($rule in $absentToBeRemoved.Rule)
                {
                    Write-CustomVerboseMessage -Action 'ActionAbsentPermission' -Path $inputPath -Rule $rule
                }

                $inDesiredState = $false
            }

            if ($toBeRemoved.Count -gt 0)
            {
                foreach ($rule in $toBeRemoved.Rule)
                {
                    Write-CustomVerboseMessage -Action 'ActionNonMatchPermission' -Path $inputPath -Rule $rule
                }

                $inDesiredState = $false
            }
        }
        else
        {
            Write-Verbose -Message ($localizedData.AclNotFound -f $inputPath)
            $inDesiredState = $false
        }
    }
    else
    {
        Write-Verbose -Message ($localizedData.ErrorPathNotFound -f $inputPath)
        $inDesiredState = $false
    }

    return $inDesiredState
}

Function ConvertTo-FileSystemAccessRule
{
    param
    (
        [Parameter(Mandatory = $true)]
        [Microsoft.Management.Infrastructure.CimInstance]
        $AccessControlList,

        [Parameter(Mandatory = $true)]
        [System.Security.Principal.NTAccount]
        $IdentityRef
    )

    $referenceRule = @()

    if
    (
        $IdentityRef -match 'APPLICATION PACKAGE AUTHORITY\\.*' -and
        (Get-PSCallStack)[1].Command -eq 'Set-TargetResource'
    )
    {
        $identityRef = Remove-NtPrincipalDomain -Identity $IdentityRef
    }

    foreach ($ace in $AccessControlList.AccessControlEntry)
    {
        $inheritance = Get-NtfsInheritenceFlag -Inheritance $ace.Inheritance
        $rule = [PSCustomObject]@{
            Rules = New-Object System.Security.AccessControl.FileSystemAccessRule(
                $IdentityRef,
                $ace.FileSystemRights,
                $Inheritance.InheritanceFlag,
                $Inheritance.PropagationFlag,
                $ace.AccessControlType
            )
            Ensure = $ace.Ensure
        }

        $referenceRule += $rule
    }

    return $referenceRule
}

Function Compare-NtfsRule
{
    param
    (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]
        $Expected,

        [Parameter()]
        [System.Security.AccessControl.FileSystemAccessRule[]]
        $Actual,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $results = @()
    $toBeRemoved = @()
    $absentToBeRemoved = @()
    $presentRules = $Expected.Where({$_.Ensure -eq 'Present'}).Rules
    $absentRules = $Expected.Where({$_.Ensure -eq 'Absent'}).Rules

    foreach ($referenceRule in $PresentRules)
    {
        $match = Test-FileSystemAccessRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Actual -Force $Force

        if
        (
            ($match.Count -ge 1) -and
            ($match.FileSystemRights.value__ -ge $referenceRule.FileSystemRights.value__)
        )
        {
            $results += [PSCustomObject]@{
                Rule  = $referenceRule
                Match = $true
            }
        }
        else
        {
            $results += [PSCustomObject]@{
                Rule  = $referenceRule
                Match = $false
            }
        }
    }

    foreach ($referenceRule in $AbsentRules)
    {
        $match = Test-FileSystemAccessRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Actual -Force $Force

        if ($match.Count -gt 0)
        {
            $absentToBeRemoved += [PSCustomObject]@{
                Rule = $match
            }
        }
    }

    foreach ($referenceRule in $Actual)
    {
        $match = Test-FileSystemAccessRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Expected.Rules -Force $Force

        if ($match.Count -eq 0)
        {
            $toBeRemoved += [PSCustomObject]@{
                Rule = $referenceRule
            }
        }
    }

    return [PSCustomObject]@{
        Rules = $results
        ToBeRemoved = $toBeRemoved
        Absent = $absentToBeRemoved
    }
}

Function Update-FileSystemRightsMapping
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $Ace
    )

    foreach ($rule in $Ace.Access)
    {
        $rightsBand = [int]0xf0000000 -band $rule.FileSystemRights.value__
        if (($rightsBand -gt 0) -or ($rightsBand -lt 0))
        {
            $sid = ConvertTo-SID -IdentityReference $rule.IdentityReference
            $mappedRight = Get-MappedGenericRight($rule.FileSystemRights)
            $mappedRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
                $sid,
                $mappedRight,
                $rule.InheritanceFlags,
                $rule.PropagationFlags,
                $rule.AccessControlType
            )

            try
            {
                $Ace.RemoveAccessRule($rule)
            }
            catch
            {
                $sidRule = $Ace.AccessRuleFactory(
                    $sid,
                    $rule.FileSystemRights,
                    $rule.IsInherited,
                    $rule.InheritanceFlags,
                    $rule.PropagationFlags,
                    $rule.AccessControlType
                )
                $Ace.RemoveAccessRule($sidRule)
            }

            $Ace.AddAccessRule($mappedRule)
        }
    }

    return $Ace
}

Function Get-MappedGenericRight
{
    param
    (
        [Parameter(Mandatory = $true)]
        [int]
        $Rights
    )

    [int]$genericRead = 0x80000000
    [int]$genericWrite = 0x40000000
    [int]$genericExecute = 0x20000000
    [int]$genericFullControl = 0x10000000
    [int]$fsarGenericRead = (
        [System.Security.AccessControl.FileSystemRights]::ReadAttributes -bor
        [System.Security.AccessControl.FileSystemRights]::ReadData -bor
        [System.Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
        [System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
        [System.Security.AccessControl.FileSystemRights]::Synchronize
    )

    [int]$fsarGenericWrite = (
        [System.Security.AccessControl.FileSystemRights]::AppendData -bor
        [System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
        [System.Security.AccessControl.FileSystemRights]::WriteData -bor
        [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
        [System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
        [System.Security.AccessControl.FileSystemRights]::Synchronize
    )

    [int]$fsarGenericExecute = (
        [System.Security.AccessControl.FileSystemRights]::ExecuteFile -bor
        [System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
        [System.Security.AccessControl.FileSystemRights]::ReadAttributes -bor
        [System.Security.AccessControl.FileSystemRights]::Synchronize
    )

    [int]$fsarGenericFullControl = [System.Security.AccessControl.FileSystemRights]::FullControl
    $fsarRights = 0

    if (($Rights -band $genericRead) -eq $genericRead)
    {
        $fsarRights = $fsarRights -bor $fsarGenericRead
    }

    if (($Rights -band $genericWrite) -eq $genericWrite)
    {
        $fsarRights = $fsarRights -bor  $fsarGenericWrite
    }

    if (($Rights -band $genericExecute) -eq $genericExecute)
    {
        $fsarRights = $fsarRights -bor  $fsarGenericExecute
    }

    if (($Rights -band $genericFullControl) -eq $genericFullControl)
    {
        $fsarRights = $fsarRights -bor  $fsarGenericFullControl
    }

    if ($fsarRights -ne 0)
    {
        return $fsarRights
    }

    return $Rights
}

Function Get-InputPath
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path
    )

    $returnPath = $Path

    # If Path has a environment variable, convert it to a locally usable path
    $returnPath = [System.Environment]::ExpandEnvironmentVariables($Path)

    return $returnPath
}

function Test-FileSystemAccessRuleMatch
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.FileSystemAccessRule[]]
        [AllowEmptyCollection()]
        $DifferenceRule,

        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.FileSystemAccessRule]
        $ReferenceRule,

        [Parameter(Mandatory = $true)]
        [bool]
        $Force
    )

    if ($Force)
    {
        $DifferenceRule.Where({
                $_.FileSystemRights -eq $ReferenceRule.FileSystemRights -and
                $_.InheritanceFlags -eq $ReferenceRule.InheritanceFlags -and
                $_.PropagationFlags -eq $ReferenceRule.PropagationFlags -and
                $_.AccessControlType -eq $ReferenceRule.AccessControlType -and
                $_.IdentityReference -eq $ReferenceRule.IdentityReference
            })
    }
    else
    {
        $DifferenceRule.Where({
                ($_.FileSystemRights.value__ -band $ReferenceRule.FileSystemRights.value__) -match
                "$($_.FileSystemRights.value__)|$($ReferenceRule.FileSystemRights.value__)" -and
                (($_.InheritanceFlags.value__ -eq 3 -and $ReferenceRule.InheritanceFlags.value__ -in 1..3) -or
                ($_.InheritanceFlags.value__ -in 1..3 -and $ReferenceRule.InheritanceFlags.value__ -eq 0) -or
                ($_.InheritanceFlags.value__ -eq $ReferenceRule.InheritanceFlags.value__)) -and
                (($_.PropagationFlags.value__ -eq 3 -and $ReferenceRule.PropagationFlags.value__ -in 1..3) -or
                ($_.PropagationFlags.value__ -in 1..3 -and $ReferenceRule.PropagationFlags.value__ -eq 0) -or
                ($_.PropagationFlags.value__ -eq $ReferenceRule.PropagationFlags.value__)) -and
                $_.AccessControlType -eq $ReferenceRule.AccessControlType -and
                $_.IdentityReference -eq $ReferenceRule.IdentityReference
        })
    }
}

function Update-NtfsAccessControlEntry
{
    [CmdletBinding()]
    [OutputType([System.Security.AccessControl.FileSystemAccessRule])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.FileSystemAccessRule]
        $AccessControlEntry
    )

    try
    {
        # there are cases where the permission does not enum correctly, detecting this case and handling accordingly
        $fileSystemRightsString = $AccessControlEntry.FileSystemRights.ToString()
        $fileSystemRights = [System.Security.AccessControl.FileSystemRights]$fileSystemRightsString
    }
    catch
    {
        switch ($fileSystemRightsString)
        {
            '268435456'   {[System.Security.AccessControl.FileSystemRights]$fileSystemRights = 'FullControl'}
            '-536805376'  {[System.Security.AccessControl.FileSystemRights]$fileSystemRights = 'Modify', 'Synchronize'}
            '-1610612736' {[System.Security.AccessControl.FileSystemRights]$fileSystemRights = 'ReadAndExecute', 'Synchronize'}
        }
    }

    $identity = Remove-NtPrincipalDomain -Identity $AccessControlEntry.IdentityReference
    $ace = New-Object -Type System.Security.AccessControl.FileSystemAccessRule -ArgumentList (
        $identity,
        $fileSystemRights,
        $AccessControlEntry.AccessControlType
    )
    return $ace
}

function Get-AccessControl
{
    [CmdletBinding()]
    [OutputType(
        [System.Security.AccessControl.DirectorySecurity],
        [System.Security.AccessControl.FileSecurity]
    )]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $Path,

        [Parameter(Mandatory = $false)]
        [System.Security.AccessControl.AccessControlSections]
        $AccessControlSection = 'Access'
    )

    try
    {
        $fileSystemItem = Get-Item -Path $Path -ErrorAction Stop
    }
    catch
    {
        throw $_
    }

    if ($fileSystemItem.PSIsContainer -eq $true)
    {
        return [System.Security.AccessControl.DirectorySecurity]::new($fileSystemItem.FullName, $AccessControlSection)
    }
    else
    {
        return [System.Security.AccessControl.FileSecurity]::new($fileSystemItem.FullName, $AccessControlSection)
    }
}

function Set-AccessControl
{
    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $Path,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
            $_ -is [System.Security.AccessControl.DirectorySecurity] -or
            $_ -is [System.Security.AccessControl.FileSecurity]
        })]
        [object]
        $Acl
    )

    try
    {
        $fileSystemItem = Get-Item -Path $Path -ErrorAction Stop
    }
    catch
    {
        throw $_
    }

    if ($PSVersionTable.PSVersion -gt [version]'7.0')
    {
        [System.IO.FileSystemAclExtensions]::SetAccessControl($fileSystemItem, $Acl)
    }
    else
    {
        $fileSystemItem.SetAccessControl($Acl)
    }
}