DSCResources/MSFT_ADFineGrainedPasswordPolicy/MSFT_ADFineGrainedPasswordPolicy.psm1

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

$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common'
Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1')

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

<#
    .SYNOPSIS
        Returns the current state of an Active Directory fine-grained password
        policy.

    .PARAMETER Name
        Specifies an Active Directory fine-grained password policy object name.

    .PARAMETER Precedence
        Specifies a value that defines the precedence of a fine-grained password policy among all fine-grained password
        policies.

    .PARAMETER DomainController
        Specifies the Active Directory Domain Services instance to connect to.

    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.

    .NOTES
        Used Functions:
            Name | Module
            ---------------------------------------|--------------------------
            Get-ADFineGrainedPasswordPolicy | ActiveDirectory
            Get-ADFineGrainedPasswordPolicySubject | ActiveDirectory
            Assert-Module | DscResource.Common
            New-InvalidOperationException | DscResource.Common
            Get-ADCommonParameters | ActiveDirectoryDsc.Common
#>

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

        [Parameter(Mandatory = $true)]
        [System.UInt32]
        $Precedence,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )

    Assert-Module -ModuleName 'ActiveDirectory'

    [HashTable] $parameters = $PSBoundParameters

    $getADFineGrainedPasswordPolicyParameters = Get-ADCommonParameters @parameters
    $getADFineGrainedPasswordPolicyParameters['Properties'] = @(
        'ProtectedFromAccidentalDeletion'
        'DisplayName'
        'Description'
    )

    Write-Verbose -Message ($script:localizedData.QueryingPasswordPolicy -f $Name)

    try
    {
        $policy = Get-ADFineGrainedPasswordPolicy @getADFineGrainedPasswordPolicyParameters
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        Write-Verbose -Message ($script:localizedData.PasswordPolicyNotFound -f $Name)
        $policy = $null
    }
    catch
    {
        $errorMessage = $script:localizedData.RetrievePasswordPolicyError -f $Name
        New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
    }

    if ($policy)
    {
        $getADFineGrainedPasswordPolicySubjectParameters = Get-ADCommonParameters @parameters

        try
        {
            [String[]] $policySubjects = (Get-ADFineGrainedPasswordPolicySubject `
                    @getADFineGrainedPasswordPolicySubjectParameters).Name
        }
        catch
        {
            $errorMessage = $script:localizedData.RetrievePasswordPolicySubjectError -f $Name
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        $targetResource = @{
            Name                            = $Name
            DisplayName                     = $policy.DisplayName
            Description                     = $policy.Description
            ComplexityEnabled               = $policy.ComplexityEnabled
            LockoutDuration                 = $policy.LockoutDuration
            LockoutObservationWindow        = $policy.LockoutObservationWindow
            LockoutThreshold                = $policy.LockoutThreshold
            MinPasswordAge                  = $policy.MinPasswordAge
            MaxPasswordAge                  = $policy.MaxPasswordAge
            MinPasswordLength               = $policy.MinPasswordLength
            PasswordHistoryCount            = $policy.PasswordHistoryCount
            ReversibleEncryptionEnabled     = $policy.ReversibleEncryptionEnabled
            Precedence                      = $policy.Precedence
            ProtectedFromAccidentalDeletion = $policy.ProtectedFromAccidentalDeletion
            Ensure                          = 'Present'
            Subjects                        = $policySubjects
        }
    }
    else
    {
        $targetResource = @{
            Name                            = $Name
            DisplayName                     = $null
            Description                     = $null
            ComplexityEnabled               = $null
            LockoutDuration                 = $null
            LockoutObservationWindow        = $null
            LockoutThreshold                = $null
            MinPasswordAge                  = $null
            MaxPasswordAge                  = $null
            MinPasswordLength               = $null
            PasswordHistoryCount            = $null
            ReversibleEncryptionEnabled     = $null
            Precedence                      = $null
            ProtectedFromAccidentalDeletion = $null
            Ensure                          = 'Absent'
            Subjects                        = @()
        }
    }

    return $targetResource
} #end Get-TargetResource

<#
    .SYNOPSIS
        Determines if the Active Directory fine-grained password policy is in
        the desired state

    .PARAMETER Name
        Specifies an Active Directory fine-grained password policy object name.

    .PARAMETER Precedence
        Specifies a value that defines the precedence of a fine-grained password policy among all fine-grained password
        policies.

    .PARAMETER DisplayName
        Specifies the display name of the object.

    .PARAMETER Description
        Specifies the description of the object.

    .PARAMETER Subjects
        Specifies the ADPrincipal names the policy is to be applied to, overwrites all existing.

    .PARAMETER Ensure
        Specifies whether the fine grained password policy should be present or absent. Default value is 'Present'.

    .PARAMETER ComplexityEnabled
        Specifies whether password complexity is enabled for the password policy.

    .PARAMETER LockoutDuration
        Specifies the length of time that an account is locked after the number of failed login attempts exceeds the
        lockout threshold. The lockout duration must be greater than or equal to the lockout observation time for a
        password policy. The value must be a string representation of a TimeSpan value.

    .PARAMETER LockoutObservationWindow
        Specifies the maximum time interval between two unsuccessful login attempts before the number of unsuccessful
        login attempts is reset to 0. The lockout observation window must be smaller than or equal to the lockout
        duration for a password policy. The value must be a string representation of a TimeSpan value.

    .PARAMETER LockoutThreshold
        Specifies the number of unsuccessful login attempts that are permitted before an account is locked out.

    .PARAMETER MinPasswordAge
        Specifies the minimum length of time before you can change a password. The value must be a string
        representation of a TimeSpan value.

    .PARAMETER MaxPasswordAge
        Specifies the maximum length of time that you can have the same password. The value must be a string
        representation of a TimeSpan value.

    .PARAMETER MinPasswordLength
        Specifies the minimum number of characters that a password must contain.

    .PARAMETER PasswordHistoryCount
        Specifies the number of previous passwords to save.

    .PARAMETER ReversibleEncryptionEnabled
        Specifies whether the directory must store passwords using reversible encryption.

    .PARAMETER ProtectedFromAccidentalDeletion
        Specifies whether to prevent the object from being deleted.

    .PARAMETER DomainController
        Specifies the Active Directory Domain Services instance to connect to.

    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.

    .NOTES
        Used Functions:
            Name | Module
            ------------------------------|--------------------------
            Compare-ResourcePropertyState | ActiveDirectoryDsc.Common
#>

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

        [Parameter(Mandatory = $true)]
        [System.UInt32]
        $Precedence,

        [Parameter()]
        [System.String]
        $DisplayName,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter()]
        [System.String[]]
        $Subjects,

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

        [Parameter()]
        [System.Boolean]
        $ComplexityEnabled,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $LockoutDuration,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $LockoutObservationWindow,

        [Parameter()]
        [System.UInt32]
        $LockoutThreshold,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $MinPasswordAge,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $MaxPasswordAge,

        [Parameter()]
        [System.UInt32]
        $MinPasswordLength,

        [Parameter()]
        [System.UInt32]
        $PasswordHistoryCount,

        [Parameter()]
        [System.Boolean]
        $ReversibleEncryptionEnabled,

        [Parameter()]
        [System.Boolean]
        $ProtectedFromAccidentalDeletion,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )

    [HashTable] $parameters = $PSBoundParameters

    # Build parameters needed to get resource properties
    $getTargetResourceParameters = @{
        Name             = $Name
        Precedence       = $Precedence
        DomainController = $DomainController
        Credential       = $Credential
    }

    @($getTargetResourceParameters.Keys) |
        ForEach-Object {
            if (-not $parameters.ContainsKey($_))
            {
                $getTargetResourceParameters.Remove($_)
            }
        }

    $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters

    if ($getTargetResourceResult.Ensure -eq 'Present')
    {
        if ($Ensure -eq 'Present')
        {
            # Resource should exist
            $propertiesNotInDesiredState = (
                Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters `
                    -IgnoreProperties 'Name', 'Identity', 'Credential', 'DomainController' |
                    Where-Object -Property InDesiredState -eq $false)

            if ($propertiesNotInDesiredState)
            {
                $inDesiredState = $false
            }
            else
            {
                # Resource is in desired state
                Write-Verbose -Message ($script:localizedData.PasswordPolicyInDesiredState -f $Name)
                $inDesiredState = $true
            }
        }
        else
        {
            # Resource should not exist
            Write-Verbose -Message ($script:localizedData.PasswordPolicyExistsButShouldNot -f $Name)
            $inDesiredState = $false
        }
    }
    else
    {
        # Resource does not exist
        if ($Ensure -eq 'Present')
        {
            # Resource should exist
            Write-Verbose -Message ($script:localizedData.PasswordPolicyDoesNotExistButShould -f $Name)
            $inDesiredState = $false
        }
        else
        {
            # Resource should not exist
            $inDesiredState = $true
        }
    }

    if ($inDesiredState)
    {
        Write-Verbose -Message ($script:localizedData.PasswordPolicyInDesiredState -f $Name)
        return $true
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.PasswordPolicyNotInDesiredState -f $Name)
        return $false
    }
} #end Test-TargetResource

<#
    .SYNOPSIS
        Modifies the Active Directory fine-grained password policy.

    .PARAMETER Name
        Specifies an Active Directory fine-grained password policy object name.

    .PARAMETER Precedence
        Specifies a value that defines the precedence of a fine-grained password policy among all fine-grained password
        policies.

    .PARAMETER DisplayName
        Specifies the display name of the object.

    .PARAMETER Description
        Specifies the description of the object.

    .PARAMETER Subjects
        Specifies the ADPrincipal names the policy is to be applied to, overwrites all existing.

    .PARAMETER Ensure
        Specifies whether the fine grained password policy should be present or absent. Default value is 'Present'.

    .PARAMETER ComplexityEnabled
        Specifies whether password complexity is enabled for the password policy.

    .PARAMETER LockoutDuration
        Specifies the length of time that an account is locked after the number of failed login attempts exceeds the
        lockout threshold. The lockout duration must be greater than or equal to the lockout observation time for a
        password policy. The value must be a string representation of a TimeSpan value.

    .PARAMETER LockoutObservationWindow
        Specifies the maximum time interval between two unsuccessful login attempts before the number of unsuccessful
        login attempts is reset to 0. The lockout observation window must be smaller than or equal to the lockout
        duration for a password policy. The value must be a string representation of a TimeSpan value.

    .PARAMETER LockoutThreshold
        Specifies the number of unsuccessful login attempts that are permitted before an account is locked out.

    .PARAMETER MinPasswordAge
        Specifies the minimum length of time before you can change a password. The value must be a string
        representation of a TimeSpan value.

    .PARAMETER MaxPasswordAge
        Specifies the maximum length of time that you can have the same password. The value must be a string
        representation of a TimeSpan value.

    .PARAMETER MinPasswordLength
        Specifies the minimum number of characters that a password must contain.

    .PARAMETER PasswordHistoryCount
        Specifies the number of previous passwords to save.

    .PARAMETER ReversibleEncryptionEnabled
        Specifies whether the directory must store passwords using reversible encryption.

    .PARAMETER ProtectedFromAccidentalDeletion
        Specifies whether to prevent the object from being deleted.

    .PARAMETER DomainController
        Specifies the Active Directory Domain Services instance to connect to.

    .PARAMETER Credential
        Specifies the user account credentials to use to perform this task.

    .NOTES
        Used Functions:
            Name | Module
            ------------------------------------------|--------------------------
            New-ADFineGrainedPasswordPolicy | ActiveDirectory
            Set-ADFineGrainedPasswordPolicy | ActiveDirectory
            Remove-ADFineGrainedPasswordPolicy | ActiveDirectory
            Add-ADFineGrainedPasswordPolicySubject | ActiveDirectory
            Remove-ADFineGrainedPasswordPolicySubject | ActiveDirectory
            New-InvalidOperationException | DscResource.Common
            Get-ADCommonParameters | ActiveDirectoryDsc.Common
            Compare-ResourcePropertyState | ActiveDirectoryDsc.Common
#>

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

        [Parameter(Mandatory = $true)]
        [System.UInt32]
        $Precedence,

        [Parameter()]
        [System.String]
        $DisplayName,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter()]
        [System.String[]]
        $Subjects,

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

        [Parameter()]
        [System.Boolean]
        $ComplexityEnabled,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $LockoutDuration,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $LockoutObservationWindow,

        [Parameter()]
        [System.UInt32]
        $LockoutThreshold,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $MinPasswordAge,

        [Parameter()]
        [ValidateScript( {
                ([ValidateRange(0, 10675199)]$valueInDays = [TimeSpan]::Parse($_).TotalDays); $?
            })]
        [System.String]
        $MaxPasswordAge,

        [Parameter()]
        [System.UInt32]
        $MinPasswordLength,

        [Parameter()]
        [System.UInt32]
        $PasswordHistoryCount,

        [Parameter()]
        [System.Boolean]
        $ReversibleEncryptionEnabled,

        [Parameter()]
        [System.Boolean]
        $ProtectedFromAccidentalDeletion,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )

    [HashTable] $parameters = $PSBoundParameters
    $parameters.Remove('Ensure')

    $getTargetResourceParameters = @{
        Name             = $Name
        Precedence       = $Precedence
        DomainController = $DomainController
        Credential       = $Credential
    }

    @($getTargetResourceParameters.Keys) |
        ForEach-Object {
            if (-not $parameters.ContainsKey($_))
            {
                $getTargetResourceParameters.Remove($_)
            }
        }

    $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters

    $passwordPolicyParameters = Get-ADCommonParameters @parameters

    if ($Ensure -eq 'Present')
    {
        # Resource should be present and set correctly
        if ($getTargetResourceResult.Ensure -eq 'Present')
        {
            # Resource exists and should be in desired state
            $propertiesNotInDesiredState = (
                Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters `
                    -IgnoreProperties 'Name', 'Identity', 'Credential', 'DomainController' |
                    Where-Object -Property InDesiredState -eq $false)

            if ($propertiesNotInDesiredState)
            {
                # Resource is present not in desired state
                $setPasswordPolicyParameters = $passwordPolicyParameters.Clone()
                $setPasswordPolicyRequired = $false

                Write-Verbose -Message ($script:localizedData.PasswordPolicyNotInDesiredState -f $Name)

                # Build parameters needed to set resource properties
                foreach ($property in $propertiesNotInDesiredState)
                {
                    if ($property.ParameterName -eq 'Subjects')
                    {
                        # Add/Remove required Policy Subjects
                        if (-not [System.String]::IsNullOrEmpty($property.Actual) -and
                            -not [System.String]::IsNullOrEmpty($property.Expected))
                        {
                            $compareResult = Compare-Object -ReferenceObject $property.Actual `
                                -DifferenceObject $property.Expected

                            $subjectsToAdd = ($compareResult |
                                    Where-Object -Property SideIndicator -eq '=>').InputObject
                            $subjectsToRemove = ($compareResult |
                                    Where-Object -Property SideIndicator -eq '<=').InputObject
                        }
                        elseif ([System.String]::IsNullOrEmpty($property.Expected))
                        {
                            $subjectsToRemove = $property.Actual
                            $subjectsToAdd = $null
                        }
                        else
                        {
                            $subjectsToAdd = $property.Expected
                            $subjectsToRemove = $null
                        }

                        if (-not [System.String]::IsNullOrEmpty($subjectsToAdd))
                        {
                            Write-Verbose -Message ($script:localizedData.AddingPasswordPolicySubjects -f
                                $Name, $($subjectsToAdd.Count))

                            try
                            {
                                Add-ADFineGrainedPasswordPolicySubject @passwordPolicyParameters `
                                    -Subjects $subjectsToAdd
                            }
                            catch
                            {
                                $errorMessage = $script:localizedData.AddingPasswordPolicySubjectsError -f $Name
                                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
                            }
                        }

                        if (-not [System.String]::IsNullOrEmpty($subjectsToRemove))
                        {
                            Write-Verbose -Message ($script:localizedData.RemovingPasswordPolicySubjects -f
                                $Name, $($SubjectstoRemove.Count))

                            try
                            {
                                Remove-ADFineGrainedPasswordPolicySubject @passwordPolicyParameters `
                                    -Subjects $subjectsToRemove -Confirm:$false
                            }
                            catch
                            {
                                $errorMessage = $script:localizedData.RemovingPasswordPolicySubjectsError -f $Name
                                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
                            }
                        }
                    }
                    else
                    {
                        $setPasswordPolicyParameters[$property.ParameterName] = $property.Expected

                        Write-Verbose -Message ($script:localizedData.SettingPasswordPolicyValue -f
                            $Name, $property.ParameterName, $property.Expected)

                        $setPasswordPolicyRequired = $true
                    }
                }

                # Update the password policy if needed
                if ($setPasswordPolicyRequired)
                {
                    try
                    {
                        Set-ADFineGrainedPasswordPolicy @setPasswordPolicyParameters
                    }
                    catch
                    {
                        $errorMessage = $script:localizedData.SettingPasswordPolicyError -f $Name
                        New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
                    }
                }
            }
            else
            {
                # Resource is in desired state
                Write-Verbose -Message ($script:localizedData.PasswordPolicyInDesiredState -f $Name)
            }
        }
        else
        {
            # Resource should exist

            Write-Verbose -Message ($script:localizedData.PasswordPolicyDoesNotExistButShould -f $Name)

            Write-Verbose -Message ($script:localizedData.CreatingPasswordPolicy -f $Name)

            # Build parameters needed to create resource properties
            $createSubjectsRequired = $false
            $newPasswordPolicyParameters = $passwordPolicyParameters.Clone()
            $newPasswordPolicyParameters.Remove('Identity')

            foreach ($property in $parameters.keys)
            {
                if ($property -eq 'Subjects')
                {
                    $createSubjectsRequired = $true
                }
                else
                {
                    $newPasswordPolicyParameters[$property] = $parameters[$property]
                }
            }

            try
            {
                New-ADFineGrainedPasswordPolicy @newPasswordPolicyParameters
            }
            catch
            {
                $errorMessage = $script:localizedData.AddingPasswordPolicyError -f $Name
                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
            }

            if ($createSubjectsRequired)
            {
                try
                {
                    Add-ADFineGrainedPasswordPolicySubject @passwordPolicyParameters  -Subjects $Subjects
                }
                catch
                {
                    $errorMessage = $script:localizedData.AddingPasswordPolicySubjectsError -f $Name
                    New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
                }
            }
        }
    }
    else
    {
        # Resource should not exist

        if ($getTargetResourceResult.Ensure -eq 'Present')
        {
            # Resource exists but shouldn't

            Write-Verbose -Message ($script:localizedData.PasswordPolicyExistsButShouldNot -f $Name)

            if ($getTargetResourceResult.ProtectedFromAccidentalDeletion)
            {
                Write-Verbose -Message ($script:localizedData.RemoveDeletionProtection -f $Name)

                try
                {
                    Set-ADFineGrainedPasswordPolicy @passwordPolicyParameters  `
                        -ProtectedFromAccidentalDeletion $false
                }
                catch
                {
                    $errorMessage = $script:localizedData.RemovingDeletionProtectionError -f $Name
                    New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
                }
            }

            Write-Verbose -Message ($script:localizedData.RemovingPasswordPolicy -f $Name)

            try
            {
                Remove-ADFineGrainedPasswordPolicy @passwordPolicyParameters
            }
            catch
            {
                $errorMessage = $script:localizedData.RemovePasswordPolicyError -f $Name
                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
            }
        }
        else
        {
            # Resource should not and does not exist

            Write-Verbose -Message ($script:localizedData.PasswordPolicyInDesiredState -f $Name)
        }
    }
} #end Set-TargetResource

Export-ModuleMember -Function *-TargetResource