Public/Services/Set-ServicePermission.ps1

function Set-ServicePermission {

    <#
        .SYNOPSIS
            Sets specific permissions for a specified service on a local or remote computer.

        .DESCRIPTION
            This function allows you to grant permissions (FullControl, ReadAndExecute, Read, Write, Start, or Stop)
            for a specified service on a local or remote computer to a specified user or group.

        .PARAMETER ServiceName
            The name of the service to modify permissions for. Accepts multiple service names.

        .PARAMETER ComputerName
            The name of the remote computer on which the service resides. Defaults to the local computer.

        .PARAMETER Group
            The group for whom to set the specified permissions.

        .PARAMETER Permission
            The level of permission to set for the specified user.
            Valid values are 'FullControl', 'ReadAndExecute', 'Read', 'Write', 'Start', and 'Stop'.

        .PARAMETER PassThru
            If specified, returns the service object with the updated permissions.

        .INPUTS
            System.String, System.String[]

        .OUTPUTS
            Win32_Service (if -PassThru is specified)

        .EXAMPLE
            Set-ServicePermission -ServiceName 'wuauserv' -Username 'EguibarIT\davade' -Permission 'FullControl' -Verbose

            Sets FullControl permissions for 'EguibarIT\davade' on the 'wuauserv' service with verbose output.

        .EXAMPLE
            $Splat = @{
                ServiceName = 'BITS'
                ComputerName = 'DC1'
                Group = 'Yoda_T0'
                Permission = 'FullControl'
                PassThru = $True
                Verbose = $true
            }
            $CurrentDACL = Set-ServicePermission @Splat

            Using splatting, Sets FullControl permissions for 'EguibarIT\Yoda_TO' on the 'BITS' service
            of DC1 remote computer with verbose output. Results are stored on $CurrentDACL variable

        .NOTES
            Version: 1.0
            DateModified: 28-Oct-2024
            LasModifiedBy: Vicente R. Eguibar
                vicente@EguibarIT.com
                http://www.eguibarit.com

        .NOTES
            Used Functions:
                Name | Module
                -------------------------------|--------------------------
                Get-AdObjectType | EguibarIT & EguibarIT.DelegationPS
                New-Object | Microsoft.PowerShell.Utility
                New-CimSession | CimCmdlets
                Invoke-CimMethod | CimCmdlets
                Get-CimInstance | CimCmdlets
                New-CimInstance | CimCmdlets
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]

    param(

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Service name (or array of service names) to which the permission will be set.',
            Position = 0)]
        [string[]]
        $ServiceName,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Remote computer to execute the commands.',
            Position = 1)]
        [PSDefaultValue(Help = 'Default Value is "$env:COMPUTERNAME"')]
        [Alias('Host', 'PC', 'Server', 'HostName', 'Computer')]
        $ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Identity of the group getting the delegation, usually a DomainLocal group.',
            Position = 2)]
        [ValidateNotNullOrEmpty()]
        [Alias('IdentityReference', 'Identity', 'Trustee', 'GroupID')]
        $Group,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Permission to be configured on the service.',
            Position = 3)]
        [PSDefaultValue(Help = 'Default Value is "Read"')]
        [ValidateSet('FullControl', 'ReadAndExecute', 'Read', 'Write', 'Start', 'Stop')]
        [string]
        $Permission = 'Read',

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Return the permission after set.',
            Position = 4)]
        [switch]
        $PassThru

    )

    Begin {

        Set-StrictMode -Version Latest

        # Display function header if variables exist
        if ($null -ne $Variables -and $null -ne $Variables.HeaderDelegation) {
            $txt = ($Variables.HeaderDelegation -f
            (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
            (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end if

        ##############################
        # Module imports


        ##############################
        # Variables Definition

        # Define access rights as enum for better readability
        enum ServiceAccessRights {
            SERVICE_QUERY_CONFIG = 0x0001
            SERVICE_CHANGE_CONFIG = 0x0002
            SERVICE_QUERY_STATUS = 0x0004
            SERVICE_ENUMERATE_DEPENDENTS = 0x0008
            SERVICE_START = 0x0010
            SERVICE_STOP = 0x0020
            SERVICE_PAUSE_CONTINUE = 0x0040
            SERVICE_INTERROGATE = 0x0080
            SERVICE_USER_DEFINED_CONTROL = 0x0100
            READ_CONTROL = 0x00020000
            WRITE_DAC = 0x00040000
            WRITE_OWNER = 0x00080000
            DELETE = 0x00010000
        }

        # Map permissions to access rights using enum values
        $permissionMap = @{
            'FullControl'    = [int]([ServiceAccessRights]::SERVICE_QUERY_CONFIG -bor
                [ServiceAccessRights]::SERVICE_CHANGE_CONFIG -bor
                [ServiceAccessRights]::SERVICE_QUERY_STATUS -bor
                [ServiceAccessRights]::SERVICE_ENUMERATE_DEPENDENTS -bor
                [ServiceAccessRights]::SERVICE_START -bor
                [ServiceAccessRights]::SERVICE_STOP -bor
                [ServiceAccessRights]::SERVICE_PAUSE_CONTINUE -bor
                [ServiceAccessRights]::SERVICE_INTERROGATE -bor
                [ServiceAccessRights]::SERVICE_USER_DEFINED_CONTROL -bor
                [ServiceAccessRights]::READ_CONTROL -bor
                [ServiceAccessRights]::WRITE_DAC -bor
                [ServiceAccessRights]::WRITE_OWNER -bor
                [ServiceAccessRights]::DELETE)
            'ReadAndExecute' = [int]([ServiceAccessRights]::SERVICE_QUERY_CONFIG -bor
                [ServiceAccessRights]::SERVICE_QUERY_STATUS -bor
                [ServiceAccessRights]::SERVICE_ENUMERATE_DEPENDENTS -bor
                [ServiceAccessRights]::SERVICE_START -bor
                [ServiceAccessRights]::SERVICE_STOP -bor
                [ServiceAccessRights]::READ_CONTROL)
            'Read'           = [int]([ServiceAccessRights]::SERVICE_QUERY_CONFIG -bor
                [ServiceAccessRights]::SERVICE_QUERY_STATUS -bor
                [ServiceAccessRights]::SERVICE_ENUMERATE_DEPENDENTS -bor
                [ServiceAccessRights]::READ_CONTROL)
            'Write'          = [int][ServiceAccessRights]::SERVICE_CHANGE_CONFIG
            'Start'          = [int][ServiceAccessRights]::SERVICE_START
            'Stop'           = [int][ServiceAccessRights]::SERVICE_STOP
        }


        # Check for admin privileges
        $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
        if (-not $isAdmin) {
            #throw 'This function requires administrative privileges. Please run PowerShell as Administrator.'
        }


        # Create CIM session if remote computer
        if ($ComputerName -ne $env:COMPUTERNAME) {

            $Computer = Get-AdObjectType -Identity $PSBoundParameters['ComputerName']

            try {
                $script:cimSession = New-CimSession -ComputerName $Computer.Name -ErrorAction Stop
                $script:cimParams = @{ CimSession = $cimSession }
            } catch {
                throw "Failed to create CIM session to '$ComputerName'. Error: $_"
            } #end Try-Catch
        } else {
            $script:cimParams = @{}
        } #end If-Else

        $Identity = Get-AdObjectType -Identity $PSBoundParameters['Group']

    } #end Begin

    Process {

        # Iterate all services
        foreach ($service in $ServiceName) {
            try {
                Write-Verbose "Processing service: $service"

                # Get the service
                $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$service'" @cimParams
                if (-not $svc) {
                    Write-Error "Service '$service' not found"
                    continue
                } #end If

                # Get current security descriptor
                $getSDResult = Invoke-CimMethod -InputObject $svc -MethodName GetSecurityDescriptor @cimParams
                $currentDACL = $getSDResult.Descriptor.DACL

                if ($getSDResult.ReturnValue -ne 0) {
                    Write-Error -Message ('
                        Failed to get security descriptor for {0}.
                        Return value: {1}'
 -f $service, $getSDResult.ReturnValue
                    )
                    continue
                } else {
                    Write-Verbose -Message ('Got security descriptor for {0}.' -f $service)
                } #end If-Else


                # Get the current Owner and Group
                $currentOwner = $getSDResult.Descriptor.Owner
                $currentGroup = $getSDResult.Descriptor.Group

                # Create new Owner Trustee if not present
                if (-not $currentOwner) {
                    $ownerTrusteeArgs = @{
                        ClassName = 'Win32_Trustee'
                        Namespace = 'root/cimv2'
                        Property  = @{
                            Domain    = 'NT AUTHORITY'
                            Name      = 'SYSTEM'
                            SIDString = 'S-1-5-18'
                        }
                    }
                    $currentOwner = New-CimInstance -ClientOnly @ownerTrusteeArgs
                }

                # Create new Group Trustee if not present
                if (-not $currentGroup) {
                    $groupTrusteeArgs = @{
                        ClassName = 'Win32_Trustee'
                        Namespace = 'root/cimv2'
                        Property  = @{
                            Domain    = 'BUILTIN'
                            Name      = 'Administrators'
                            SIDString = 'S-1-5-32-544'
                        }
                    }
                    $currentGroup = New-CimInstance -ClientOnly @groupTrusteeArgs
                }

                # Create the new Trustee object for the user
                $trusteeArgs = @{
                    ClassName = 'Win32_Trustee'
                    Namespace = 'root/cimv2'
                    Property  = @{
                        Name      = $Identity.SamAccountName
                        SIDString = ([System.Security.Principal.SecurityIdentifier]::New($Identity.SID.Value)).Value
                        Domain    = $env:USERDNSDOMAIN
                    }
                }
                $trustee = New-CimInstance -ClientOnly @trusteeArgs
                Write-Verbose -Message ('Created trustee: {0}' -f ($trustee | Out-String))

                # Define new ACE (Access Control Entry)
                $aceArgs = @{
                    ClassName = 'Win32_Ace'
                    Namespace = 'root/cimv2'
                    Property  = @{
                        AccessMask = [UInt32]$permissionMap[$Permission]
                        AceFlags   = [UInt32]0  # Adjusted for default case, change if needed
                        AceType    = [UInt32]0  # ACCESS_ALLOWED_ACE_TYPE
                        Trustee    = $trustee
                    }
                }
                $ace = New-CimInstance -ClientOnly @aceArgs
                Write-Verbose -Message ('Created ACE: {0}' -f ($ace | Out-String))

                # Add new ACE to the DACL
                $newDACL = @($currentDACL + $ace)

                # Convert to CIM-compatible ACE instances
                $cimDACL = foreach ($aceEntry in $newDACL) {
                    New-CimInstance -ClientOnly -Namespace 'root/cimv2' -ClassName 'Win32_Ace' -Property @{
                        AccessMask = [UInt32]$aceEntry.AccessMask
                        AceFlags   = [UInt32]$aceEntry.AceFlags
                        AceType    = [UInt32]$aceEntry.AceType
                        Trustee    = $aceEntry.Trustee
                    }
                }

                # Create the new security descriptor with updated DACL
                $newSDArgs = @{
                    ControlFlags = [UInt32]0x0004  # SE_DACL_PRESENT
                    Owner        = $currentOwner
                    Group        = $currentGroup
                    DACL         = [CimInstance[]]$cimDACL
                }
                $Splat = @{
                    ClientOnly = $true
                    Namespace  = 'root/cimv2'
                    ClassName  = 'Win32_SecurityDescriptor'
                    Property   = $newSDArgs
                }
                $newSD = New-CimInstance @Splat


                # Apply the updated security descriptor
                if ($PSCmdlet.ShouldProcess($service, "Add permission $Permission for $Groupe")) {

                    Write-Verbose -Message ('Setting security descriptor for service {0}...' -f $service)
                    Write-Verbose -Message ('Owner: {0}' -f $currentOwner.Name)
                    Write-Verbose -Message ('Group: {0}' -f $currentGroup.Name)
                    Write-Verbose -Message ('Number of ACEs: {0}' -f $newAces.Count)

                    $Splat = @{
                        InputObject = $svc
                        MethodName  = 'SetSecurityDescriptor'
                        Arguments   = @{ Descriptor = $newSD }
                    }
                    $setSDResult = Invoke-CimMethod @Splat @cimParams

                    if ($setSDResult.ReturnValue -ne 0) {
                        Write-Error "Failed to set security descriptor for '$service'. Return value: $($setSDResult.ReturnValue)"
                        continue
                    }

                    Write-Verbose -Message ('
                        Successfully added {0} permission
                        for $Username on service {1}'
 -f $Permission, $service
                    )

                    if ($PassThru) {
                        Get-CimInstance -ClassName Win32_Service -Filter "Name='$service'" @cimParams
                    }
                }
            } catch {
                Write-Error -Message ('Error processing service {0}: {1}' -f $service, $_)
            }
        } #end Foreach Service
    } #end Process

    End {
        $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName,
            'setting permissions on service.'
        )
        Write-Verbose -Message $txt

        if ($script:cimSession) {
            Remove-CimSession -CimSession $script:cimSession
        }
    } #end End
} #end Function