DSCResources/MSFT_SPUserProfileSyncService/MSFT_SPUserProfileSyncService.psm1

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

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

        [Parameter()]
        [System.Boolean]
        $RunOnlyWhenWriteable
    )

    Write-Verbose -Message "Getting user profile sync service for $UserProfileServiceAppName"

    if ((Get-SPDscInstalledProductVersion).FileMajorPart -ne 15)
    {
        $message = ("Only SharePoint 2013 is supported to deploy the user profile sync " + `
                "service via DSC, as 2016/2019 do not use the FIM based sync service.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $farmAccount = Invoke-SPDscCommand -Arguments $PSBoundParameters `
        -ScriptBlock {
        return Get-SPDscFarmAccount
    }

    if ($null -ne $farmAccount)
    {
        # PSDSCRunAsCredential or System
        if (-not $Env:USERNAME.Contains("$"))
        {
            # PSDSCRunAsCredential used
            $localaccount = "$($Env:USERDOMAIN)\$($Env:USERNAME)"
            if ($localaccount -eq $farmAccount.UserName)
            {
                $message = ("Specified PSDSCRunAsCredential ($localaccount) is the Farm " + `
                        "Account. Make sure the specified PSDSCRunAsCredential isn't the " + `
                        "Farm Account and try again")
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }
        }
    }
    else
    {
        $message = ("Unable to retrieve the Farm Account. Check if the farm exists.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $result = Invoke-SPDscCommand -Arguments $PSBoundParameters `
        -ScriptBlock {
        $params = $args[0]

        $services = Get-SPServiceInstance -Server $env:COMPUTERNAME `
            -ErrorAction SilentlyContinue

        if ($null -eq $services)
        {
            return @{
                UserProfileServiceAppName = $params.UserProfileServiceAppName
                Ensure                    = "Absent"
                RunOnlyWhenWriteable      = $params.RunOnlyWhenWriteable
            }
        }

        $syncService = $services | Where-Object -FilterScript {
            $_.GetType().Name -eq "ProfileSynchronizationServiceInstance"
        }

        if ($null -eq $syncService)
        {
            $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
            $currentServer = "$($env:COMPUTERNAME).$domain"
            $services = Get-SPServiceInstance -Server $currentServer `
                -ErrorAction SilentlyContinue
            $syncService = $services | Where-Object -FilterScript {
                $_.GetType().Name -eq "ProfileSynchronizationServiceInstance"
            }
        }

        if ($null -eq $syncService)
        {
            return @{
                UserProfileServiceAppName = $params.UserProfileServiceAppName
                Ensure                    = "Absent"
                RunOnlyWhenWriteable      = $params.RunOnlyWhenWriteable
            }
        }
        if ($null -ne $syncService.UserProfileApplicationGuid -and `
                $syncService.UserProfileApplicationGuid -ne [Guid]::Empty)
        {
            $upa = Get-SPServiceApplication -Identity $syncService.UserProfileApplicationGuid `
                -ErrorAction SilentlyContinue
        }
        if ($syncService.Status -eq "Online")
        {
            $localEnsure = "Present"
        }
        else
        {
            $localEnsure = "Absent"
        }

        return @{
            UserProfileServiceAppName = $upa.Name
            Ensure                    = $localEnsure
            RunOnlyWhenWriteable      = $params.RunOnlyWhenWriteable
        }
    }
    return $result
}


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

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

        [Parameter()]
        [System.Boolean]
        $RunOnlyWhenWriteable
    )

    Write-Verbose -Message "Setting user profile sync service for $UserProfileServiceAppName"

    $PSBoundParameters.Ensure = $Ensure

    if ((Get-SPDscInstalledProductVersion).FileMajorPart -ne 15)
    {
        $message = ("Only SharePoint 2013 is supported to deploy the user profile sync " + `
                "service via DSC, as 2016/2019 do not use the FIM based sync service.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $farmAccount = Invoke-SPDscCommand -Arguments $PSBoundParameters `
        -ScriptBlock {
        return Get-SPDscFarmAccount
    }

    if ($null -ne $farmAccount)
    {
        # PSDSCRunAsCredential or System
        if (-not $Env:USERNAME.Contains("$"))
        {
            # PSDSCRunAsCredential used
            $localaccount = "$($Env:USERDOMAIN)\$($Env:USERNAME)"
            if ($localaccount -eq $farmAccount.UserName)
            {
                $message = ("Specified PSDSCRunAsCredential ($localaccount) is the Farm " + `
                        "Account. Make sure the specified PSDSCRunAsCredential isn't the " + `
                        "Farm Account and try again")
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }
        }
    }
    else
    {
        $message = ("Unable to retrieve the Farm Account. Check if the farm exists.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    if ($PSBoundParameters.ContainsKey("RunOnlyWhenWriteable") -eq $true)
    {
        $databaseReadOnly = Test-SPDscUserProfileDBReadOnly `
            -UserProfileServiceAppName $UserProfileServiceAppName

        if ($databaseReadOnly)
        {
            Write-Verbose -Message ("User profile database is read only, setting user profile " + `
                    "sync service to not run on the local server")
            $PSBoundParameters.Ensure = "Absent"
        }
        else
        {
            $PSBoundParameters.Ensure = "Present"
        }
    }

    # Add the Farm Account to the local Admins group, if it's not already there
    $isLocalAdmin = Test-SPDscUserIsLocalAdmin -UserName $farmAccount.UserName

    if (!$isLocalAdmin)
    {
        Write-Verbose -Message "Adding farm account to Local Administrators group"
        Add-SPDscUserToLocalAdmin -UserName $farmAccount.UserName

        # Cycle the Timer Service and flush Kerberos tickets
        # so that it picks up the local Admin token
        Restart-Service -Name "SPTimerV4"

        Clear-SPDscKerberosToken -Account $farmAccount.UserName
    }

    $isInDesiredState = $false
    try
    {
        Invoke-SPDscCommand -Credential $FarmAccount `
            -Arguments ($PSBoundParameters, $MyInvocation.MyCommand.Source, $farmAccount) `
            -ScriptBlock {
            $params = $args[0]
            $eventSource = $args[1]
            $farmAccount = $args[2]

            $currentServer = $env:COMPUTERNAME

            $services = Get-SPServiceInstance -Server $currentServer `
                -ErrorAction SilentlyContinue
            $syncService = $services | Where-Object -FilterScript {
                $_.GetType().Name -eq "ProfileSynchronizationServiceInstance"
            }
            if ($null -eq $syncService)
            {
                $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
                $currentServer = "$currentServer.$domain"
                $syncService = $services | Where-Object -FilterScript {
                    $_.GetType().Name -eq "ProfileSynchronizationServiceInstance"
                }
            }
            if ($null -eq $syncService)
            {
                $message = "Unable to locate a user profile sync service instance on $currentServer to start"
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $eventSource
                throw $message
            }

            # Start the Sync service if it should be running on this server
            if (($params.Ensure -eq "Present") -and ($syncService.Status -ne "Online"))
            {
                $ups = Get-SPServiceApplication | Where-Object -FilterScript {
                    $_.Name -eq $params.UserProfileServiceAppName
                }

                if ($null -eq $ups)
                {
                    $message = ("No User Profile Service Application was found " + `
                            "named $($params.UserProfileServiceAppName)")
                    Add-SPDscEvent -Message $message `
                        -EntryType 'Error' `
                        -EventID 100 `
                        -Source $eventSource
                    throw $message
                }

                $userName = $farmAccount.UserName
                $password = $farmAccount.GetNetworkCredential().Password
                $ups.SetSynchronizationMachine($currentServer, $syncService.ID, $userName, $password)

                Start-SPServiceInstance -Identity $syncService.ID

                $desiredState = "Online"
            }
            # Stop the Sync service in all other cases
            else
            {
                Stop-SPServiceInstance -Identity $syncService.ID -Confirm:$false
                $desiredState = "Disabled"
            }

            $count = 0
            $maxCount = 20

            while (($count -lt $maxCount) -and ($syncService.Status -ne $desiredState))
            {
                if ($syncService.Status -ne $desiredState)
                {
                    Start-Sleep -Seconds 60
                }

                # Get the current status of the Sync service
                Write-Verbose -Message ("$([DateTime]::Now.ToShortTimeString()) - Waiting for user " + `
                        "profile sync service to become '$desiredState' (waited " + `
                        "$count of $maxCount minutes)")

                $services = Get-SPServiceInstance -Server $currentServer `
                    -ErrorAction SilentlyContinue
                $syncService = $services | Where-Object -FilterScript {
                    $_.GetType().Name -eq "ProfileSynchronizationServiceInstance"
                }
                $count++
            }

            if ($syncService.Status -ne $desiredState)
            {
                $message = "An error occured. We couldn't properly set the User Profile Sync Service on the server."
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $eventSource
                throw $message
            }
        }
    }
    finally
    {
        # Remove the Farm Account from the local Admins group, if it was added above
        if (!$isLocalAdmin)
        {
            Write-Verbose -Message "Removing farm account from Local Administrators group"
            Remove-SPDscUserToLocalAdmin -UserName $farmAccount.UserName

            # Cycle the Timer Service and flush Kerberos tickets
            # so that it picks up the local Admin token
            Restart-Service -Name "SPTimerV4"

            Clear-SPDscKerberosToken -Account $farmAccount.UserName
        }
    }
}

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

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

        [Parameter()]
        [System.Boolean]
        $RunOnlyWhenWriteable
    )

    Write-Verbose -Message "Testing user profile sync service for $UserProfileServiceAppName"

    $PSBoundParameters.Ensure = $Ensure

    if ((Get-SPDscInstalledProductVersion).FileMajorPart -ne 15)
    {
        $message = ("Only SharePoint 2013 is supported to deploy the user profile sync " + `
                "service via DSC, as 2016/2019 do not use the FIM based sync service.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $CurrentValues = Get-TargetResource @PSBoundParameters

    Write-Verbose -Message "Current Values: $(Convert-SPDscHashtableToString -Hashtable $CurrentValues)"
    Write-Verbose -Message "Target Values: $(Convert-SPDscHashtableToString -Hashtable $PSBoundParameters)"

    if ($PSBoundParameters.ContainsKey("RunOnlyWhenWriteable") -eq $true)
    {
        $databaseReadOnly = Test-SPDscUserProfileDBReadOnly `
            -UserProfileServiceAppName $UserProfileServiceAppName

        if ($databaseReadOnly)
        {
            Write-Verbose -Message ("User profile database is read only, setting user profile " + `
                    "sync service to not run on the local server")
            $PSBoundParameters.Ensure = "Absent"
        }
        else
        {
            $PSBoundParameters.Ensure = "Present"
        }
    }

    $result = Test-SPDscParameterState -CurrentValues $CurrentValues `
        -Source $($MyInvocation.MyCommand.Source) `
        -DesiredValues $PSBoundParameters `
        -ValuesToCheck @("Ensure")

    Write-Verbose -Message "Test-TargetResource returned $result"

    return $result
}

function Test-SPDscUserProfileDBReadOnly()
{
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $UserProfileServiceAppName
    )

    $databaseReadOnly = Invoke-SPDscCommand -Arguments @($UserProfileServiceAppName, $MyInvocation.MyCommand.Source) `
        -ScriptBlock {
        $UserProfileServiceAppName = $args[0]
        $eventSource = $args[1]

        $serviceApps = Get-SPServiceApplication | Where-Object -FilterScript {
            $_.Name -eq $UserProfileServiceAppName
        }

        if ($null -eq $serviceApps)
        {
            $message = ("No user profile service was found " + `
                    "named $UserProfileServiceAppName")
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $eventSource
            throw $message
        }
        $ups = $serviceApps | Where-Object -FilterScript {
            $_.GetType().FullName -eq "Microsoft.Office.Server.Administration.UserProfileApplication"
        }

        $propType = $ups.GetType()
        $propData = $propType.GetProperties([System.Reflection.BindingFlags]::Instance -bor `
                [System.Reflection.BindingFlags]::NonPublic)
        $profileProp = $propData | Where-Object -FilterScript {
            $_.Name -eq "ProfileDatabase"
        }
        $profileDBName = $profileProp.GetValue($ups).Name

        $database = Get-SPDatabase | Where-Object -FilterScript {
            $_.Name -eq $profileDBName
        }
        return $database.IsReadyOnly
    }
    return $databaseReadOnly
}

<## This function retrieves all Services in the SharePoint farm. It does not care if the service is enabled or not. It lists them all, and simply sets the "Ensure" attribute of those that are disabled to "Absent". #>
function Export-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter()]
        [System.String[]]
        $Servers
    )

    $VerbosePreference = "SilentlyContinue"
    $servicesMasterList = @()
    foreach ($Server in $Servers)
    {
        Write-Host "Scanning SPServiceInstance on {$Server}"
        $serviceInstancesOnCurrentServer = Get-SPServiceInstance -Server $Server | Sort-Object -Property TypeName
        $serviceStatuses = @()
        $ensureValue = "Present"

        $i = 1
        $total = $serviceInstancesOnCurrentServer.Length
        foreach ($serviceInstance in $serviceInstancesOnCurrentServer)
        {
            try
            {
                $serviceTypeName = $serviceInstance.GetType().Name
                Write-Host " -> Scanning instance [$i/$total] {$serviceTypeName}"

                if ($serviceInstance.Status -eq "Online")
                {
                    $ensureValue = "Present"
                }
                else
                {
                    $ensureValue = "Absent"
                }

                $currentService = @{
                    Name   = $serviceInstance.TypeName
                    Ensure = $ensureValue
                }

                if ($serviceTypeName -ne "SPDistributedCacheServiceInstance" -and $serviceTypeName -ne "ProfileSynchronizationServiceInstance")
                {
                    $serviceStatuses += $currentService
                }
                if ($ensureValue -eq "Present" -and !$servicesMasterList.Contains($serviceTypeName))
                {
                    $servicesMasterList += $serviceTypeName
                    if ($serviceTypeName -eq "ProfileSynchronizationServiceInstance")
                    {
                        $ParentModuleBase = Get-Module "SharePointDsc" -ListAvailable | Select-Object -ExpandProperty Modulebase
                        $module = Join-Path -Path $ParentModuleBase -ChildPath  "\DSCResources\MSFT_SPUserProfileSyncService\MSFT_SPUserProfileSyncService.psm1" -Resolve
                        $Content = ''
                        $params = Get-DSCFakeParameters -ModulePath $module
                        $params.Ensure = $ensureValue

                        $results = Get-TargetResource @params
                        if ($ensureValue -eq "Present")
                        {
                            $PartialContent = " SPUserProfileSyncService " + $serviceTypeName.Replace(" ", "") + "Instance`r`n"
                            $PartialContent += " {`r`n"

                            $results = Repair-Credentials -results $results
                            $currentBlock = Get-DSCBlock -Params $results -ModulePath $module
                            $currentBlock = Convert-DSCStringParamToVariable -DSCBlock $currentBlock -ParameterName "PsDscRunAsCredential"
                            $PartialContent += $currentBlock
                            $PartialContent += " }`r`n"
                            $Content += $PartialContent
                        }
                    }
                }
                $i++
            }
            catch
            {
                $_
                $Global:ErrorLog += "[Service Instance]" + $serviceInstance.TypeName + "`r`n"
                $Global:ErrorLog += "$_`r`n`r`n"
            }
        }

        if ($DynamicCompilation)
        {
            Add-ConfigurationDataEntry -Node  $env:ComputerName -Key "ServiceInstances" -Value $serviceStatuses
        }
        elseif ($StandAlone)
        {
            Add-ConfigurationDataEntry -Node $env:ComputerName -Key "ServiceInstances" -Value $serviceStatuses
        }
        elseif ($servicesStatuses.Length -gt 0)
        {
            Add-ConfigurationDataEntry -Node $Server -Key "ServiceInstances" -Value $serviceStatuses
        }
    }
    return $Content
}

Export-ModuleMember -Function *-TargetResource