PowerShell Problem Solver: Active Directory Remote Desktop Settings

During my recent PowerShell workshop in Finland, an attendee asked about Active Directory cmdlets from Microsoft in regards to remote desktop user settings. Although you can readily see the settings in Active Directory Users and Computers, Get-ADUser doesn’t retrieve them. I haven’t worked with Remote Desktop Services in quite a while, but I told him I’d look into this long-standing problem.

Let’s look at the problem. In Active Directory Users and Computers, you might have a setting like this:

Active Directory Users and Computers settings. (Image Credit: Jeff Hicks)
Active Directory Users and Computers settings. (Image Credit: Jeff Hicks)

I don’t have a Remote Desktop server, so I’m just improvising values. You would think that when you run Get-ADUser, you would see these properties somewhere.

Get-ADUser jdemo –properties *

But you don’t. However, if you happen to be using the ActiveRoles cmdlets from Dell (formerly part of Quest Software), then you can easily get these settings.

Using the ActiveRoles cmdlets from Dell. (Image Credit: Jeff Hicks)
Using the ActiveRoles cmdlets from Dell. (Image Credit: Jeff Hicks)

What’s going on here?
After a bit of research, I discovered this is a long-standing issue that seems to originate with a design decision made by the Terminal Services team. Instead of populating the user account with appropriate properties, they elected to store the information in a blob that’s part of the UserParameters property.
The UserParameters property. (Image Credit: Jeff Hicks)
The UserParameters property. (Image Credit: Jeff Hicks)

That’s unfortunate for us or at least those of us who don’t want to have to write complicated code to decode this data. I looked, and I found examples and thought about converting them to PowerShell, but it would be a ridiculously complicated process. Quest’s developers took the time to write the necessary code to extract the information. I don’t have that much time.
Instead, we can revert to some old-school techniques to get this information. This requires traditional LDAP connections to a domain controller. First, we need to create an ADSI object for the user account.

[ADSI]$user = "LDAP://CN=John Demo,OU=Development,DC=Globomantics,DC=Local"

Creating an ADSI object for the user account. (Image Credit: Jeff Hicks)
Creating an ADSI object for the user account. (Image Credit: Jeff Hicks)

By using the COM object, the terminal services properties are easily retrieved.
Retrieving the terminal services properties. (Image Credit: Jeff Hicks)
Retrieving the terminal services properties. (Image Credit: Jeff Hicks)

I can also easily change them.

$user.terminalServicesHomeDirectory = "Y:\RDS"
$user.setinfo()

061815 1516 PowerShellP6
As you can imagine, this would be a lot of work to do manually, so I wrote a function to get remote desktop settings from the ADUC tab that I’ve been showing you.

Function Get-RDUserSetting {
[cmdletbinding(DefaultParameterSetName="SAM")]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter a user's sAMAccountName",
ValueFromPipeline,ParameterSetName="SAM")]
[ValidateNotNullorEmpty()]
[Alias("Name")]
[string]$SAMAccountname,
[Parameter(ParameterSetName="SAM")]
[string]$SearchRoot,
[Parameter(Mandatory,HelpMessage="Enter a user's distingished name",
ValueFromPipelineByPropertyName,ParameterSetName="DN")]
[ValidateNotNullorEmpty()]
[Alias("DN")]
[string]$DistinguishedName,
[string]$Server
)
Begin {
    Write-Verbose "Starting $($MyInvocation.MyCommand)"
    Write-Verbose ($PSBoundParameters | Out-String)
    #remote desktop properties
    $TSSettings = @("TerminalServicesProfilePath","TerminalServicesHomeDirectory","TerminalServicesHomeDrive")
} #Begin
Process {
    Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)"
    Switch ($PSCmdlet.ParameterSetName) {
    "SAM" {
        Write-Verbose "Retrieving distinguishedname for $samAccountname"
        $searcher = New-Object DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectcategory=person)(objectclass=user)(samAccountname=$sAMAccountname))"
        Write-Verbose $searcher.filter
        if ($SearchRoot) {
            Write-Verbose "Searching from $SearchRoot"
            if ($Server) {
                $searchPath = "LDAP://$server/$SearchRoot"
            }
            else {
                $searchPath = "LDAP://$SearchRoot"
            }
            $r = New-Object System.DirectoryServices.DirectoryEntry $SearchPath
            $searcher.SearchRoot = $r
        }
        $user = $searcher.FindOne().GetDirectoryEntry()
    }
    "DN" {
        Write-Verbose "Processing $DistinguishedName"
        if ($server) {
            Write-Verbose "Connecting to $Server"
            [ADSI]$User = "LDAP://$Server/$DistinguishedName"
        }
        else {
            [ADSI]$User = "LDAP://$DistinguishedName"
        }
    }
    } #close Switch
    if ($user.path) {
        #initialize a hashtable
        Try {
        $hash=[ordered]@{
            DistinguishedName = $User.DistinguishedName.Value
            Name = $user.name.Value
            samAccountName = $user.samAccountName.value
            AllowLogon = $user.psbase.InvokeGet("AllowLogon") -as [Boolean]
            }
         foreach ($property in $TSSettings) {
            $hash.Add($property,$user.psbase.invokeGet($property))
        } #foreach
        #create an object
        New-Object -TypeName PSObject -Property $hash
        }
        Catch {
            Write-Warning "Failed to retrieve remote desktop settings for $Distinguishedname. $($_.exception.message)"
        }
    } #if user found
    else {
        Write-Warning "Failed to find user $DistinguishedName. $($_.exception.message)"
    }
} #Process
End {
    Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #End
} #end function


To use the command, you can either specify a samaccountname or a distinguishedname. I wrote it so that you could use it in conjunction with Get-ADUser.

Using the Get-rDUserSetting function in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the Get-rDUserSetting function in Windows PowerShell. (Image Credit: Jeff Hicks)

I always encourage people to write tools that can take advantage of the pipeline.

get-aduser -filter "Name -like 'demouser10*'" | get-rdusersetting | where {-Not $_.AllowLogon}

Using our function in the pipeline. (Image Credit: Jeff Hicks)
Using our function in the pipeline. (Image Credit: Jeff Hicks)

The function doesn’t have much in the way of help, but you can specify a search path if searching by account name. You can also specify a domain controller.
Of course, you need a corresponding command to set these settings.

Function Set-RDUserSetting {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter a user's sAMAccountName",
ValueFromPipeline,ParameterSetName="SAM")]
[ValidateNotNullorEmpty()]
[Alias("Name")]
[string]$SAMAccountname,
[Parameter(ParameterSetName="SAM")]
[string]$SearchRoot,
[Parameter(Mandatory,HelpMessage="Enter a user's distingished name",
ValueFromPipelineByPropertyName,ParameterSetName="DN")]
[ValidateNotNullorEmpty()]
[Alias("DN")]
[string]$DistinguishedName,
[boolean]$AllowLogon,
[Alias("Profile")]
[string]$TerminalServicesProfilePath,
[Alias("HomeDirectory")]
[string]$TerminalServicesHomeDirectory,
[Alias("HomeDrive")]
[string]$TerminalServicesHomeDrive,
[string]$Server,
[switch]$Passthru
)
Begin {
    Write-Verbose "Starting $($MyInvocation.MyCommand)"
    Write-Verbose ($PSBoundParameters | out-string)
    #remote desktop properties
    $TSSettings = @("TerminalServicesProfilePath","TerminalServicesHomeDirectory","TerminalServicesHomeDrive")
} #Begin
Process {
  Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)"
    Switch ($PSCmdlet.ParameterSetName) {
    "SAM" {
        Write-Verbose "Retrieving distinguishedname for $samAccountname"
        $searcher = New-Object DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectcategory=person)(objectclass=user)(samAccountname=$sAMAccountname))"
        Write-Verbose $searcher.filter
        if ($SearchRoot) {
            Write-Verbose "Searching from $SearchRoot"
            if ($Server) {
                $searchPath = "LDAP://$server/$SearchRoot"
            }
            else {
                $searchPath = "LDAP://$SearchRoot"
            }
            $r = New-Object System.DirectoryServices.DirectoryEntry $SearchPath
            $searcher.SearchRoot = $r
        }
        $user = $searcher.FindOne().GetDirectoryEntry()
    }
    "DN" {
        Write-Verbose "Processing $DistinguishedName"
        if ($server) {
            Write-Verbose "Connecting to $Server"
            [ADSI]$User = "LDAP://$Server/$DistinguishedName"
        }
        else {
            [ADSI]$User = "LDAP://$DistinguishedName"
        }
    }
    } #close Switch
    if ($user.path) {
        if ($PSBoundParameters.ContainsKey("AllowLogon")) {
            Write-Verbose "Configuring AllowLogon"
            $user.psbase.invokeSet("AllowLogon",$AllowLogon -as [int])
        }
        foreach ($property in $TSSettings) {
        if ($PSBoundParameters.ContainsKey($property)) {
            Write-Verbose "Setting $property = $($PSBoundParameters[$property])"
            $user.psbase.invokeSet($property,$PSBoundParameters[$property])
        }
        }
        #commit changes
        if ($PSCmdlet.ShouldProcess($DistinguishedName)){
            $user.setInfo()
        } #Whatif
        if ($Passthru) {
           $hash=[ordered]@{
            DistinguishedName = $User.DistinguishedName.Value
            Name = $user.name.Value
            samAccountName = $user.samAccountName.value
            AllowLogon = $user.psbase.InvokeGet("AllowLogon") -as [Boolean]
            }
         foreach ($property in $TSSettings) {
            $hash.Add($property,$user.psbase.InvokeGet($property))
        } #foreach
        #create an object
        New-Object -TypeName PSObject -Property $hash
        }
    } #if user found
    else {
        Write-Warning "Failed to find user $DistinguishedName"
    }
} #Process
End {
    Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #End
} #end function

Normally I’m not a big fan of parameters that take Boolean values, but it was the simplest solution in this case. Now I can easily set remote desktop settings for multiple user accounts.

Get-ADUser -filter "Name -like 'demouser20*'" |
Set-RDUserSetting -AllowLogon $True -TerminalServicesProfilePath "\\CHI-RDS01\Profiles\$($_.samaccountname)" -TerminalServicesHomeDirectory "Y:\RDS" -Passthru

You might also need to configure settings from the Sessions tab. Those settings can be set the same way. You can either modify my functions or write your own. The property names are:

  • MaxConnectionTime
  • MaxDisconnectionTime
  • MaxIdleTime
  • ReconnectionAction
  • BrokenConnectionAction

The time settings are in minutes.

I would recommend putting these functions into a module. This is definitely something you will want to test in a non-production setting.