Delegating RDP Session Management


In the organization where I work, udalenka is prohibited in principle. It was. Until last week. Now I had to urgently implement the solution. From business - adaptation of processes to a new format of work, from us - PKI with pin codes and tokens, VPN, detailed logging and much more.
Among other things, I was involved in configuring the aka Terminal Services Remote Desktop Infrastructure. We have several RDS deployments in different data centers. One of the tasks was to enable colleagues from related IT departments to connect to user sessions interactively. As you know, there is a standard RDS Shadow mechanism for this and the easiest way to delegate it is to give local administrator rights on RDS servers.
I respect and value my colleagues, but very greedy for the distribution of admin rights. :) Those who agree with me, please, under the cat.

Well, the task is clear, now to the point.

Step 1


Let's create the RDP_Operators security group in Active Directory and include the accounts of those users to whom we want to delegate rights:

$Users = @(
    "UserLogin1",
    "UserLogin2",
    "UserLogin3"
)
$Group = "RDP_Operators"
New-ADGroup -Name $Group -GroupCategory Security -GroupScope DomainLocal
Add-ADGroupMember -Identity $Group -Members $Users

If you have several AD sites, then before proceeding to the next step, you need to wait until it is replicated to all domain controllers. It usually takes no more than 15 minutes.

Step 2


We give the group the right to manage terminal sessions on each of the RDSH servers:

Set-RDSPermissions.ps1
$Group = "RDP_Operators"
$Servers = @(
    "RDSHost01",
    "RDSHost02",
    "RDSHost03"
)
ForEach ($Server in $Servers) {
    #    
    $WMIHandles = Get-WmiObject `
        -Class "Win32_TSPermissionsSetting" `
        -Namespace "root\CIMV2\terminalservices" `
        -ComputerName $Server `
        -Authentication PacketPrivacy `
        -Impersonation Impersonate
    ForEach($WMIHandle in $WMIHandles)
    {
        If ($WMIHandle.TerminalName -eq "RDP-Tcp")
        {
        $retVal = $WMIHandle.AddAccount($Group, 2)
        $opstatus = ""
        If ($retVal.ReturnValue -ne 0) {
            $opstatus = ""
        }
        Write-Host ("      " +
            $Group + "   " + $Server + ": " + $opstatus + "`r`n")
        }
    }
}


Step 3


Add the group to the local Remote Desktop Users group on each of the RDSH servers. If your servers are combined in a collection of sessions, then we do this at the collection level:

$Group = "RDP_Operators"
$CollectionName = "MyRDSCollection"
[String[]]$CurrentCollectionGroups = @(Get-RDSessionCollectionConfiguration -CollectionName $CollectionName -UserGroup).UserGroup
Set-RDSessionCollectionConfiguration -CollectionName $CollectionName -UserGroup ($CurrentCollectionGroups + $Group)

For single servers we use group policy , waiting until it is applied on the servers. Those who are too lazy to wait can force the process using the good old gpupdate, preferably centrally .

Step 4


We’ll prepare the following PS-script for “managers”:

RDSManagement.ps1
$Servers = @(
    "RDSHost01",
    "RDSHost02",
    "RDSHost03"
)

function Invoke-RDPSessionLogoff {
    Param(
        [parameter(Mandatory=$True, Position=0)][String]$ComputerName,
        [parameter(Mandatory=$true, Position=1)][String]$SessionID
    )
    $ErrorActionPreference = "Stop"
    logoff $SessionID /server:$ComputerName /v 2>&1
}

function Invoke-RDPShadowSession {
    Param(
        [parameter(Mandatory=$True, Position=0)][String]$ComputerName,
        [parameter(Mandatory=$true, Position=1)][String]$SessionID
    )
    $ErrorActionPreference = "Stop"
    mstsc /shadow:$SessionID /v:$ComputerName /control 2>&1
}

Function Get-LoggedOnUser {
    Param(
        [parameter(Mandatory=$True, Position=0)][String]$ComputerName="localhost"
    )
    $ErrorActionPreference = "Stop"
    Test-Connection $ComputerName -Count 1 | Out-Null
    quser /server:$ComputerName 2>&1 | Select-Object -Skip 1 | ForEach-Object {
        $CurrentLine = $_.Trim() -Replace "\s+"," " -Split "\s"
        $HashProps = @{
            UserName = $CurrentLine[0]
            ComputerName = $ComputerName
        }
        If ($CurrentLine[2] -eq "Disc") {
            $HashProps.SessionName = $null
            $HashProps.Id = $CurrentLine[1]
            $HashProps.State = $CurrentLine[2]
            $HashProps.IdleTime = $CurrentLine[3]
            $HashProps.LogonTime = $CurrentLine[4..6] -join " "
            $HashProps.LogonTime = $CurrentLine[4..($CurrentLine.GetUpperBound(0))] -join " "
        }
        else {
            $HashProps.SessionName = $CurrentLine[1]
            $HashProps.Id = $CurrentLine[2]
            $HashProps.State = $CurrentLine[3]
            $HashProps.IdleTime = $CurrentLine[4]
            $HashProps.LogonTime = $CurrentLine[5..($CurrentLine.GetUpperBound(0))] -join " "
        }
        New-Object -TypeName PSCustomObject -Property $HashProps |
        Select-Object -Property UserName, ComputerName, SessionName, Id, State, IdleTime, LogonTime
    }
}

$UserLogin = Read-Host -Prompt "  "
Write-Host " RDP-   ..."
$SessionList = @()
ForEach ($Server in $Servers) {
    $TargetSession = $null
    Write-Host "    $Server"
    Try {
        $TargetSession = Get-LoggedOnUser -ComputerName $Server | Where-Object {$_.UserName -eq $UserLogin}
    }
    Catch {
        Write-Host ": " $Error[0].Exception.Message -ForegroundColor Red
        Continue
    }
    If ($TargetSession) {
        Write-Host "       ID $($TargetSession.ID)   $Server" -ForegroundColor Yellow
        Write-Host "      ?"
        Write-Host "      1 -   "
        Write-Host "      2 -  "
        Write-Host "      0 - "
        $Action = Read-Host -Prompt " "
        If ($Action -eq "1") {
            Invoke-RDPShadowSession -ComputerName $Server -SessionID $TargetSession.ID
        }
        ElseIf ($Action -eq "2") {
            Invoke-RDPSessionLogoff -ComputerName $Server -SessionID $TargetSession.ID
        }
        Break
    }
    Else {
        Write-Host "      "
    }
}


To make the PS script convenient to run, we will make a shell for it in the form of a cmd file with the same name as the PS script:

RDSManagement.cmd
@ECHO OFF
powershell -NoLogo -ExecutionPolicy Bypass -File "%~d0%~p0%~n0.ps1" %*


We put both files in a folder that will be available to "managers" and ask them to log in. Now, having launched the cmd-file, they will be able to connect to the sessions of other users in the RDS Shadow mode and force them to log out (it is useful when the user cannot independently end the “hung” session).

It looks something like this:

For the "manager"


For the user


A few comments in the end


Nuance 1 . If the user session to which we are trying to get control was started before the Set-RDSPermissions.ps1 script worked on the server, the “manager” will receive an access error. The solution here is obvious: wait until the managed user logs in.

Nuance 2 . After several days of working with RDP, Shadow noticed an interesting bug or feature: after the completion of the shadow session, the user who was connected to the language bar disappears in the tray and to return it, the user needs to log in. As it turned out, we are not alone: one , two , three .

That's all. I wish you and your servers good health. As always, I look forward to feedback in the comments and ask you to go through a small survey below.

Sources



All Articles