DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.psm1

$resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$modulesFolderPath = Join-Path -Path $resourceModulePath -ChildPath 'Modules'

$aDCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common'
Import-Module -Name $aDCommonModulePath

$dscResourceCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'DscResource.Common'
Import-Module -Name $dscResourceCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

<#
    .SYNOPSIS
        Get the current state of the object permission entry.
 
    .PARAMETER Path
        Active Directory path of the target object to add or remove the
        permission entry, specified as a Distinguished Name.
 
    .PARAMETER IdentityReference
        Indicates the identity of the principal for the permission entry.
 
    .PARAMETER AccessControlType
        Indicates whether to Allow or Deny access to the target object.
 
    .PARAMETER ObjectType
        The schema GUID or display name of the object to which the access rule
        applies.
 
    .PARAMETER ActiveDirectorySecurityInheritance
        One of the 'ActiveDirectorySecurityInheritance' enumeration values that
        specifies the inheritance type of the access rule.
 
    .PARAMETER InheritedObjectType
        The schema GUID or display name of the child object type that can
        inherit this access rule.
#>

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

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [System.String]
        $AccessControlType,

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')]
        [System.String]
        $ActiveDirectorySecurityInheritance,

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

    if (-not (Test-IsGuid -InputString $ObjectType))
    {
        $ObjectType = Get-ADSchemaGuid -DisplayName $ObjectType
    }

    if (-not (Test-IsGuid -InputString $InheritedObjectType))
    {
        $InheritedObjectType = Get-ADSchemaGuid -DisplayName $InheritedObjectType
    }

    $ADDrivePSPath = Get-ADDrivePSPath

    # Return object, by default representing an absent ace
    $returnValue = @{
        Ensure                             = 'Absent'
        Path                               = $Path
        IdentityReference                  = $IdentityReference
        ActiveDirectoryRights              = [System.String[]] @()
        AccessControlType                  = $AccessControlType
        ObjectType                         = $ObjectType
        ActiveDirectorySecurityInheritance = $ActiveDirectorySecurityInheritance
        InheritedObjectType                = $InheritedObjectType
    }

    try
    {
        # Get the current acl
        $acl = Get-Acl -Path ($ADDrivePSPath + $Path) -ErrorAction Stop
    }
    catch [System.Management.Automation.ItemNotFoundException]
    {
        Write-Verbose -Message ($script:localizedData.ObjectPathIsAbsent -f $Path)
        $acl = $null
    }
    catch
    {
        throw $_
    }

    if ($null -ne $acl)
    {
        foreach ($access in $acl.Access)
        {
            if ($access.IsInherited -eq $false)
            {
                <#
                    Check if the ace does match the parameters. If yes, the target
                    ace has been found, return present with the assigned rights.
                #>

                if ($access.IdentityReference.Value -eq $IdentityReference -and
                    $access.AccessControlType -eq $AccessControlType -and
                    $access.ObjectType.Guid -eq $ObjectType -and
                    $access.InheritanceType -eq $ActiveDirectorySecurityInheritance -and
                    $access.InheritedObjectType.Guid -eq $InheritedObjectType)
                {
                    $returnValue['Ensure'] = 'Present'
                    $returnValue['ActiveDirectoryRights'] = [System.String[]] $access.ActiveDirectoryRights.ToString().Split(',').ForEach( { $_.Trim() })
                }
            }
        }
    }

    if ($returnValue.Ensure -eq 'Present')
    {
        Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryFound -f $Path)
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryNotFound -f $Path)
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Add or remove the object permission entry.
 
    .PARAMETER Ensure
        Indicates if the access will be added (Present) or will be removed
        (Absent). Default is 'Present'.
 
    .PARAMETER Path
        Active Directory path of the target object to add or remove the
        permission entry, specified as a Distinguished Name.
 
    .PARAMETER IdentityReference
        Indicates the identity of the principal for the permission entry.
 
    .PARAMETER ActiveDirectoryRights
        A combination of one or more of the ActiveDirectoryRights enumeration
        values that specifies the rights of the access rule. Default is
        'GenericAll'.
 
    .PARAMETER AccessControlType
        Indicates whether to Allow or Deny access to the target object.
 
    .PARAMETER ObjectType
        The schema GUID or display name of the object to which the access rule
        applies.
 
    .PARAMETER ActiveDirectorySecurityInheritance
        One of the 'ActiveDirectorySecurityInheritance' enumeration values that
        specifies the inheritance type of the access rule.
 
    .PARAMETER InheritedObjectType
        The schema GUID or display name of the child object type that can
        inherit this access rule.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

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

        [Parameter()]
        [ValidateSet('AccessSystemSecurity', 'CreateChild', 'Delete', 'DeleteChild', 'DeleteTree', 'ExtendedRight', 'GenericAll', 'GenericExecute', 'GenericRead', 'GenericWrite', 'ListChildren', 'ListObject', 'ReadControl', 'ReadProperty', 'Self', 'Synchronize', 'WriteDacl', 'WriteOwner', 'WriteProperty')]
        [System.String[]]
        $ActiveDirectoryRights = 'GenericAll',

        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [System.String]
        $AccessControlType,

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')]
        [System.String]
        $ActiveDirectorySecurityInheritance,

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

    if (-not (Test-IsGuid -InputString $ObjectType))
    {
        $ObjectType = Get-ADSchemaGuid -DisplayName $ObjectType
    }

    if (-not (Test-IsGuid -InputString $InheritedObjectType))
    {
        $InheritedObjectType = Get-ADSchemaGuid -DisplayName $InheritedObjectType
    }

    $ADDrivePSPath = Get-ADDrivePSPath

    # Get the current acl
    $acl = Get-Acl -Path ($ADDrivePSPath + $Path)

    if ($Ensure -eq 'Present')
    {
        Write-Verbose -Message ($script:localizedData.AddingObjectPermissionEntry -f $Path)

        $ntAccount = New-Object -TypeName 'System.Security.Principal.NTAccount' -ArgumentList $IdentityReference

        $ace = New-Object -TypeName 'System.DirectoryServices.ActiveDirectoryAccessRule' -ArgumentList @(
            $ntAccount,
            $ActiveDirectoryRights,
            $AccessControlType,
            $ObjectType,
            $ActiveDirectorySecurityInheritance,
            $InheritedObjectType
        )

        $acl.AddAccessRule($ace)
    }
    else
    {
        <#
            Iterate through all ace entries to find the desired ace, which
            should be absent. If found, remove the ace from the acl.
        #>

        foreach ($access in $acl.Access)
        {
            if ($access.IsInherited -eq $false)
            {
                if ($access.IdentityReference.Value -eq $IdentityReference -and
                    $access.AccessControlType -eq $AccessControlType -and
                    $access.ObjectType.Guid -eq $ObjectType -and
                    $access.InheritanceType -eq $ActiveDirectorySecurityInheritance -and
                    $access.InheritedObjectType.Guid -eq $InheritedObjectType)
                {
                    Write-Verbose -Message ($script:localizedData.RemovingObjectPermissionEntry -f $Path)

                    $acl.RemoveAccessRule($access)
                }
            }
        }
    }

    # Set the updated acl to the object
    $acl |
        Set-Acl -Path ($ADDrivePSPath + $Path)
}

<#
    .SYNOPSIS
        Test the object permission entry.
 
    .PARAMETER Ensure
        Indicates if the access will be added (Present) or will be removed
        (Absent). Default is 'Present'.
 
    .PARAMETER Path
        Active Directory path of the target object to add or remove the
        permission entry, specified as a Distinguished Name.
 
    .PARAMETER IdentityReference
        Indicates the identity of the principal for the permission entry.
 
    .PARAMETER ActiveDirectoryRights
        A combination of one or more of the ActiveDirectoryRights enumeration
        values that specifies the rights of the access rule. Default is
        'GenericAll'.
 
    .PARAMETER AccessControlType
        Indicates whether to Allow or Deny access to the target object.
 
    .PARAMETER ObjectType
        The schema GUID or display name of the object to which the access rule
        applies.
 
    .PARAMETER ActiveDirectorySecurityInheritance
        One of the 'ActiveDirectorySecurityInheritance' enumeration values that
        specifies the inheritance type of the access rule.
 
    .PARAMETER InheritedObjectType
        The schema GUID or display name of the child object type that can
        inherit this access rule.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

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

        [Parameter()]
        [ValidateSet('AccessSystemSecurity', 'CreateChild', 'Delete', 'DeleteChild', 'DeleteTree', 'ExtendedRight', 'GenericAll', 'GenericExecute', 'GenericRead', 'GenericWrite', 'ListChildren', 'ListObject', 'ReadControl', 'ReadProperty', 'Self', 'Synchronize', 'WriteDacl', 'WriteOwner', 'WriteProperty')]
        [System.String[]]
        $ActiveDirectoryRights = 'GenericAll',

        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [System.String]
        $AccessControlType,

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')]
        [System.String]
        $ActiveDirectorySecurityInheritance,

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

    # Get the current state
    $getTargetResourceSplat = @{
        Path                               = $Path
        IdentityReference                  = $IdentityReference
        AccessControlType                  = $AccessControlType
        ObjectType                         = $ObjectType
        ActiveDirectorySecurityInheritance = $ActiveDirectorySecurityInheritance
        InheritedObjectType                = $InheritedObjectType
    }
    $currentState = Get-TargetResource @getTargetResourceSplat

    # Always check, if the ensure state is desired
    $returnValue = $currentState.Ensure -eq $Ensure

    # Only check the Active Directory rights, if ensure is set to present
    if ($Ensure -eq 'Present')
    {
        # Convert to array to a string for easy compare
        [System.String] $currentActiveDirectoryRights = ($currentState.ActiveDirectoryRights |
                Sort-Object) -join ', '

        [System.String] $desiredActiveDirectoryRights = ($ActiveDirectoryRights |
                Sort-Object) -join ', '

        $returnValue = $returnValue -and $currentActiveDirectoryRights -eq $desiredActiveDirectoryRights
    }

    if ($returnValue)
    {
        Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryInDesiredState -f $Path)
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryNotInDesiredState -f $Path)
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Returns this computers's full PSPath for the AD Drive.
 
    .DESCRIPTION
        This is used to retrieve the full PSPath for the AD Drive, which varies between operating systems.
 
#>

function Get-ADDrivePSPath
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param ()

    # Need to use the full PSPath to avoid issues when escaping paths - https://github.com/dsccommunity/ActiveDirectoryDsc/issues/675
    # The full PSPath varies between operating systems, so we obtain it dynamically - https://github.com/dsccommunity/ActiveDirectoryDsc/issues/724

    Assert-ADPSDrive

    $adDrivePSPath = (Get-Item -Path 'AD:').PSPath
    Write-Verbose -Message ($script:localizedData.RetrievedADDrivePSPath -f $adDrivePSPath)
    return $adDrivePSPath
}

<#
    .SYNOPSIS
        Retrieves the schemaIDGUID or rightsGUID of an Active Directory object based on its display name.
 
    .DESCRIPTION
        This function searches the Active Directory schema for an object with the matching lDAPDisplayName,
        or the Extended Rights container for an object with the matching displayName.
 
    .PARAMETER DisplayName
        The lDAPDisplayName (for schema objects) or displayName (for extended rights) to search for.
 
    .OUTPUTS
        System.String
 
        If a matching entry is found, the corresponding GUID (schemaIDGUID or rightsGUID) is returned.
 
    .EXAMPLE
        PS C:\> Get-ADSchemaGuid -DisplayName "user"
 
        Returns the schemaIDGUID of the schema object with lDAPDisplayName "user".
 
    .EXAMPLE
        PS C:\> Get-ADSchemaGuid -DisplayName "Send As"
 
        Returns the rightsGUID of the Extended Rights object with displayName "Send As".
#>

function Get-ADSchemaGuid
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DisplayName
    )

    try
    {
        $rootDse = Get-ADRootDSE -ErrorAction Stop
    }
    catch
    {
        throw ($script:localizedData.FailedToRetrieveRootDSE -f $_)
    }

    $escapedDisplayName = Get-EscapedLdapFilterValue -Value $DisplayName

    # Search the schema for a matching lDAPDisplayName
    try
    {
        $schemaResults = @(Get-ADObject `
            -SearchBase $rootDse.schemaNamingContext `
            -LDAPFilter "(&(schemaIDGUID=*)(lDAPDisplayName=$escapedDisplayName))" `
            -Properties 'lDAPDisplayName','schemaIDGUID' `
            -ErrorAction Stop)
    }
    catch
    {
        throw ($script:localizedData.ErrorSearchingSchema -f $DisplayName, $_)
    }

    if ($schemaResults.Count -gt 1)
    {
        throw ($script:localizedData.ErrorMultipleSchemaObjectsFound -f $DisplayName)
    }
    elseif ($schemaResults.Count -eq 1)
    {
        return ([System.Guid]$schemaResults[0].schemaIDGUID).Guid
    }

    # If not found in the schema: search the Extended Rights container
    try
    {
        $rightsResults = @(Get-ADObject `
            -SearchBase "CN=Extended-Rights,$($rootDse.configurationNamingContext)" `
            -LDAPFilter "(&(objectClass=controlAccessRight)(displayName=$escapedDisplayName))" `
            -Properties 'displayName','rightsGUID' `
            -ErrorAction Stop)
    }
    catch
    {
        throw ($script:localizedData.ErrorSearchingExtendedRights -f $DisplayName, $_)
    }

    if ($rightsResults.Count -gt 1)
    {
        throw ($script:localizedData.ErrorMultipleExtendedRightsFound -f $DisplayName)
    }
    elseif ($rightsResults.Count -eq 1)
    {
        return ([System.Guid]$rightsResults[0].rightsGUID).Guid
    }

    throw ($script:localizedData.NoMatchingGuidFound -f $DisplayName)
}

<#
    .SYNOPSIS
        Checks whether a string is a valid GUID.
 
    .DESCRIPTION
        The 'Test-IsGuid' function uses the .NET method [System.Guid]::TryParse() to
        determine whether the provided string is a valid GUID (Globally Unique Identifier).
 
    .PARAMETER InputString
        The string to be tested for a valid GUID format.
 
    .OUTPUTS
        System.Boolean
 
        Returns $true if the string is a valid GUID, otherwise returns $false.
 
    .EXAMPLE
        Test-IsGuid "550e8400-e29b-41d4-a716-446655440000"
 
        Returns 'True' because the string is a valid GUID.
 
    .EXAMPLE
        Test-IsGuid "abc"
 
        Returns 'False' because the string is not a valid GUID.
#>

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

    $nullGuid = [System.Guid]::Empty
    return [System.Guid]::TryParse($InputString, [ref]$nullGuid)
}

<#
    .SYNOPSIS
        Escapes a string for safe use in an LDAP filter according to RFC 4515.
 
    .DESCRIPTION
        This function replaces special characters in the input string with their corresponding
        escape sequences for use in LDAP filters (e.g., in Active Directory queries). It prevents
        syntax errors or unexpected behavior when constructing LDAP search filters.
 
        The following characters are escaped:
        \ => \5c
        * => \2a
        ( => \28
        ) => \29
        NULL byte (ASCII 0) => \00
 
    .PARAMETER Value
        The input string to be escaped, such as a username or part of an LDAP search filter.
 
    .EXAMPLE
        PS> Get-EscapedLdapFilterValue -Value 'Smith (Admin)*'
        Smith \28Admin\29\2a
 
    .EXAMPLE
        PS> $filter = "(cn=$(Get-EscapedLdapFilterValue -Value 'Admin*'))"
        PS> $filter
        (cn=Admin\2a)
#>

function Get-EscapedLdapFilterValue
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Value
    )

    $escaped = $Value -replace '\\', '\5c'
    $escaped = $escaped -replace '\*', '\2a'
    $escaped = $escaped -replace '\(', '\28'
    $escaped = $escaped -replace '\)', '\29'
    $escaped = $escaped -replace "`0", '\00'

    return $escaped
}