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 = @{ Ensure = 'Absent' 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 $result.Ensure = 'Present' } 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 # Set works without BUILTIN\, but Get fails (silently) without this logic. # This is tested by the 'Users' group in the Functional # Integration test logic, which is actually BUILTIN\USERS per ACLs, however # this is not obvious to users and results in unexpected functionality # such as successful SETs, but TEST's that fail every time, so this regex # workaround for the common windows identifier prefixes makes behavior consistent. # Local groups are fully qualified with $env:ComputerName\. $regexEscapedIdentity = [RegEx]::Escape($Identity) $escapedComputerName = [RegEx]::Escape($ENV:ComputerName) $regex = "^(NT AUTHORITY|BUILTIN|NT SERVICES|$escapedComputerName)\\$regexEscapedIdentity" $matchingRules = $accessRules | Where-Object -FilterScript { $_.IdentityReference -eq $Identity -or $_.IdentityReference -match $regex } if ( $matchingRules ) { $result.Ensure = 'Present' $result.Rights = @( ( $matchingRules.FileSystemRights -split ', ' ) | Select-Object -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 ( $currentValues.Rights -and (-not $Rights) ) { $result = $false Write-Verbose -Message ( 'Returning false. The identity "{0}" has the rights "{1}" when expected no rights by the Ensure Absent.' -f $Identity,( $currentValues.Rights -join ', ' ) ) } elseif ( -not $currentValues.Rights ) { $result = $true Write-Verbose -Message ( 'Returning true. The identity "{0}" has no rights as expected by the Ensure Absent.' -f $Identity) } elseif ( $Rights ) # always hit, but just clarifying what the actual case is by filling in the if block { foreach ($right in $Rights) { $notAllowed = [System.Security.AccessControl.FileSystemRights]$right # If any rights that we want to deny are individually a full subset of existing rights... $currentRightResult = -not ($notAllowed -eq ( $notAllowed -band ([System.Security.AccessControl.FileSystemRights] $currentValues.Rights ) ) ) if (-not $currentRightResult) { Write-Verbose -Message ( 'Testing right {0} absence: false. The identity "{1}" has the rights "{2}" which include "{0}", which are included in the desired Absent rights "{3}".' -f $notAllowed, $Identity,( $currentValues.Rights -join ', ' ), ($Rights -join ', ') ) } else { Write-Verbose -Message ( 'Testing right {0} absence: true. The identity "{1}" has the rights "{2}" which do not contain "{0}".' -f $notAllowed, $Identity,( $currentValues.Rights -join ', ' ) ) } $result = $result -and $currentRightResult } Write-Verbose -Message ( 'Returning {0}.' -f $result ) } } 'Present' { # Validate the rights parameter was passed if ( -not $PSBoundParameters.ContainsKey('Rights') ) { throw "No rights were specified for '$Identity' on '$Path'" } # This isn't always the same as the input if parts of the input are subset permissions, so pre-cast it. # For example [System.Security.AccessControl.FileSystemRights]@('Modify', 'Read', 'Write') is actually just 'Modify' within the flagged enum, so test as such to avoid false test failures. $expected = [System.Security.AccessControl.FileSystemRights]$Rights $result = $false if ($currentValues.Rights) { # At minimum the AND result of the current and expected rights should be the expected rights (allow extra rights, but not missing). # Otherwise permission flags are missing from the enum. $result = $expected -eq ($expected -band ([System.Security.AccessControl.FileSystemRights] $currentValues.Rights)) Write-Verbose -Message ( 'Returning {0}. The identity "{1}" has the rights "{2}". The expected rights are "{3}" (combined from input Rights "{4}").' -f $result, $Identity,( $currentValues.Rights -join ', ' ), $expected,( $Rights -join ', ' ) ) } } } 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') } |