DscResources/NTFSAccessEntry/NTFSAccessEntry.psm1

Import-Module -Name (Join-Path -Path ( Split-Path $PSScriptRoot -Parent ) `
                               -ChildPath 'AccessControlResourceHelper\AccessControlResourceHelper.psm1') `
                               -Force

# Localized messages
data LocalizedData
{
    # culture="en-US"
    ConvertFrom-StringData -StringData @'
        ErrorPathNotFound = The requested path "{0}" cannot be found.
        AclNotFound = Error obtaining "{0}" ACL
        AclFound = Obtained "{0}" ACL
        RemoveAccessError = "Unable to remove Access for "{0}"
'@

}

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)

    if(Test-Path -Path $inputPath)
    {
        $currentACL = Get-Acl -Path $inputPath -ErrorAction Stop

        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)

    if(Test-Path -Path $inputPath)
    {
        $currentAcl = Get-Acl -Path $inputPath
        if($null -ne $currentAcl)
        {
            if($Force)
            {
                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 = $currentAcl.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)

                    $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
                    $AbsentToBeRemoved += $Results.Absent

                    if($AccessControlItem.ForcePrincipal)
                    {
                        $ToBeRemoved += $Results.ToBeRemoved
                    }
                }
            }

            $isInherited = 0
            $isInherited += $AbsentToBeRemoved.Rule.Where({$_.IsInherited -eq $true}).Count
            $isInherited += $ToBeRemoved.Rule.Where({$_.IsInherited -eq $true}).Count

            if($isInherited -gt 0)
            {
                $currentAcl.SetAccessRuleProtection($true,$true)
                Set-Acl -Path $inputPath -AclObject $currentAcl             
            }
            

            foreach($Rule in $ToBeRemoved.Rule)
            {
                try
                {
                    ("Removing access rule:"),
                    ("> Principal : '{0}'" -f $Rule.IdentityReference),
                    ("> Path : '{0}'" -f $inputPath),
                    ("> IdentityReference : '{0}'" -f $Rule.IdentityReference),
                    ("> AccessControlType : '{0}'" -f $Rule.AccessControlType),
                    ("> FileSystemRights : '{0}'" -f $Rule.FileSystemRights),
                    ("> InheritanceFlags : '{0}'" -f $Rule.InheritanceFlags),
                    ("> PropagationFlags : '{0}'" -f $Rule.PropagationFlags) |
                    Write-Verbose
                    $currentAcl.RemoveAccessRule($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__)
                        $currentAcl.RemoveAccessRule($SIDRule)
                    }
                    catch
                    {
                        $message = $LocalizedData.AclNotFound -f $($Rule.IdentityReference.Value)
                        Write-Verbose -Message $message
                    }
                }
            }

            foreach($Rule in $AbsentToBeRemoved.Rule)
            {
                ("Removing access rule:"),
                ("> Principal : '{0}'" -f $Rule.IdentityReference),
                ("> Path : '{0}'" -f $inputPath),
                ("> IdentityReference : '{0}'" -f $Rule.IdentityReference),
                ("> AccessControlType : '{0}'" -f $Rule.AccessControlType),
                ("> FileSystemRights : '{0}'" -f $Rule.FileSystemRights),
                ("> InheritanceFlags : '{0}'" -f $Rule.InheritanceFlags),
                ("> PropagationFlags : '{0}'" -f $Rule.PropagationFlags) |
                Write-Verbose

                $currentAcl.RemoveAccessRule($Rule)
            }                      

            foreach($Rule in $Expected)
            {
                    $NonMatch = $Rule.Rule
                    ("Adding access rule:"),
                    ("> Principal : '{0}'" -f $NonMatch.IdentityReference),
                    ("> Path : '{0}'" -f $inputPath),
                    ("> IdentityReference : '{0}'" -f $NonMatch.IdentityReference),
                    ("> AccessControlType : '{0}'" -f $NonMatch.AccessControlType),
                    ("> FileSystemRights : '{0}'" -f $NonMatch.FileSystemRights),
                    ("> InheritanceFlags : '{0}'" -f $NonMatch.InheritanceFlags),
                    ("> PropagationFlags : '{0}'" -f $NonMatch.PropagationFlags) |
                    Write-Verbose

                    $currentAcl.AddAccessRule($Rule.Rule)
            }
            
            Set-Acl -Path $inputPath -AclObject $currentAcl
        }
        else
        {
            $message = $LocalizedData.AclNotFound -f $inputPath
            Write-Verbose -Message $message
        }
    }
    else
    {
        $Message = $LocalizedData.ErrorPathNotFound -f $inputPath
        Write-Verbose -Message $Message
    }
}

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)
  
    if(Test-Path -Path $inputPath)
    {
        $currentACL = Get-Acl -Path $inputPath
        $mappedACL = Update-FileSystemRightsMapping($currentAcl)

        if($null -ne $currentACL)
        {
            if($Force)
            {
                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)
                {
                    $NonMatch = $Rule.Rule
                    ("Found missing [present] permission rule:"),
                    ("> Principal : '{0}'" -f $NonMatch.IdentityReference),
                    ("> Path : '{0}'" -f $inputPath),
                    ("> IdentityReference : '{0}'" -f $NonMatch.IdentityReference),
                    ("> AccessControlType : '{0}'" -f $NonMatch.AccessControlType),
                    ("> FileSystemRights : '{0}'" -f $NonMatch.FileSystemRights),
                    ("> InheritanceFlags : '{0}'" -f $NonMatch.InheritanceFlags),
                    ("> PropagationFlags : '{0}'" -f $NonMatch.PropagationFlags) |
                    Write-Verbose

                    $InDesiredState = $False
                }
            }

            if($AbsentToBeRemoved.Count -gt 0)
            {
                foreach ($rule in $AbsentToBeRemoved)
                {
                    ("Found [absent] permission rule:"),
                    ("> Principal : '{0}'" -f $Rule.IdentityReference),
                    ("> Path : '{0}'" -f $inputPath),
                    ("> IdentityReference : '{0}'" -f $Rule.IdentityReference),
                    ("> AccessControlType : '{0}'" -f $Rule.AccessControlType),
                    ("> FileSystemRights : '{0}'" -f $Rule.FileSystemRights),
                    ("> InheritanceFlags : '{0}'" -f $Rule.InheritanceFlags),
                    ("> PropagationFlags : '{0}'" -f $Rule.PropagationFlags) |
                    Write-Verbose
                }

                $InDesiredState = $False
            }

            if($ToBeRemoved.Count -gt 0)
            {
                foreach ($Rule in $ToBeRemoved)
                {
                    ("Non-matching permission entry found:"),
                    ("> Principal : '{0}'" -f $Rule.IdentityReference),
                    ("> Path : '{0}'" -f $inputPath),
                    ("> IdentityReference : '{0}'" -f $Rule.IdentityReference),
                    ("> AccessControlType : '{0}'" -f $Rule.AccessControlType),
                    ("> FileSystemRights : '{0}'" -f $Rule.FileSystemRights),
                    ("> InheritanceFlags : '{0}'" -f $Rule.InheritanceFlags),
                    ("> PropagationFlags : '{0}'" -f $Rule.PropagationFlags) |
                    Write-Verbose
                }

                $InDesiredState = $False
            }
        }
        else
        {
            $message = $LocalizedData.AclNotFound -f $inputPath
            Write-Verbose -Message $message
            $InDesiredState = $False
        }
    }
    else
    {
        $Message = $LocalizedData.ErrorPathNotFound -f $inputPath
        Write-Verbose -Message $Message
        $InDesiredState = $False
    }
    
    return $InDesiredState
}

Function Get-NtfsInheritenceFlag
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Inheritance
    )

    switch($Inheritance)
    {
        "This folder only"{
            $InheritanceFlag = "0"
            $PropagationFlag = "0"
            break

        }
        "This folder subfolders and files"{
            $InheritanceFlag = "3"
            $PropagationFlag = "0"
            break

        }
        "This folder and subfolders"{
            $InheritanceFlag = "1"
            $PropagationFlag = "0"
            break
        }
        "This folder and files"{
            $InheritanceFlag = "2"
            $PropagationFlag = "0"
            break

        }
        "Subfolders and files only"{
            $InheritanceFlag = "3"
            $PropagationFlag = "2"
            break

        }
        "Subfolders only"{
            $InheritanceFlag = "1"
            $PropagationFlag = "2"
            break
        }
        "Files only"{
            $InheritanceFlag = "2"
            $PropagationFlag = "2"
            break
        }
    }

    return [PSCustomObject]@{
                InheritanceFlag = $InheritanceFlag
                PropagationFlag = $PropagationFlag
            }
}

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

        [Parameter(Mandatory = $true)]
        [System.String]
        $PropagationFlag
    )

    switch("$InheritanceFlag-$PropagationFlag")
    {
        "0-0"{
            return "This folder only"
        }
        "3-0"{
            return "This folder subfolders and files"
        }
        "1-0"{
            return "This folder and subfolders"
        }
        "2-0"{
            return "This folder and files"
        }
        "3-2"{
            return "Subfolders and files only"
        }
        "1-2"{
            return "Subfolders Only"
        }
        "2-2"{
            return "Files Only"
        }
    }

    return "none"
}

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

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

    $refrenceObject = @()

    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
        }
        $refrenceObject += $rule
    }

    return $refrenceObject
}

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($refrenceObject in $PresentRules)
    {
        $match = $Actual.Where({
            (((($_.FileSystemRights.value__ -band $refrenceObject.FileSystemRights.value__) -match "$($_.FileSystemRights.value__)|$($refrenceObject.FileSystemRights.value__)") -and !$Force) -or ($_.FileSystemRights -eq $refrenceObject.FileSystemRights -and $Force)) -and
            $_.InheritanceFlags -eq $refrenceObject.InheritanceFlags -and
            $_.PropagationFlags -eq $refrenceObject.PropagationFlags -and
            $_.AccessControlType -eq $refrenceObject.AccessControlType -and
            $_.IdentityReference -eq $refrenceObject.IdentityReference
        })
        if($match.Count -ge 1)
        {
            $results += [PSCustomObject]@{
                Rule = $refrenceObject
                Match = $true
            }
        }
        else
        {
            $results += [PSCustomObject]@{
                Rule = $refrenceObject
                Match = $false
            }
        }
    }

    foreach($refrenceObject in $AbsentRules)
    {
        $match = $Actual.Where({
            (((($_.FileSystemRights.value__ -band $refrenceObject.FileSystemRights.value__) -match "$($_.FileSystemRights.value__)|$($refrenceObject.FileSystemRights.value__)") -and !$Force) -or ($_.FileSystemRights -eq $refrenceObject.FileSystemRights -and $Force)) -and
            $_.InheritanceFlags -eq $refrenceObject.InheritanceFlags -and
            $_.PropagationFlags -eq $refrenceObject.PropagationFlags -and
            $_.AccessControlType -eq $refrenceObject.AccessControlType -and
            $_.IdentityReference -eq $refrenceObject.IdentityReference
        })
        if($match.Count -gt 0)
        {
            $AbsentToBeRemoved += [PSCustomObject]@{
                Rule = $refrenceObject
            }
        }
    }

    foreach($refrenceObject in $Actual)
    {
        $match = @($Expected.Rules).Where({
            (((($_.FileSystemRights.value__ -band $refrenceObject.FileSystemRights.value__) -match "$($_.FileSystemRights.value__)|$($refrenceObject.FileSystemRights.value__)") -and !$Force) -or ($_.FileSystemRights -eq $refrenceObject.FileSystemRights -and $Force)) -and
            $_.InheritanceFlags -eq $refrenceObject.InheritanceFlags -and
            $_.PropagationFlags -eq $refrenceObject.PropagationFlags -and
            $_.AccessControlType -eq $refrenceObject.AccessControlType -and
            $_.IdentityReference -eq $refrenceObject.IdentityReference
        })
        if($match.Count -eq 0)
        {
            $ToBeRemoved += [PSCustomObject]@{
                Rule = $refrenceObject
            }
        }
    }

    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)]
        [string]
        $Path
    )

    $returnPath = $Path

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