Group Policy – a Practical Overview

Group Policy is a powerful feature in Microsoft’s Active Directory that allows administrators to centrally manage and configure user and computer settings. This overview will address common misconceptions and provide practical insights into its application.

Active Directory Containers vs. Organizational Units (OUs)

Default AD containers like “Users” and “Computers” cannot be targeted by Group Policy. Creating a company OU structure is preferred as it allows for more granular Group Policy application[1]. OUs are the lowest-level AD containers to which you can assign Group Policy settings[4].

For an example:

_ThisCompany (the _ sorts it to the top, no matter the name)

  • Users
    • Employees
      • Executives
      • Office
      • Remote
    • Vendors
    • Service Accounts
    • Disabled User Accounts
  • Computers
    • Servers
      • RDP Servers
      • Disabled Servers
    • Workstations
      • Shared Workstations
      • Disabled Workstations
  • Security Groups
    • User Groups
    • Computer Groups

Applying Group Policy

Domain-Level Application

Group Policy can be applied to the entire domain by linking it at the domain level. With the default scope of Authenticated Users, this applies to all users and computers in the domain. Please note that “Authenticated Users” is a system group that contains all users and all computers in the whole domain.


Adjusting Scope

To target specific users or computers within the domain:

  1. Remove “Authenticated Users” from the scope.
  2. Add specific security groups for users or computers, or “Domain Computers” or “Domain Users” to the scope.

For example, adding “Executive” group and “Domain Computers” to the scope would apply the policy to any Executive user on any domain computer they log into. Adding “Office” and “RDP Servers” to the scope would apply the policy to users in the Office security group when they are on servers in the RDP Servers group.


OU-Level Application

OUs provide granular control. Policies linked to an OU only apply to objects within that OU, even with a scope of Authenticated Users.

In the above image, the User policies in “Executive Drive Maps” will apply to people in the Executives security group.


Group Policy Objects (GPOs)

GPOs are virtual collections of policy settings stored in the Group Policy Objects container in Group Policy Management Console (GPMC)[1]. They must be linked to OUs or the domain to take effect.

Computer vs. User Configuration

GPOs have two main sections:

  1. Computer Configuration: Applies to computer objects.
  2. User Configuration: Applies to user objects.

If a GPO is linked to an OU containing only computers, the User Configuration settings won’t apply, and vice versa.

Item-Level Targeting

For more granular control within a GPO, Item-Level Targeting can be used on certain policy items, such as mapped drives. This allows for specific settings to apply only to certain users or computers within the scope of the GPO. You could make one Drive Map policy and map the “Public” drive to all users, map “Executive” drive only to the Executives security group, map “Office” drive to only the users in the Office security group and map “RDPDrive” to users in the RDP Users security group when they log in to servers in the RDP Servers security group, for example.


Best Practices and Considerations

  1. Avoid modifying Default Domain Policy or Default Domain Controllers Policy unless necessary. Create separate GPOs for additional settings[5].
  2. Password policy can only be affected by the Default Domain Policy.
  3. Computer reboots or user logoffs may be necessary for new group memberships and certain policies to take effect.
  4. Some policies require computers to be connected to the domain network to apply, which may affect remote VPN users.
  5. New folder redirection policies require a user logoff and logon to apply.
  6. Avoid redirecting the Desktop folder. Redirecting the Desktop can lead to performance issues and a poor user experience, especially for remote users[9].
  7. Always test Group Policy settings before deploying to production. Use a test environment that closely resembles your production environment, including domain controllers, domain members, operating systems, and network configuration[11].
  8. Utilize a testing organizational unit (OU) to evaluate GPOs before deployment. This allows you to verify the effects of new policies without impacting the production environment[14][17].
  9. Take advantage of Group Policy Modeling and Group Policy Results to predict and verify policy application[18].
  10. Be aware of the Group Policy processing order (Local, Site, Domain, OU) and use this knowledge to resolve conflicts between GPOs[16].
  11. When implementing folder redirection, carefully consider permissions. Ensure users have appropriate access to their redirected folders while maintaining security[15].
  12. Use item-level targeting for more granular control over policy application, but be aware that the GPO itself will still appear as applied even if individual items don’t take effect due to targeting[10].
  13. Regularly review and clean up unused or outdated GPOs to maintain an efficient and manageable Group Policy environment[13].

By understanding these concepts and best practices, administrators can effectively use Group Policy to manage and secure their Active Directory environment while avoiding common pitfalls.

Citations:
[1] https://www.techtarget.com/searchwindowsserver/definition/Group-Policy
[2] https://activedirectorypro.com/group-policy-guide/
[3] https://www.ninjaone.com/blog/what-is-group-policy-in-active-directory/
[4] https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/group-policy/group-policy-overview
[5] https://blog.quest.com/what-is-group-policy-and-how-do-gpos-work/
[6] https://blog.netwrix.com/group-policy-management
[7] https://www.windows-active-directory.com/benefits-of-group-policy-in-active-directory.html
[8] https://www.varonis.com/blog/group-policy-objects
[9] https://learn.microsoft.com/en-us/windows-server/storage/folder-redirection/folder-redirection-using-group-policy
[10] https://activedirectorypro.com/group-policy-guide/
[11] https://www.oreilly.com/library/view/microsoft-windows-group/0735622175/ch04s04.html
[12] https://learn.microsoft.com/en-us/windows-server/storage/folder-redirection/deploy-folder-redirection
[13] https://www.globalknowledge.com/us-en/resources/resource-library/articles/in-the-trenches-eight-tips-n-tricks-for-microsoft-windows-group-policy/
[14] https://learn.microsoft.com/en-us/previous-versions/windows/microsoft-desktop-optimization-pack/agpm/use-a-test-environment
[15] https://www.amorales.org/2019/03/folder-redirection-permissions-and-gpo.html
[16] https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn581922(v=ws.11)
[17] https://learn.microsoft.com/en-us/microsoft-desktop-optimization-pack/agpm/test-a-gpo-in-a-separate-organizational-unit-agpm40
[18] https://community.spiceworks.com/t/what-if-with-group-policy-before-rolling-out/804250
[19] https://www.reddit.com/r/sysadmin/comments/chr1wo/gpo_testing_and_deployment_rings/


BONUS:

Create Multiple AD Password Policies

To create multiple password policies in Active Directory (AD), you can use Fine-Grained Password Policies (FGPPs). Here’s a step-by-step guide:

Prerequisites:

  1. Your AD forest functional level must be Windows Server 2008 or higher.
  2. You must be a member of the Domain Admins group or have equivalent permissions.

Create a Fine-Grained Password Policy (FGPP)

  1. Open the Active Directory Administrative Center (ADAC) or use the PowerShell cmdlet dsac.exe.
  2. Navigate to the System container > Password Settings Container.
  3. Right-click and select “New” > “Password Settings” (or use the PowerShell cmdlet New-ADFineGrainedPasswordPolicy).
  4. Enter a name for the policy (e.g., “Admins Policy”) and set the precedence (a lower number indicates higher priority).
  5. Configure the password settings you want to apply, such as:
    • Minimum password length
    • Password history (number of old passwords stored)
    • Maximum password age
    • Minimum password age
    • Lockout threshold and duration
    • Complexity requirements (e.g., uppercase, lowercase, digits, special characters)
  6. Click “OK” to create the policy.

Assign the FGPP to a Security Group

  1. Open the ADAC or use the PowerShell cmdlet dsac.exe.
  2. Navigate to the Security Groups container.
  3. Select the security group you want to assign the policy to (e.g., “Server Admins”).
  4. Right-click and select “Properties” (or use the PowerShell cmdlet Set-ADGroup).
  5. In the “Membership” tab, click “Add” and select the FGPP you created (e.g., “Admins Policy”).
  6. Click “OK” to save the changes.

Verify Policy Application

  1. Use the PowerShell cmdlet Get-ADUserResultantPasswordPolicy to verify which policy applies to a specific user.
  2. Run the command with the user’s name or distinguished name (e.g., Get-ADUserResultantPasswordPolicy -Identity s.wolf).

Additional Tips:

  • You can create multiple FGPPs and assign them to different security groups or OUs.
  • FGPPs override the default domain password policy.
  • Use the “Directly Applies To” setting to specify which users or groups the policy applies to.
  • You can also use Group Policy Objects (GPOs) to apply password policies, but FGPPs provide more granular control.

Remember to test your password policies and verify their application to ensure they meet your organization’s security requirements.

RDP RDWeb Oopsie

I encountered an odd behavior from a recent Gateway RDWeb. I have Remote App and Full Desktop published to RDWeb, but when I went to download the FullDesktop.rdp – it got the same settings as the remote app.

I’ve checked the registry entries and they are correct. Even copied the contents of RDPFileContents entry to notepad and saved as .rdp and it worked great – so its valid…

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\CentralPublishedResources\PublishedFarms\MyCollection\Applications\MyAppName

and

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\CentralPublishedResources\PublishedFarms\MyCollection\RemoteDesktops\MyCollection

The contents have been changed to modify some settings and add the domain name in username section – but again, valid and works. Also changed the Name of each and it shows up in RDWeb properly.

I don’t know HOW RDWeb is pulling the wrong entry for the Desktop, since it SHOULD be pulling it from the Registry.

The ONLY thing I can think of is that my Collection Name and the Remote App name are the same… (above MyCollection and MyAppName are the same) but why would that be a problem?

Apparently, it is. My Collection is “MyAppName” and the published Remote App is “MyAppName” – and Windows Remote Desktop can’t decide which is which in RDWeb – despite the right settings in the registry.

Trusted Domain users to RDP session

Can users from a Trusted Domain authenticate on a Remote Desktop connection in a different, trusting domain? Can it be done? Yes.

So, after some painful back and forth, it can be done, but its not totally secure, and it can be quite the hassle.

  1. Requires Two-Way domain trust, which is not as secure as a one-way, limited trust,
  2. The RDP broker must be able to talk to the Trusted DC and the Trusted DC must be able to talk to the Trusting RDP broker as well as the Trusting DC
  3. Domain Local group on the Trusting domain that has a group from the Trusted domain as a member.
  4. NPS on the Gateway must be set to allow the Trusted domain user group. (May not be required on environments without a gateway)

Assuming you can set up a Two-Way domain trust – maybe even set up as Select – so, not covering that here. On the Trusting domain (the one with the RDP server), you will need to set up a Domain Local security group:

That has the Trusted domain’s group as a member:

On the broker/gateway, in NPS, right click on the NPS (local) and if Register server in Active Directory is bold, click it to register in active directory.

In the Network Policies, RDG_CAP_AllUsers, Conditions, User Groups, you need to add the Trusted domain’s user group and the domain local group (just in case).

In the collection(s), you can add the Domain Local group, or groups, to the User Groups assigned to that collection:

Now, users in the group from the Trusted domain, who are in the Domain Local group, can authenticate to an RDP session in the Trusting domain, as long as the firewall or VPN, etc allows the RDP connection broker to reach the Trusted DC and vice versa.

To sum up:

  • Two-Way Domain Trust: Establish a two-way domain trust between the trusting and trusted domains. This facilitates communication between the RDP broker/gateway and the trusted domain’s domain controller.
  • Communication Channels: Ensure that communication channels are open bidirectionally between the RDP broker/gateway and the Trusted DC, as well as between the Trusted DC and the Trusting RDP broker and DC. Verify there are no network restrictions impeding this communication.
  • Domain Local Group Creation: Create a Domain Local group on the Trusting domain, adding a group from the Trusted domain as a member. This allows users from the Trusted domain to be granted access permissions within the Trusting domain.
  • NPS Configuration: In the Network Policy Server (NPS) on the RDP broker/gateway:
    • Register the server in Active Directory.
    • Configure the RDG_CAP_AllUsers network policy:
      • In Conditions, under User Groups, add the Trusted domain’s user group and the Domain Local group.
      • In collections, assign the Domain Local group(s) to the User Groups associated with the collection.
  • Active Directory Registration: Ensure the RDP broker/gateway is properly registered in Active Directory, which is vital for its integration with NPS.
  • Firewall/VPN Configuration: Confirm that firewall or VPN settings allow the RDP connection broker to communicate with the Trusted DC and vice versa. This ensures seamless authentication for Trusted domain users accessing RDP sessions in the Trusting domain.

By meticulously following these steps, users from the Trusted domain who are members of the Domain Local group can successfully authenticate to RDP sessions in the Trusting domain. However, it’s crucial to monitor and maintain the setup’s security to mitigate potential risks.

Helpful Group Policy Query

So, have you ever been tasked with moving file shares or printer shares to a new server? How enjoyable is it combing through Group Policy to find where the old server name is used? Yeah, its not.

Powershell to the Rescue! The below script will look for any text within all the group policies and let you know which ones the text is found in. A server name is the most obvious thing to look for, but it could be a user name or a share name, etc.

# Get the string to search for
$searchString = Read-Host -Prompt "Enter the string to search for in GPOs"

# Set the domain to search for GPOs
$DomainName = $env:USERDNSDOMAIN

# Check if Group Policy module is available, import if not
if (-not (Get-Module -Name GroupPolicy -ListAvailable)) {
    Import-Module GroupPolicy -ErrorAction Stop
}

# Find all GPOs in the current domain
Write-Host "Finding all the GPOs in $DomainName"

try {
    $allGposInDomain = Get-GPO -All -Domain $DomainName -ErrorAction Stop
} catch {
    Write-Host "Error: Failed to retrieve GPOs. $_" -ForegroundColor Red
    exit
}

$matchedGPOs = @()

# Search through each GPO's XML for the specified string
Write-Host "Starting search..."
foreach ($gpo in $allGposInDomain) {
    try {
        $report = Get-GPOReport -Guid $gpo.Id -ReportType Xml -ErrorAction Stop
    } catch {
        Write-Host "Error: Failed to retrieve report for $($gpo.DisplayName). $_" -ForegroundColor Yellow
        continue
    }
    
    if ($report -match $searchString) {
        Write-Host "Match found in: $($gpo.DisplayName)" -ForegroundColor Green
        $matchedGPOs += $gpo.DisplayName
    } else {
        Write-Host "No match in: $($gpo.DisplayName)"
    }
}

# Display results
Write-Host "`nResults:`n**************" -ForegroundColor Yellow
foreach ($match in $matchedGPOs) {
    Write-Host "Match found in: $($match)" -ForegroundColor Green
}

Looping in Labtech (Connectwise Automate)

I’ve been looking for this article on several occasions, and there it was, in LinkedIn! Re-posting here….

So, you want to do a loop in Labtech.  No problem – as long as you keep a few things in mind.

First, you have to follow the logic!  Use script notes to let yourself know what you want to accomplish.  Also, you need to use Labels and Script GoTo to create the loop. Labels are Script Notes with “:” preceding the label.  Short, simple labels are best.  Know where you want to exit the loop and when to continue the loop.

In the above image, we are taking a “String of Stuff” and splitting it, then parsing the string for each start and taking two characters after that to be a Percent Free.

Note the “SET: @Counter@ = @Counter@ – 1” entry.  This is a Script Math line.  It is, however, not written as shown.  If you put @Counter@ in the variable section, it will use the Value of the variable previously set, so you may wind up with @4@ set to ‘3’ if you try this. 

Despite what the description says, it does matter whether you use @ or not!

In order to do a Loop successfully, you need to make sure that the “Variable” section of each Variable Set, Script Math, etc. that may change value in the loop (or otherwise) does not have the ‘@’ with the variable name.

Users can’t see on RDP Session

Sometimes we have a larger resolution on an RDP server and users complain they can’t see – or, just a couple users complain and everyone else thinks it is fine. Here are some methods to Magnify an RDP session, Make Text Bigger on the RDP session or, increase the scaling directly from the Remote Desktop Session’s .RDP file.

Using Magnifier on RDP session

First, lets change the settings on Magnifier, because it starts at 100% increments and that can just look ugly.  Start button and type “mag” should bring up the following.

There is a drop-down to pick the zoom level increments – pick 10% to start with

The directions on this page tell you how to use it.  (Ignore make everything bigger)

You can now turn on the Magnifier by pressing the Windows logo key on the keyboard, then the Plus Sign.

To turn the magnifier off, hold down the Windows Logo key and press ESC key.

Adjust Font Size on Remote Desktop session:

Click start or the Search in the lower left corner of the screen and type “Make Text Size Bigger” (may not have to type the whole thing)

Click on the Make text size bigger (system settings)

Drag the slider bar to the size you need, then click Apply

Adjusting the Text of the .RDP file

This is the last effort if neither of the above work properly, because it involves editing the .RDP using Notepad++ – or Notepad, if you don’t like better programs.

You will need the .RDP file you use to connect – or you can Save As on the Remote Desktop and save that to the local desktop.

You will need to right click and Edit with Notepad++, or select Open With – then choose Notepad

You may need to choose another app, then More Apps and scroll down to find Notepad, then click OK.

When open in Notepad++ (or Notepad), you will see a lot of text.  Scroll down to the bottom.

Add the following at the bottom:
desktopscalefactor:i:125
devicescalefactor:i:125

You can adjust these numbers, but I think 125% is good to start with.

While this is open, we can also make some performance improvements! Let’s find :

redirectsmartcards:i:1 and change the 1 to a 0 (zero). So it would be:

redirectsmartcards:i:0

And you may want to change the session bpp:i: – I suggest:
session bpp:i:24

Now, save and close Notepad++ (or Notepad).

Use the .RDP file to connect to your session.

Stop and Prevent Office apps from Saving to One Drive by Default

Are you troubled by the way Office M365 apps – Word, Excel, PowerPoint, Outlook – all save to One Drive by default, because that can get pretty annoying.

Fortunately, preventing this behavior is rather simple – and not as extreme as unlinking and uninstalling One Drive – which you may be using for other reasons (or not).

Open Word, then click on Options in the lower left corner

Click on Save in the left menu, then check the Save to Computer by default.

This sets the option for the whole office suite of products, though, I would double check the next time you use them.

Edit Settings for RDP file in RDWeb

RDP has a lot of issues, one is how to get the users to use the right settings for the .RDP file they use to connect. In the past, I’ve downloaded the .rdp from RDWeb, edited it and distributed the file to end-users manually – via email or group policy to put it on their desktop, etc. That was getting old, so now, I have found a way to manage it via the Registry on the RDP connection broker.

One thing I’d like to mention before getting into the code is another registry entry : Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\CentralPublishedResources\PublishedFarms\{TheNameOfYourCollection}\RemoteDesktops\{YourRemoteDesktop} and the entry ShowInPortal – set that to 1 to show, 0 to hide (which 0 is the default).

Now, on to editing the RDPFileContents – oh, you saw that in the image above? Same location in the registry. This entry has the whole RDP file contents in it – new lines and all – that is downloaded from RDWeb links. Sure, you could (carefully) edit the line in the registry – but if you accidentally take out a new line between entries, you can’t put it back in! So, the better way to do it is to download the file from RDWeb, Edit with Notepad++ (or similar, I guess plain ole notepad would work as well).

You can now edit the file – I suggest Session bpp:i:24, redirectsmartcards:i:0 (This can clear up performance issues!), and drivestoredirect:s:C:\; (note the ; after) at a minimum. See the whole list of settings at Microsoft. ( Note the Remote Desktop Services column )

Once you have the settings down – and have tested using the file you just edited – you can use the following Powershell (edit the variables, of course…) Oh, you do need to run this on your RD Web Server, if it is separate from your Gateway, Connection Broker, Host, etc., to set the .rdp file configuration in your RDWeb registry. For RemoteApps, change the RemoteDesktops to RemoteApps in the registry path.

##Powershell to create a multi-line registry entry from a file
#_RDP registry location is : Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\CentralPublishedResources\PublishedFarms
##_Gather the Variable Information
#_The file to import the information from
$FileToImport = "\\Path\to\your\saved\RDP-Desktop.rdp"
#_Set the entry type - for RDPFileContents, this is String.
$RegType = "string"  # For New-ItemProperty, but not used in Set-ItemProperty
#_Set the path to the Key (Folder) level of registry.
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\Test\PublishedFarms\{YourRDPName}\RemoteDesktops\{YourRDPName}"
#_The Registry entry to Edit
$RegEntry = "RDPFileContents"
#_Import the file contents to a variable
$rContents = [system.string]::Join("`r`n",(Get-Content -Path $FileToImport))
# Assign the value
Set-ItemProperty -Path $RegPath -Name $RegEntry -Value $rContents -Force

Now your settings will be downloaded from RDWeb! No more distributing .rdp files with customized settings! With that set up and all user workstations set to not use UDP for RDP sessions, things should run smoothly!

Log off Idle User with Quser and Powershell

I recently had a reason to need a solution to log off idle users from a workstation. “Group Policy!” was my first thought – but, no, the group policy idle logoff is only for Remote Desktop Sessions. Looking online, there were a lot of posts pointing this out – and decrying why Microsoft is so stupid as to not have this available. Its 2022 for crying out loud! I pieced together a script to log off idle users that actually works in a real company environment.

In my searching, however, I did find quser and powershell. You use quser, parse the output and log off idle users over an hour. Easy peasey, right? Nope. Not at all. For one thing, quser has some reality issues. ( I posted asking for help on sorting it out! )

The first thing about the quser output is how to properly put it in a good, powershell readable table. Some have suggested “convertfrom-string” – but that doesn’t handle the “Idle Time” and “Logon Time” very well, due to spaces.

The ‘original’ used :
Select-Object -Skip 1 | ForEach-Object {($_ -replace '\s{2,}', ',').Trim()} | ConvertFrom-Csv -Header $Header |

But that sometimes had issues as well, because if another user is logged on (switch user), the Session is empty, so you have a missing column and everything after ‘session’ gets shifted left by one.

Olaf (on the reality issues link above) helped me see how to use substring() to get what will work most, if not all, of the time. I haven’t seen an issue for it. I guess I may have been just too lazy to count it out. Either, way, it works.

The next thing is that “Idle Time” outputs a lot of different things – it can be ‘none’ or a ‘.’ or a measure of time ‘5’ minutes, ’14:54′ hours and minutes, or ‘1+13:33’ days plus hours and minutes. So, you have to, in a real-world situation, make allowances for such output and do your best to If/Then for all iterations.

The next thing is, that in Windows 10 and below, “Idle Time” doesn’t seem to actually be the individual user’s Idle Time. In testing, it seems more like ‘the time since any user last had a log on event.’
UserName SessionName ID State IdleTime
-------- ----------- -- ----- --------
bkearan console 1 Active 1+03:57
bktest 2 Disc 1+03:57

See how both users have the same ‘Idle Time?’ Even the Active user has an idle time. Now, in Windows 11, it appears to be closer to the user’s idle time (at least on my home, pc – my company laptop is 11, but has the same issue as 10) – but corporate environments haven’t all got to Windows 11. Some places still have Windows 7, but I digress. In Windows 10, you can’t use only idle time to log off a user – as it will log off an active user. … Yes, I got logged off as I was testing this theory. I clicked run in PowerShell ISE and got logged off immediately.

Now for the final kicker – a user is “Active” as long as they are the primary account logged on. Yep, the last logged on user NEVER goes to an Idle state. So far, I haven’t found a way around this last issue. However, if another user logs on after an hour – the Idle Time counter resets and the first user won’t be logged off for another hour.

I have the script set to run as a scheduled task on boot of the computers that need to log off users (so as not to get bogged down with multiple user sessions).

And I just added the Days as I was writing this. Just so much to parse! My Windows 11 laptop has been running the script for 3 days, 4 hours and 41 minutes – and I haven’t been near the laptop for two days, but my logged on user is still showing “Active.”

Then, I started looking for how to get the actual Idle time of the whole system. Now, I have added, as I write this, code to detect the time the system has not received any input whatsoever. So, here is the code that will log off ‘disconnected’ users and even the Console user if the whole system has been idle for over an hour: — and of course that doesn’t work. The Idle Console piece doesn’t get any input when tracking as the SYSTEM account – so logs off everything after an hour – even new logons. So I had to break it up into TWO scripts. The first script (below) is the one to run as a scheduled task on startup, run as SYSTEM.

#
# Complied from pieces of code and additions by Bobby Kearan with inspiration from
# https://www.reddit.com/r/PowerShell/comments/8r56tr/getting_idle_time_for_logged_on_domain_users/
# and Thanks to Olaf (https://forums.powershell.org/u/Olaf) for helping with parsing the output of quser properly


function Log-Message {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $currentSize = 0
    if (Test-Path $Logfile) {
        $currentSize = (Get-Item $Logfile).length/1MB
    }

    if ($currentSize -gt 10) {
        if (Test-Path $oldLogFile) {
            Remove-Item $oldLogFile -Force
        }
        Move-Item $Logfile $oldLogFile -Force
    }

    Add-Content -Path $Logfile -Value ("[" + (Get-Date) + "] " + $Message)
}

function Get-QUserInfo {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string[]]
        $ComputerName = $env:COMPUTERNAME
    )

    begin
        {
        $Header = 'UserName','SessionName','ID','State','IdleTime','LogonTime'
        $No_Connection = '-- No Connection --'
        }

    process
        {
        foreach ($CN_Item in $ComputerName)
            {
            if (Test-Connection -ComputerName $CN_Item -Count 1 -Quiet)
                {
                $QuserOutput = quser /server:$CN_Item
                $QuserOutput -split "`n" | Select-Object -Skip 3
                $QuserOutput | Select-Object -Skip 1 |
                    ForEach-Object {
                    
						$IdleTime = $_.Substring(54, 11).trim()
						#Write-Host $IdleTime
						If (($IdleTime -eq 'none') -or ($IdleTime -eq '.') -or ($IdleTime -eq '$Null')) {
						$IdleTime = "Not Idle"
						}
						#Write-Host "Testing Idle Time " $IdleTime "for the + sign"
						IF ($IdleTime -like '*+*') {
                        $IdleTime = $IdleTime.replace("+",":")
                        #Write-Host $IdleTime " was the Plus sign replaced + ? "
						}
              
					If ($IdleTime -as [DateTime]) {
                        If ($IdleTime -match "\d\d\:\d\d"){
							#$IdleTime = [timespan]$IdleTime
							$Idleness = $IdleTime -split ":"
                        #Write-Host $Idleness.length
							If($Idleness.length -eq 2){
							$IdleTime = New-Object -TypeName PSObject
							$d = [ordered]@{Days=0;Hours=$Idleness[0];Minutes=$Idleness[1]}
							$IdleTime | Add-Member -NotePropertyMembers $d -TypeName Asset
							}
                        Else {
							$IdleTime = New-Object -TypeName PSObject
							$d = [ordered]@{Days=$Idleness[0];Hours=$Idleness[1];Minutes=$Idleness[2]}
							$IdleTime | Add-Member -NotePropertyMembers $d -TypeName Asset
							}
                        #Write-Host $Idleness[0] " = " $IdleTime.Hours
                        }
                        ELSE {
                          $IdleTime = New-TimeSpan -Start $IdleTime -End (Get-Date)
                          #Write-Host "IdleTime was a datetime-" $IdleTime
                          }
                       }
					   #Write-Host $IdleTime
						$final = $_.length - 65
						$Username = $_.Substring(1, 22).trim()
                    
						[PSCustomObject]@{
						UserName    = $_.Substring(1, 22).trim()
						SessionName = $_.Substring(23, 19).trim()
						ID          = $_.Substring(42, 3).trim()
						State       = $_.Substring(46, 8).trim()
						IdleTime    = $IdleTime
						LogonTime   = $_.Substring(65, $final).trim()
                        }
					}
                }
                else
                {
                [PSCustomObject]@{
                    ComputerName = $CN_Item
                    UserName = $No_Connection
                    SessionName = $No_Connection
                    ID = $No_Connection
                    State = $No_Connection
                    IdleTime = $No_Connection
                    LogonTime = $No_Connection
                    }                
                }
            } # end >> foreach ($CN_Item in $ComputerName)
        } # end >> process {}

    end {}

    } # end >> function Get-QUserInfo

    #$Info = Get-QUserInfo -ComputerName localhost

    $Logfile = "c:\scripts\LogOff.log"
    $oldLogFile = "c:\scripts\Logoff.old.log"
    #Initalize Log file
    Log-Message "Log Start: " + (Get-Date)
    

    $state = "Active"
    while ($state -eq "Active") {
    $Info = Get-QUserInfo -ComputerName LocalHost
    foreach ($user in $Info) {
        if ($user.State.Trim() -ne "Active" -and $user.IdleTime.Hours -ge 1) {
            logoff $user.ID
            Log-Message "$($user.State) User $($user.UserName) has been idle $($user.IdleTime.Days) Days $($user.IdleTime.Hours) hours, $($user.IdleTime.Minutes) Minutes - Logged On Time = $($user.IdleTime)"
        }
    }
    Start-Sleep -Seconds 260
    }

I did add a couple things after posting. That would be adding in self-cleaning Logging. Now, the second one, runs as a scheduled task as well, but on logon of any user, and it runs in the console session.

# Written by Bobby Kearan with inspiration and code from: 
# https://www.reddit.com/r/PowerShell/comments/8r56tr/getting_idle_time_for_logged_on_domain_users/
# and, finally, https://stackoverflow.com/questions/15845508/get-idle-time-of-machine
#
Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PInvoke.Win32 {

    public static class UserInput {

        [DllImport("user32.dll", SetLastError=false)]
        private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

        [StructLayout(LayoutKind.Sequential)]
        private struct LASTINPUTINFO {
            public uint cbSize;
            public int dwTime;
        }

        public static DateTime LastInput {
            get {
                DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
                DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
                return lastInput;
            }
        }

        public static TimeSpan IdleTime {
            get {
                return DateTime.UtcNow.Subtract(LastInput);
            }
        }

        public static int LastInputTicks {
            get {
                LASTINPUTINFO lii = new LASTINPUTINFO();
                lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
                GetLastInputInfo(ref lii);
                return lii.dwTime;
            }
        }
    }
}
'@

function Log-Message {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $currentSize = 0
    if (Test-Path $Logfile) {
        $currentSize = (Get-Item $Logfile).length / 1MB
    }

    if ($currentSize -gt 5) {
        if (Test-Path $oldLogFile) {
            Remove-Item $oldLogFile -Force
        }
        Move-Item $Logfile $oldLogFile -Force
    }

    Add-Content -Path $Logfile -Value ("[" + (Get-Date) + "] " + $Message)
}

$Logfile = "c:\scripts\LogOff-console.log"
$oldLogFile = "c:\scripts\Logoff-console.old.log"

Log-Message "Log Start"

$state = "Active"

while ($state -eq "Active") {
    $idleTime = [PInvoke.Win32.UserInput]::IdleTime

    if ($idleTime.Hours -ge 1) {
        $sessionid = (Get-Process -PID $pid).SessionID
        Log-Message "$env:USERNAME has been idle for $($idleTime.Hours) hours and $($idleTime.Minutes) minutes."
        logoff $sessionid
    }

    Start-Sleep -Seconds 260
}

To assist with this, I wrote this powershell to create the scheduled tasks (mostly) – it launches task scheduler so you can check and verify.

# Check if the files are in C:\scripts
$scriptPath = "C:\scripts"
$files = @("Logoffonidle.ps1", "LogoffOnIdle_Console.ps1")

foreach ($file in $files) {
    if (-not (Test-Path "$scriptPath\$file")) {
        Write-Output "Error: $file is not present in $scriptPath"
        exit
    }
}


# Create or Update scheduled tasks
$TaskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -WakeToRun
$TaskSettings.ExecutionTimeLimit = 'PT0S'
$TaskSettings.Compatibility = 'win8'

$actionStartup = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $scriptPath\Logoffonidle.ps1"
$triggerStartup = New-ScheduledTaskTrigger -AtStartup

$taskNameStartup = "LogOffOnIdleAtStartup"
if (Get-ScheduledTask -TaskName $taskNameStartup -ErrorAction SilentlyContinue) {
    Set-ScheduledTask -TaskName $taskNameStartup -Action $actionStartup -Trigger $triggerStartup -Settings $TaskSettings
} else {
    Register-ScheduledTask -Action $actionStartup -Trigger $triggerStartup -TaskName $taskNameStartup -User "SYSTEM" -RunLevel Highest -Settings $TaskSettings
}

$actionIdle = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $scriptPath\LogoffOnIdle_Console.ps1"
$triggerIdle = New-ScheduledTaskTrigger -AtLogOn

$taskNameIdle = "LogOffOnIdleWhenUserIdle"
$principalIdle = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\INTERACTIVE" -RunLevel Limited
if (Get-ScheduledTask -TaskName $taskNameIdle -ErrorAction SilentlyContinue) {
    Set-ScheduledTask -TaskName $taskNameIdle -Action $actionIdle -Trigger $triggerIdle -Principal $principalIdle -Settings $TaskSettings
} else {
    # Register for any user that logs on
    Register-ScheduledTask -Action $actionIdle -Trigger $triggerIdle -TaskName $taskNameIdle -Principal $principalIdle -Settings $TaskSettings
}

taskschd.msc

This should be working for both disconnected and console sessions now.

Run the script and make sure it creates both Scheduled Tasks.

Open the LogOffOnIdleAtStartup scheduled task and make sure it is set the Configure For: to the latest system there is available

Now open the LogOffOnIdleWhenUserIdle scheduled task and make sure it is set for: to the latest system there is – AND  the User is set to Interactive (or “NT AUTHORITY\INTERACTIVE”) and Run Only when user is logged on.

Edit each task to Uncheck the Stop the task if it runs more than 3 days.  We don’t want these tasks to stop.

Run task as soon as possible after scheduled start is missed.  Make the other show changes as needed:

The script creates a log of every logoff event it does in c:\scripts\logoff.log

If you need to keep a system alive when important tasks are running, add to the top section of LogoffIdle:

# Important processes that should prevent logging off
$KeepAliveTasks = @("robocopy", "ping", "Syncing")

Then, adjust the While loop:

while ($state -eq "Active") {
    $idleTime = [PInvoke.Win32.UserInput]::IdleTime

    $importantProcessRunning = Get-Process | Where-Object {
        $_.Name -in $KeepAliveTasks -or 
        $KeepAliveTasks -contains $_.MainWindowTitle
    }

    if ($idleTime.Hours -ge 1 -and (-not $importantProcessRunning)) {
        Log-Message "User $muser has been idle for $($idleTime.Hours) hours and $($idleTime.Minutes) minutes. Attempting logoff."
        
        # Trying a direct PowerShell approach for logoff
        $sessionid = (Get-Process -PID $pid).SessionID
        logoff $sessionid
    }

    Start-Sleep -Seconds 260
}

DFSRMig stuck at Eliminating

Moving a client from Server 2008 to Server 2019 Domain Controller. Which means adding the 2019 server to the domain as a domain controller, promoting it, etc. But the first step is to do DFSRMig – migrate from FRS to DFSR for Active Directory.

DFSRMig – Run this on the old DC

1# To see what state DFS is in
DFSRMig /GetGlobalState

2# Start the migration to DFSR with
DFSRMig /SetGlobalState 1

3# To check on the migration progress
DFSRMig /GetMigrationState – This will take some time: Run this command until it is in a consistent state.

4# Next step is to Redirect
DFSRMig /SetGlobalState 2

5# When that process is complete (check using /GetMigrationState), move to 3
DFSRMig /SetGlobalState 3

The idea is to get to “Eliminated” state – so you can promote the 2019 server to a DC. Well, I found out that when you get it to “Eliminating” – just after running Set Global State 3 – you can go ahead and promote. However… the old DC might get stuck in eliminating.

Now, the interwebs have plenty of suggestions and those should work in most cases – not mine, but most. I was completely baffled… but was restarting the service and looking at event logs… and seeing “access denied” messages. Which was totally weird. I eventually put 7 and 10 together and got 42…

I opened ADSIEdit (as admin) and went to Domain Controllers, expanded those…

After NTFSR is eliminated…

And clicked on the Domain Controller names that was there and went to security… at first, nothing looked off… but eventually, I saw that somebody(!!) had set a Deny on the EVERYONE group so that nobody could delete anything under there – including the NTFSR (NTFRS?) folders! So, of course, it couldn’t eliminate it – access was, in fact, denied. I removed the deny from a couple ‘delete’ permissions and stopped the DFSR service and started it again – Boom! Eliminated!