cWindowsServiceDSC.psm1

enum Ensure
{ 
    Absent
    Present
}

enum StartupType
{ 
    Automatic
    AutomaticDelayed
    Disabled
    Manual
}

enum RecoveryType
{
    Restart
    Reboot
}

function Set-CurrentRecoveryType
{
    param($original, [WindowsServiceDSC]$strongTypedOutput)

    switch -Wildcard ($original)
    {
        "*RESTART*"
        {
            $strongTypedOutput.ServiceCurrentRecoveryType += 'Restart'
            break
        }
        "*REBOOT*"
        {
            $strongTypedOutput.ServiceCurrentRecoveryType += 'Reboot'
            break
        }
    }
}

<#
    .SYNOPSIS
        Grants the 'Log on as a service' right to the user with the given username.

    .PARAMETER Username
        The username of the user to grant 'Log on as a service' right to
#>

function Grant-LogOnAsServiceRight
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Username
    )

    $logOnAsServiceText = @"
        namespace LogOnAsServiceHelper
        {
            using Microsoft.Win32.SafeHandles;
            using System;
            using System.Runtime.ConstrainedExecution;
            using System.Runtime.InteropServices;
            using System.Security;

            public class NativeMethods
            {
                #region constants
                // from ntlsa.h
                private const int POLICY_LOOKUP_NAMES = 0x00000800;
                private const int POLICY_CREATE_ACCOUNT = 0x00000010;
                private const uint ACCOUNT_ADJUST_SYSTEM_ACCESS = 0x00000008;
                private const uint ACCOUNT_VIEW = 0x00000001;
                private const uint SECURITY_ACCESS_SERVICE_LOGON = 0x00000010;

                // from LsaUtils.h
                private const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;

                // from lmcons.h
                private const int UNLEN = 256;
                private const int DNLEN = 15;

                // Extra characteres for '\', '@' etc.
                private const int EXTRA_LENGTH = 3;
                #endregion constants

                #region interop structures
                /// <summary>
                /// Used to open a policy, but not containing anything meaqningful
                /// </summary>
                [StructLayout(LayoutKind.Sequential)]
                private struct LSA_OBJECT_ATTRIBUTES
                {
                    public UInt32 Length;
                    public IntPtr RootDirectory;
                    public IntPtr ObjectName;
                    public UInt32 Attributes;
                    public IntPtr SecurityDescriptor;
                    public IntPtr SecurityQualityOfService;

                    public void Initialize()
                    {
                        this.Length = 0;
                        this.RootDirectory = IntPtr.Zero;
                        this.ObjectName = IntPtr.Zero;
                        this.Attributes = 0;
                        this.SecurityDescriptor = IntPtr.Zero;
                        this.SecurityQualityOfService = IntPtr.Zero;
                    }
                }

                /// <summary>
                /// LSA string
                /// </summary>
                [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                private struct LSA_UNICODE_STRING
                {
                    internal ushort Length;
                    internal ushort MaximumLength;
                    [MarshalAs(UnmanagedType.LPWStr)]
                    internal string Buffer;

                    internal void Set(string src)
                    {
                        this.Buffer = src;
                        this.Length = (ushort)(src.Length * sizeof(char));
                        this.MaximumLength = (ushort)(this.Length + sizeof(char));
                    }
                }

                /// <summary>
                /// Structure used as the last parameter for LSALookupNames
                /// </summary>
                [StructLayout(LayoutKind.Sequential)]
                private struct LSA_TRANSLATED_SID2
                {
                    public uint Use;
                    public IntPtr SID;
                    public int DomainIndex;
                    public uint Flags;
                };
                #endregion interop structures

                #region safe handles
                /// <summary>
                /// Handle for LSA objects including Policy and Account
                /// </summary>
                private class LsaSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
                {
                    [DllImport("advapi32.dll")]
                    private static extern uint LsaClose(IntPtr ObjectHandle);

                    /// <summary>
                    /// Prevents a default instance of the LsaPolicySafeHAndle class from being created.
                    /// </summary>
                    private LsaSafeHandle(): base(true)
                    {
                    }

                    /// <summary>
                    /// Calls NativeMethods.CloseHandle(handle)
                    /// </summary>
                    /// <returns>the return of NativeMethods.CloseHandle(handle)</returns>
                    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
                    protected override bool ReleaseHandle()
                    {
                        long returnValue = LsaSafeHandle.LsaClose(this.handle);
                        return returnValue != 0;

                    }
                }

                /// <summary>
                /// Handle for IntPtrs returned from Lsa calls that have to be freed with
                /// LsaFreeMemory
                /// </summary>
                private class SafeLsaMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
                {
                    [DllImport("advapi32")]
                    internal static extern int LsaFreeMemory(IntPtr Buffer);

                    private SafeLsaMemoryHandle() : base(true) { }

                    private SafeLsaMemoryHandle(IntPtr handle)
                        : base(true)
                    {
                        SetHandle(handle);
                    }

                    private static SafeLsaMemoryHandle InvalidHandle
                    {
                        get { return new SafeLsaMemoryHandle(IntPtr.Zero); }
                    }

                    override protected bool ReleaseHandle()
                    {
                        return SafeLsaMemoryHandle.LsaFreeMemory(handle) == 0;
                    }

                    internal IntPtr Memory
                    {
                        get
                        {
                            return this.handle;
                        }
                    }
                }
                #endregion safe handles

                #region interop function declarations
                /// <summary>
                /// Opens LSA Policy
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaOpenPolicy(
                    IntPtr SystemName,
                    ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
                    uint DesiredAccess,
                    out LsaSafeHandle PolicyHandle
                );

                /// <summary>
                /// Convert the name into a SID which is used in remaining calls
                /// </summary>
                [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true), SuppressUnmanagedCodeSecurityAttribute]
                private static extern uint LsaLookupNames2(
                    LsaSafeHandle PolicyHandle,
                    uint Flags,
                    uint Count,
                    LSA_UNICODE_STRING[] Names,
                    out SafeLsaMemoryHandle ReferencedDomains,
                    out SafeLsaMemoryHandle Sids
                );

                /// <summary>
                /// Opens the LSA account corresponding to the user's SID
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaOpenAccount(
                    LsaSafeHandle PolicyHandle,
                    IntPtr Sid,
                    uint Access,
                    out LsaSafeHandle AccountHandle);

                /// <summary>
                /// Creates an LSA account corresponding to the user's SID
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaCreateAccount(
                    LsaSafeHandle PolicyHandle,
                    IntPtr Sid,
                    uint Access,
                    out LsaSafeHandle AccountHandle);

                /// <summary>
                /// Gets the LSA Account access
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaGetSystemAccessAccount(
                    LsaSafeHandle AccountHandle,
                    out uint SystemAccess);

                /// <summary>
                /// Sets the LSA Account access
                /// </summary>
                [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
                private static extern uint LsaSetSystemAccessAccount(
                    LsaSafeHandle AccountHandle,
                    uint SystemAccess);
                #endregion interop function declarations

                /// <summary>
                /// Sets the Log On As A Service Policy for <paramref name="userName"/>, if not already set.
                /// </summary>
                /// <param name="userName">the user name we want to allow logging on as a service</param>
                /// <exception cref="ArgumentNullException">If the <paramref name="userName"/> is null or empty.</exception>
                /// <exception cref="InvalidOperationException">In the following cases:
                /// Failure opening the LSA Policy.
                /// The <paramref name="userName"/> is too large.
                /// Failure looking up the user name.
                /// Failure opening LSA account (other than account not found).
                /// Failure creating LSA account.
                /// Failure getting LSA account policy access.
                /// Failure setting LSA account policy access.
                /// </exception>
                public static void SetLogOnAsServicePolicy(string userName)
                {
                    if (String.IsNullOrEmpty(userName))
                    {
                        throw new ArgumentNullException("userName");
                    }

                    LSA_OBJECT_ATTRIBUTES objectAttributes = new LSA_OBJECT_ATTRIBUTES();
                    objectAttributes.Initialize();

                    // All handles are delcared in advance so they can be closed on finally
                    LsaSafeHandle policyHandle = null;
                    SafeLsaMemoryHandle referencedDomains = null;
                    SafeLsaMemoryHandle sids = null;
                    LsaSafeHandle accountHandle = null;

                    try
                    {
                        uint status = LsaOpenPolicy(
                            IntPtr.Zero,
                            ref objectAttributes,
                            POLICY_LOOKUP_NAMES | POLICY_CREATE_ACCOUNT,
                            out policyHandle);

                        if (status != 0)
                        {
                            throw new InvalidOperationException("CannotOpenPolicyErrorMessage");
                        }

                        // Unicode strings have a maximum length of 32KB. We don't want to create
                        // LSA strings with more than that. User lengths are much smaller so this check
                        // ensures userName's length is useful
                        if (userName.Length > UNLEN + DNLEN + EXTRA_LENGTH)
                        {
                            throw new InvalidOperationException("UserNameTooLongErrorMessage");
                        }

                        LSA_UNICODE_STRING lsaUserName = new LSA_UNICODE_STRING();
                        lsaUserName.Set(userName);

                        LSA_UNICODE_STRING[] names = new LSA_UNICODE_STRING[1];
                        names[0].Set(userName);

                        status = LsaLookupNames2(
                            policyHandle,
                            0,
                            1,
                            new LSA_UNICODE_STRING[] { lsaUserName },
                            out referencedDomains,
                            out sids);

                        if (status != 0)
                        {
                            throw new InvalidOperationException("CannotLookupNamesErrorMessage");
                        }

                        LSA_TRANSLATED_SID2 sid = (LSA_TRANSLATED_SID2)Marshal.PtrToStructure(sids.Memory, typeof(LSA_TRANSLATED_SID2));

                        status = LsaOpenAccount(policyHandle,
                                            sid.SID,
                                            ACCOUNT_VIEW | ACCOUNT_ADJUST_SYSTEM_ACCESS,
                                            out accountHandle);

                        uint currentAccess = 0;

                        if (status == 0)
                        {
                            status = LsaGetSystemAccessAccount(accountHandle, out currentAccess);

                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotGetAccountAccessErrorMessage");
                            }

                        }
                        else if (status == STATUS_OBJECT_NAME_NOT_FOUND)
                        {
                            status = LsaCreateAccount(
                                policyHandle,
                                sid.SID,
                                ACCOUNT_ADJUST_SYSTEM_ACCESS,
                                out accountHandle);

                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotCreateAccountAccessErrorMessage");
                            }
                        }
                        else
                        {
                            throw new InvalidOperationException("CannotOpenAccountErrorMessage");
                        }

                        if ((currentAccess & SECURITY_ACCESS_SERVICE_LOGON) == 0)
                        {
                            status = LsaSetSystemAccessAccount(
                                accountHandle,
                                currentAccess | SECURITY_ACCESS_SERVICE_LOGON);
                            if (status != 0)
                            {
                                throw new InvalidOperationException("CannotSetAccountAccessErrorMessage");
                            }
                        }
                    }
                    finally
                    {
                        if (policyHandle != null) { policyHandle.Close(); }
                        if (referencedDomains != null) { referencedDomains.Close(); }
                        if (sids != null) { sids.Close(); }
                        if (accountHandle != null) { accountHandle.Close(); }
                    }
                }
            }
        }
"@


    try
    {
        $null = [LogOnAsServiceHelper.NativeMethods]
    }
    catch
    {
      $null = Add-Type $logOnAsServiceText -PassThru  
    }

    if ($Username.StartsWith('.\'))
    {
        $Username = $Username.Substring(2)
    }

    try 
    {
        [LogOnAsServiceHelper.NativeMethods]::SetLogOnAsServicePolicy($Username)
    }
    catch 
    {
        throw 
    }
}

    <#
    .SYNOPSIS
        Converts the given username to the string version of it that would be expected in a
        service's StartName property.

    .PARAMETER Username
        The username to convert.
    #>

    function ConvertTo-StartName
    {
        [OutputType([System.String])]
        [CmdletBinding()]
        param
        (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $Username
        )
    
        $startName = $Username
    
        if ($Username -ieq 'NetworkService' -or $Username -ieq 'LocalService' -or $Username -ieq 'LocalSystem')
        {
            $startName = "NT Authority\$Username"
            return $startName
        }

        if (-not $Username.Contains('\') -and -not $Username.Contains('@'))
        {
            $startName = ".\$Username"
            return $startName
        }
        
        if ($Username.StartsWith("$env:computerName\"))
        {
            $startName = $Username.Replace($env:computerName, '.')
            return $startName
        }
    
        return $startName
    }

[DscResource()]
class WindowsServiceDSC
{
    [DscProperty(Key)]
    [String]$ServiceName

    [DscProperty(NotConfigurable)]
    [Ensure]$ServiceExists

    [DscProperty(NotConfigurable)]
    [StartupType]$ServiceCurrentStartupType

    [DscProperty(NotConfigurable)]
    [RecoveryType[]]$ServiceCurrentRecoveryType

    [DscProperty(NotConfigurable)]
    [String]$ServiceUser

    [DscProperty(Mandatory = $true)]
    [Ensure]$Ensure

    [DscProperty(Mandatory = $false)]
    [RecoveryType]$RecoveryType = [RecoveryType]::Restart

    [DscProperty(Mandatory = $false)]
    [StartupType]$StartupType = [StartupType]::Automatic

    [DscProperty(Mandatory = $false)]
    [System.Management.Automation.PSCredential]
    [System.Management.Automation.Credential()]
    $Credential = (New-Object -TypeName pscredential -ArgumentList 'NT AUTHORITY\SYSTEM', $(ConvertTo-SecureString -AsPlainText -Force -string 'whatever'))

    # Gets the resource's current state.
    [WindowsServiceDSC] Get()
    {
        try
        {
            Get-Service $this.ServiceName -ErrorAction 'stop'
            $this.ServiceExists = 'Present'

            $servieConfig = (sc.exe qc "$($this.ServiceName)")[4].trim()
            switch ($servieConfig)
            {
                "START_TYPE : 2 AUTO_START"
                {
                    $this.ServiceCurrentStartupType = 'Automatic'
                }
                "START_TYPE : 2 AUTO_START (DELAYED)"
                {
                    $this.ServiceCurrentStartupType = 'AutomaticDelayed'
                }
                "START_TYPE : 3 DEMAND_START"
                {
                    $this.ServiceCurrentStartupType = 'Manual'
                }
                "START_TYPE : 4 DISABLED"
                {
                    $this.ServiceCurrentStartupType = 'Disabled'
                }
            }

            $serviceRecoveryConfig = (sc.exe qfailure "$($this.ServiceName)").trim()
            #First Failure Recovery
            Set-CurrentRecoveryType $serviceRecoveryConfig[6] ([ref]$this)

            #Second Failure Recovery
            Set-CurrentRecoveryType $serviceRecoveryConfig[7] ([ref]$this)
            
            #Third Failure Recovery
            Set-CurrentRecoveryType $serviceRecoveryConfig[8] ([ref]$this)

            $this.ServiceUser = (Get-WmiObject -Class Win32_Service -Filter "name='$($this.ServiceName)'").Startname
        }
        catch #[Microsoft.PowerShell.Commands.ServiceCommandException] Cannot find this exception type
        {
            $this.ServiceExists = 'Absent'
        }
        return $this
    }

    # Sets the desired state of the resource.
    [void] Set()
    {
        Write-Verbose "Desired State is: $($this.ServiceName) $($this.Ensure)"
        Write-Verbose "Desired State is: $($this.ServiceName) StartupType $($this.StartupType))"
        Write-Verbose "Desired State is: $($this.ServiceName) RecoveryType $($this.RecoveryType)"
        Write-Verbose "Desired State is: $($this.ServiceName) ServiceUser $($this.ServiceUser)"

        $ServiceInfo = $this.Get()
        if ($ServiceInfo.ServiceCurrentStartupType -ne $this.StartupType)
        {
            Write-Verbose "Setting $($this.ServiceName) StartupType to $($this.StartupType)"
            
            switch ($this.StartupType)
            {
                'Automatic'
                {
                    sc.exe config ($this.ServiceName) start= auto | Write-Verbose
                    break
                }
                'AutomaticDelayed'
                {
                    sc.exe config ($this.ServiceName) start= delayed-auto | Write-Verbose
                    break
                }
                'Disabled'
                {
                    sc.exe config ($this.ServiceName) start= disabled | Write-Verbose
                    break
                }
                'Manual'
                {
                    sc.exe config ($this.ServiceName) start= demand | Write-Verbose
                    break
                }
            }
        }

        if ($ServiceInfo.ServiceCurrentRecoveryType.Where{ $_ -notin $this.RecoveryType } -or $ServiceInfo.ServiceCurrentRecoveryType.Where{ ![string]::IsNullOrWhiteSpace($_) }.count -ne 3)
        {
            Write-Verbose "Setting $($this.ServiceName) RecoveryType to $($this.RecoveryType)"
            
            switch ($this.RecoveryType)
            {   
                'Restart'
                {
                    sc.exe failure ($this.ServiceName) reset= 3600 actions= restart/60000/restart/60000/restart/60000 | Write-Verbose
                    break
                }
                'REBOOT'
                {
                    sc.exe failure ($this.ServiceName) reset= 3600 actions= reboot/60000/reboot/60000/reboot/60000 | Write-Verbose
                    break
                }
            }
        }

        if ($ServiceInfo.ServiceUser -ne $this.Credential.UserName)
        {
            $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($this.ServiceName)'"
            $changeServiceArguments = @{}
            Write-Verbose "Setting Service $($this.Servicename) User from $($ServiceInfo.ServiceUser) to $($this.Credential.UserName)"
            
            $startName = ConvertTo-StartName -Username $this.Credential.UserName
            Write-Verbose "The service startName is $startName"
            Grant-LogOnAsServiceRight -Username $startName
            $changeServiceArguments['StartName'] = $startName
            If($this.Credential.GetNetworkCredential().Password)
            {
                $changeServiceArguments['StartPassword'] = $this.Credential.GetNetworkCredential().Password
            }
            $changeServiceResult = Invoke-CimMethod -InputObject $ServiceCimInstance -MethodName 'Change' -Arguments $changeServiceArguments
            if ($changeServiceResult.ReturnValue -ne 0)
            {
                throw "Service $($this.Servicename) credential change failed with error code $($changeServiceResult.ReturnValue)"
            }
        }
    }
    
    # Tests if the resource is in the desired state.
    [bool] Test()
    {
        $ServiceInfo = $this.Get()
        if ($ServiceInfo.ServiceExists -ne $this.Ensure)
        {
            Write-Verbose "$($this.ServiceName) expects to be $($this.Ensure), But $($ServiceInfo.ServiceExists)"
            return $false
        }

        if ($ServiceInfo.ServiceCurrentStartupType -ne $this.StartupType)
        {
            Write-Verbose "$($this.ServiceName) StartupType expects to be $($this.StartupType), But $($ServiceInfo.ServiceCurrentStartupType)"

            return $false
        }

        if ($ServiceInfo.ServiceCurrentRecoveryType.Where{ $_ -notin $this.RecoveryType } -or $ServiceInfo.ServiceCurrentRecoveryType.Where{ ![string]::IsNullOrWhiteSpace($_) }.count -ne 3)
        {
            Write-Verbose "$($this.ServiceName) RecoveryType expects to be $($this.RecoveryType), But $($ServiceInfo.ServiceCurrentRecoveryType)"

            return $false
        }


        if ($ServiceInfo.ServiceUser -ne $this.Credential.UserName)
        {
            Write-Verbose "$($this.ServiceName) User expects to be $($this.Credential.UserName), But $($ServiceInfo.ServiceUser)"
            return $false
        }

        Write-Verbose "$($this.ServiceName) RecoveryType: $($this.ServiceCurrentRecoveryType):$($this.ServiceCurrentRecoveryType.count), StartupType: $($this.ServiceCurrentStartupType)"
        return $true
    }
}