DSCResources/MSFT_xFileSystemAccessRule/MSFT_xFileSystemAccessRule.psm1

<#
    .SYNOPSIS
        Gets the rights of the specified filesystem object for the specified identity.
 
    .PARAMETER Path
        The path to the item that should have permissions set.
 
    .PARAMETER Identity
        The identity to set permissions for.
#>

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

        [Parameter(Mandatory = $true)]
        [String]
        $Identity
    )

    $result = @{
        Path = $Path
        Identity = $Identity
        Rights = [System.String[]] @()
        IsActiveNode = $true
    }
    
    if ( -not ( Test-Path -Path $Path ) )
    {
        $isClusterResource = $false

        # Is the node a member of a WSFC?
        $msCluster = Get-CimInstance -Namespace root/MSCluster -ClassName MSCluster_Cluster -ErrorAction SilentlyContinue

        if ( $msCluster )
        {
            Write-Verbose -Message "$($env:COMPUTERNAME) is a member of the Windows Server Failover Cluster '$($msCluster.Name)'"
            
            # Is the defined path built off of a known mount point in the cluster?
            $clusterPartition = Get-CimInstance -Namespace root/MSCluster -ClassName MSCluster_ClusterDiskPartition |
                Where-Object -FilterScript {
                    $currentPartition = $_

                    $currentPartition.MountPoints | ForEach-Object -Process {
                        [regex]::Escape($Path) -match "^$($_)"
                    }
                }

            # Get the possible owner nodes for the partition
            [array]$possibleOwners = $clusterPartition |
                Get-CimAssociatedInstance -ResultClassName 'MSCluster_Resource' |
                    Get-CimAssociatedInstance -Association 'MSCluster_ResourceToPossibleOwner' | 
                        Select-Object -ExpandProperty Name -Unique
            
            # Ensure the current node is a possible owner of the drive
            if ( $possibleOwners -contains $env:COMPUTERNAME )
            {
                $isClusterResource = $true                
                $result.IsActiveNode = $false
            }
            else
            {
                Write-Verbose -Message "'$($env:COMPUTERNAME)' is not a possible owner for '$Path'."
            }
        }

        if ( -not $isClusterResource )
        {
            throw "Unable to get ACL for '$Path' because it does not exist"
        }
    }
    else
    {
        $acl = Get-Acl -Path $Path
        $accessRules = $acl.Access

        $result.Rights = [System.String[]] @(
            $accessRules |
                Where-Object -FilterScript { $_.IdentityReference -eq $Identity } |
                Select-Object -ExpandProperty FileSystemRights -Unique
        )
    }
    return $result
}

<#
    .SYNOPSIS
        Sets the rights of the specified filesystem object for the specified identity.
 
    .PARAMETER Path
        The path to the item that should have permissions set.
 
    .PARAMETER Identity
        The identity to set permissions for.
     
    .PARAMETER Rights
        The permissions to include in this rule. Optional if Ensure is set to value 'Absent'.
 
    .PARAMETER Ensure
        Present to create the rule, Absent to remove an existing rule. Default value is 'Present'.
 
    .PARAMETER ProcessOnlyOnActiveNode
        Specifies that the resource will only determine if a change is needed if the target node is the active host of the filesystem object. The user the configuration is run as must have permission to the Windows Server Failover Cluster.
        Not used in Set-TargetResource.
#>

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

        [Parameter(Mandatory = $true)]
        [String]
        $Identity,

        [Parameter()]
        [ValidateSet(
            'ListDirectory',
            'ReadData',
            'WriteData',
            'CreateFiles',
            'CreateDirectories',
            'AppendData',
            'ReadExtendedAttributes',
            'WriteExtendedAttributes',
            'Traverse',
            'ExecuteFile',
            'DeleteSubdirectoriesAndFiles',
            'ReadAttributes',
            'WriteAttributes',
            'Write',
            'Delete',
            'ReadPermissions',
            'Read',
            'ReadAndExecute',
            'Modify',
            'ChangePermissions',
            'TakeOwnership',
            'Synchronize',
            'FullControl'
        )]
        [String[]]
        $Rights,

        [Parameter()]
        [ValidateSet('Present','Absent')]
        [String]
        $Ensure = 'Present',

        [Parameter()]
        [Boolean]
        $ProcessOnlyOnActiveNode
    )

    if ( -not ( Test-Path -Path $Path ) )
    {
        throw ( "The path '$Path' does not exist." )
    }

    $acl = Get-ACLAccess -Path $Path
    $accessRules = $acl.Access

    if ( $Ensure -eq 'Present' )
    {
        # Validate the rights parameter was passed
        if ( -not $PSBoundParameters.ContainsKey('Rights') )
        {
            throw "No rights were specified for '$Identity' on '$Path'"
        }
        
        Write-Verbose -Message "Setting access rules for '$Identity' on '$Path'"

        $newFileSystemAccessRuleParameters = @{
            TypeName = 'System.Security.AccessControl.FileSystemAccessRule'
            ArgumentList = @(
                $Identity, 
                [System.Security.AccessControl.FileSystemRights]$Rights, 
                'ContainerInherit,ObjectInherit', 
                'None', 
                'Allow'
            )
        }

        $ar = New-Object @newFileSystemAccessRuleParameters
        $acl.SetAccessRule($ar)

        Set-Acl -Path $Path -AclObject $acl
    }

    if ($Ensure -eq 'Absent')
    {
        $identityRule = $accessRules | Where-Object -FilterScript {
            $_.IdentityReference -eq $Identity
        } | Select-Object -First 1

        if ( $null -ne $identityRule )
        {
            Write-Verbose -Message "Removing access rules for '$Identity' on '$Path'"
            $acl.RemoveAccessRule($identityRule) | Out-Null
            Set-Acl -Path $Path -AclObject $acl
        }
    }
}

<#
    .SYNOPSIS
        Tests the rights of the specified filesystem object for the specified identity.
 
    .PARAMETER Path
        The path to the item that should have permissions set.
 
    .PARAMETER Identity
        The identity to set permissions for.
     
    .PARAMETER Rights
        The permissions to include in this rule. Optional if Ensure is set to value 'Absent'.
 
    .PARAMETER Ensure
        Present to create the rule, Absent to remove an existing rule. Default value is 'Present'.
 
    .PARAMETER ProcessOnlyOnActiveNode
        Specifies that the resource will only determine if a change is needed if the target node is the active host of the filesystem object. The user the configuration is run as must have permission to the Windows Server Failover Cluster.
#>

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

        [Parameter(Mandatory = $true)]
        [String]
        $Identity,

        [Parameter()]
        [ValidateSet(
            'ListDirectory',
            'ReadData',
            'WriteData',
            'CreateFiles',
            'CreateDirectories',
            'AppendData',
            'ReadExtendedAttributes',
            'WriteExtendedAttributes',
            'Traverse',
            'ExecuteFile',
            'DeleteSubdirectoriesAndFiles',
            'ReadAttributes',
            'WriteAttributes',
            'Write',
            'Delete',
            'ReadPermissions',
            'Read',
            'ReadAndExecute',
            'Modify',
            'ChangePermissions',
            'TakeOwnership',
            'Synchronize',
            'FullControl'
        )]
        [String[]]
        $Rights,

        [Parameter()]
        [ValidateSet('Present','Absent')]
        [String]
        $Ensure = 'Present',
        
        [Parameter()]
        [Boolean]
        $ProcessOnlyOnActiveNode
    )

    $result = $true

    $getTargetResourceParameters = @{
        Path = $Path
        Identity = $Identity
    }
    
    $currentValues = Get-TargetResource @getTargetResourceParameters

    <#
        If this is supposed to process on the active node, and this is not the
        active node, don't bother evaluating the test.
    #>

    if ( $ProcessOnlyOnActiveNode -and -not $currentValues.IsActiveNode )
    {
        Write-Verbose -Message ( 'The node "{0}" is not actively hosting the path "{1}". Exiting the test.' -f $env:COMPUTERNAME,$Path )
        return $result
    }

    switch ( $Ensure )
    {
        'Absent'
        {
            # If no rights were passed
            if ( -not $PSBoundParameters.ContainsKey('Rights') )
            {
                # Set rights to an empty array
                $Rights = @()
            }
            
            # If the right is defined and currently set, return it
            $comparisonResult = Compare-Object -ReferenceObject $Rights -DifferenceObject $currentValues.Rights -ExcludeDifferent -IncludeEqual |
                Select-Object -ExpandProperty InputObject
        }
        
        'Present'
        {
            # Validate the rights parameter was passed
            if ( -not $PSBoundParameters.ContainsKey('Rights') )
            {
                throw "No rights were specified for '$Identity' on '$Path'"
            }
            
            # If the right is defined and missing, return it
            $comparisonResult = Compare-Object -ReferenceObject $Rights -DifferenceObject $currentValues.Rights |
                Where-Object -FilterScript { $_.SideIndicator -eq '<=' } |
                Select-Object -ExpandProperty InputObject
        }
    }

    # If results were found from the comparison
    if ( $comparisonResult.Count -gt 0 )
    {
        Write-Verbose -Message ( 'The identity "{0}" has the rights "{1}". The expected rights are "{2}".' -f $Identity,( $currentValues.Rights -join ', ' ),( $Rights -join ', ' ) )
        $result = $false
    }

    return $result
}

<#
    .SYNOPSIS
        Retrieves the access control list from a filesystem object.
 
    .PARAMETER Path
        The path of the filesystem object to retrieve the ACL from.
#>

function Get-AclAccess
{
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $Path
    )
    
    return (Get-Item -Path $Path).GetAccessControl('Access')
}