DSCResources/DSC_FileSystemAccessRule/DSC_FileSystemAccessRule.psm1
$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' Import-Module -Name $script:resourceHelperModulePath $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' <# .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)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.String] $Identity ) Write-Verbose -Message ( $script:localizedData.GetCurrentState -f $Identity, $Path ) $result = @{ Ensure = 'Absent' Path = $Path Identity = $Identity Rights = [System.String[]] @() IsActiveNode = $true } if (-not (Test-Path -Path $Path)) { Write-Verbose -Message $script:localizedData.EvaluatingIfCluster $failoverClusterInstance = Get-CimInstance -Namespace 'root/MSCluster' -ClassName 'MSCluster_Cluster' -ErrorAction 'SilentlyContinue' if ($failoverClusterInstance) { Write-Verbose -Message ( $script:localizedData.NodeIsClusterMember -f $env:COMPUTERNAME, $failoverClusterInstance.Name ) $clusterPartition = Get-CimInstance -Namespace 'root/MSCluster' -ClassName 'MSCluster_ClusterDiskPartition' | Where-Object -FilterScript { $currentPartition = $_ # The property MountPoints is an array of mount points, e.g. @('D:', 'E:'). $currentPartition.MountPoints | ForEach-Object -Process { [regex]::Escape($Path) -match ('^{0}' -f $_) } } if ($clusterPartition) { Write-Verbose -Message ( $script:localizedData.EvaluatingOwnerOfClusterDiskPartition -f @( (Split-Path -Path $Path -Qualifier) $env:COMPUTERNAME ) ) # Get the possible owner nodes for the partition. [System.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 -and $possibleOwners -contains $env:COMPUTERNAME) { Write-Verbose -Message ( $script:localizedData.PossibleClusterResourceOwner -f @( $env:COMPUTERNAME $Path ) ) $result.IsActiveNode = $false $result.Ensure = 'Present' } else { Write-Verbose -Message ( $script:localizedData.NotPossibleClusterResourceOwner -f @( $env:COMPUTERNAME $Path ) ) } } else { Write-Verbose -Message ( $script:localizedData.NoClusterDiskPartitionFound -f $Path ) } } else { Write-Verbose -Message $script:localizedData.NodeIsNotClusterMember } <# Evaluates if the path was not found and the node is the active node. The node is always assumed to be the active node unless the node is a possible member of a cluster disk partition the path belong to. If the node could be a possible member but currently was not the active node then the property IsActiveNode was set to $false. #> if ($result.IsActiveNode) { Write-Warning -Message ( $script:localizedData.PathDoesNotExist -f $Path ) } } else { Write-Verbose -Message ( $script:localizedData.PathExist -f $Identity ) $acl = Get-ACLAccess -Path $Path $accessRules = $acl.Access <# Set-TargetResource works without BUILTIN\, but Get-TargetResource fails (silently) without this logic. This is regression tested by the 'Users' group in the test logic, which is actually BUILTIN\USERS per ACLs, however this is not obvious to users and results in unexpected functionality such as successfully running Set-TargetResource, but result in Test-TargetResource that fail every time. This regex workaround for the common windows identifier prefixes makes behavior consistent. Local groups are fully qualified with "$env:COMPUTERNAME\". #> $regexEscapedIdentity = [System.Text.RegularExpressions.Regex]::Escape($Identity) $escapedComputerName = [System.Text.RegularExpressions.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. .NOTES This function uses Set-Acl that was first introduced in Windows Powershell 5.1. #> function Set-TargetResource { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.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' )] [System.String[]] $Rights, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.Boolean] $ProcessOnlyOnActiveNode ) if (-not (Test-Path -Path $Path)) { $errorMessage = $script:localizedData.PathDoesNotExist -f $Path New-ObjectNotFoundException -Message $errorMessage } $acl = Get-ACLAccess -Path $Path if ($Ensure -eq 'Present') { # Validate the rights parameter was passed. if (-not $PSBoundParameters.ContainsKey('Rights')) { $errorMessage = $script:localizedData.NoRightsWereSpecified -f $Identity, $Path New-InvalidArgumentException -ArgumentName 'Rights' -Message $errorMessage } Write-Verbose -Message ( $script:localizedData.SetAllowAccessRule -f ($Rights -join ', '), $Identity, $Path ) $newFileSystemAccessRuleParameters = @{ TypeName = 'System.Security.AccessControl.FileSystemAccessRule' ArgumentList = @( $Identity, [System.Security.AccessControl.FileSystemRights] $Rights, 'ContainerInherit,ObjectInherit', 'None', 'Allow' ) } $fileSystemAccessRule = New-Object @newFileSystemAccessRuleParameters $acl.SetAccessRule($fileSystemAccessRule) } if ($Ensure -eq 'Absent') { # If no rights were passed. if (-not $PSBoundParameters.ContainsKey('Rights')) { # Set rights to an empty array. $Rights = @() } <# If no specific rights was provided then purge all rights for the identity, otherwise remove just the specific rights. #> if ($Rights.Count -eq 0) { $identityRules = $acl.Access | Where-Object -FilterScript { $_.IdentityReference -eq $Identity } $identityRule = $identityRules | Select-Object -First 1 if ($identityRule) { Write-Verbose -Message ( $script:localizedData.RemoveAllAllowAccessRules -f $Identity, $Path ) $acl.PurgeAccessRules($identityRule.IdentityReference) } } else { foreach ($right in $Rights) { Write-Verbose -Message ( $script:localizedData.RemoveAllowAccessRule -f $right, $Identity, $Path ) $removeFileSystemAccessRuleParameters = @{ TypeName = 'System.Security.AccessControl.FileSystemAccessRule' ArgumentList = @( $Identity, [System.Security.AccessControl.FileSystemRights] $right, 'ContainerInherit,ObjectInherit', 'None', 'Allow' ) } $fileSystemAccessRule = New-Object @removeFileSystemAccessRuleParameters $null = $acl.RemoveAccessRule($fileSystemAccessRule) } } } try { Set-Acl -Path $Path -AclObject $acl } catch { $errorMessage = $script:localizedData.FailedToSetAccessRules -f $Path New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } <# .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)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.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' )] [System.String[]] $Rights, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.Boolean] $ProcessOnlyOnActiveNode ) $result = $true $getTargetResourceParameters = @{ Path = $Path Identity = $Identity } $currentState = 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 $currentState.IsActiveNode) { Write-Verbose -Message ( $script:localizedData.IsNotActiveNode -f $env:COMPUTERNAME, $Path ) return $result } Write-Verbose -Message ( $script:localizedData.EvaluatingRights -f $Identity ) switch ($Ensure) { 'Absent' { # If no rights were passed. if (-not $PSBoundParameters.ContainsKey('Rights')) { # Set rights to an empty array. $Rights = @() } if ($currentState.Rights -and (-not $Rights)) { $result = $false Write-Verbose -Message ( $script:localizedData.AbsentRightsNotInDesiredState -f $Identity, ($currentState.Rights -join ', ') ) } elseif (-not $currentState.Rights) { $result = $true Write-Verbose -Message ( $script:localizedData.InDesiredState -f $Identity ) } # Always hit, but just clarifying what the actual case is by filling in the if block. elseif ($Rights) { Write-Verbose -Message ( $script:localizedData.EvaluatingIndividualRight -f $Identity, ($currentState.Rights -join ', '), ($Rights -join ', ') ) foreach ($right in $Rights) { $rightNotAllowed = [System.Security.AccessControl.FileSystemRights] $right $currentRights = [System.Security.AccessControl.FileSystemRights] $currentState.Rights # If any rights that we want to deny are individually a full subset of existing rights. $currentRightResult = -not ($rightNotAllowed -eq ($rightNotAllowed -band $currentRights)) if (-not $currentRightResult) { Write-Verbose -Message ( $script:localizedData.IndividualRightNotInDesiredState -f $Identity, $rightNotAllowed ) } else { Write-Verbose -Message ( $script:localizedData.IndividualRightInDesiredState -f $rightNotAllowed ) } $result = $result -and $currentRightResult } } } 'Present' { # Validate the rights parameter was passed. if (-not $PSBoundParameters.ContainsKey('Rights')) { $errorMessage = $script:localizedData.NoRightsWereSpecified -f $Identity, $Path New-InvalidArgumentException -ArgumentName 'Rights' -Message $errorMessage } <# 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 ($currentState.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] $currentState.Rights)) } if ($result) { Write-Verbose -Message ( $script:localizedData.InDesiredState -f $Identity ) } else { Write-Verbose -Message ( $script:localizedData.NotInDesiredState -f @( $Identity, ($currentState.Rights -join ', '), $expected, ($Rights -join ', ' ) ) ) } } } return $result } <# .SYNOPSIS This function is wrapper for getting the DACL for the specified path. .PARAMETER Path The path to the item that should have permissions set. .NOTES "Well the limited features of Get-ACL means that you always read the full security descriptor including the owner whether you intended to or not. That means that when you come to write to the object based on a modified version of what you read, you are attempting to write back to the owner attribute. The GetAccessControl('Access') method reads only the DACL so when you write it back you are not trying to write something you did not intend to." https://www.mickputley.net/2015/11/set-acl-security-identifier-is-not.html https://github.com/dsccommunity/FileSystemDsc/issues/3 #> function Get-ACLAccess { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path ) return (Get-Item -Path $Path).GetAccessControl('Access') } |