Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

Cleaning Up User Profiles with Powershell

5.00/5 (1 vote)
9 Mar 2023MIT5 min read 21.4K  
How to clean up user profiles on a computer
In this article, I describe how to clean up user profiles on a computer, when using the GUI is not an option or not desirable.

Introduction

As part of my day job, I manage servers in a controlled environment, which users use via RDP. As a consequence, there is a large amount of user profiles created on each RDP server. Additionally, these servers have a relatively small C disk so the user profiles have an impact on the system. Because of compliance and documentation reasons, moving the user profiles to a different drive is not an option.

The good news is that these user profiles do not contain critical data. So pruning them occasionally does not pose a problem. However, there are quite a lot of them, on quite a lot of servers, so manually doing that is not an option.

This is the perfect situation where scripting is a good idea.

Premises

The script we need has the following requirements:

  1. Certain user profiles shall not be deleted, based on group membership. In this example that means profiles belonging to administrator accounts.
  2. Logged in user profiles shall not be deleted, nor profiles of user accounts marked as 'special' by the operating system such as service accounts.
  3. Only profiles for users that belong to a specific OU will deleted. This is a way to guarantee that the profiles belong to actual users, not service accounts or local accounts.

WMI vs. CIM

Before getting into the nitty gritty, we need to look at the methods we can use to interact with Windows. While we could get rid of profiles by removing the profile folder, this is not a clean operation because it doesn't clean up the registry. So we need to interact with Windows itself and ask Windows to delete the profile for us, similar to what we would do manually via the control panel.

In the old days, WMI would have been the obvious choice. However, WMI is being deprecated. Rather than go into the details myself here, I link to this excellent article which explains the differences and why CIM is the future and WMI the past.

For our purposes, we use CIM because there is a very convenient way to retrieve a list of user profiles via Get-CimObject, and then remove selected user profiles by passing the CIM objects to Remove-CimInstance.

Building the Script

The script is built from various sections.

Setting Up the Prerequisites

In my environment, the exclusion list contains the admin groups as an example. Based on your own needs, you can add specific application groups for accounts that have a need for persistent user profiles.

At some point during the script, we need to evaluate whether the list of groups that the user belongs to, does not contain a group from the exclusion list. A script guru may be able to implement this with a single clear line, but for my own sanity, I put that check in a function to keep the rest of the script comprehensible.

PowerShell
Import-Module ActiveDirectory

function MemberOfExcludedGroup
{
    param ($memberof, $exclusionlist)
    $retval = $false
    $exclusionlist | foreach {if($memberof -match $_ ) {$retval = $true}}
    $retval
}

#if a user belongs to one of these groups, 
#their profile is automatically excluded from removal
$exclusionlist = @("Domain Admins", "Administrators")

The MemberOfExcludedGroup function returns $true if the user is a member of one of the exclusion groups, and $false if it isn't.

Getting the List of Users in Scope

Finding out which users may be in scope of the cleanup process is the first step. We do that with the following code segment. That code segment in English would say 'get all users in the specified OU which are not member of one of the exclusion groups, and get their SID.'

PowerShell
#get the list of actual DeltaV users. 
#We only remove the profile for actal user accounts, not for
#internal service accounts or other accounts not in the DeltaV Users OU
$userOUpath = (Get-ADOrganizationalUnit -Filter 'Name -like "DeltaV Users"').DistinguishedName
$users = Get-ADUser -Filter * -SearchBase $userOUpath  -properties memberof, SID |
    where {! (MemberOfExcludedGroup $_.MemberOf $exclusionlist)} |
    foreach { $_.SID.Value }

At first, it may seem odd that we specify the users in scope through their SID. There is a very good reason for that.

In Step 2, we need to match the user profiles to the users from which they are. In Windows, these are linked through their SID. Sadly, because of how the Local Security Authority (LSA) performs SID lookup, getting the name for a SID is extremely slow; so slow that the script just freezes for a long time, trying to retrieve the names and domain for a SID. We're talking about a half hour or more for a couple hundred users.

PowerShell
#this takes ages to complete
gwmi win32_userprofile |
    foreach {
        ($act = gwmi win32_useraccount -Filter "sid = '$($_.sid)'"); $act.Name;$act.Domain }

However, there is a silver lining here: we don't care about the name.

Rather than translate the SID to a name and then check if the name is a domain users and the SID Domain is the user Domain, we just get all SIDs for the domain users in scope, and check if the profile SID is in that list. This lookup is blazingly fast.

Getting the List of Profiles

Now that we have the list of users that are in scope, we match it against the profiles that are in scope.

PowerShell
Get-CimInstance win32_userprofile |
    where { $users.Contains($_.SID) -and
            !$_.Special -and
            !$_.Loaded } |
            foreach {"Deleting profile $($_.LocalPath)"; Remove-CimInstance $_} 

We get the list of user profiles through WMI and the win32_userprofile class. From that list, we keep only the ones that belong to a user in scope, if said profile is not marked as special or loaded (the user is currently logged in).

The remaining userprofiles are then removed via another WMI function: Remove-CimInstance.

Putting Everything Together

Putting all those things together, the resulting script is surprisingly simple.

PowerShell
Import-Module ActiveDirectory

function MemberOfExcludedGroup
{
    param ($memberof, $exclusionlist)

    $retval = $false
    $exclusionlist | foreach {if($memberof -match $_ ) {$retval = $true}}

    return $retval
}

#if a user belongs to one of these groups, their profile 
#is automatically excluded from removal
$exclusionlist = @("Domain Admins", "Administrators")

#get the list of actual DeltaV users. 
#We only remove the profile for actual user accounts, not for
#internal service accounts or other accounts not in the DeltaV Users OU
$userOUpath = (Get-ADOrganizationalUnit -Filter 'Name -like "DeltaV Users"').DistinguishedName
$users = Get-ADUser -Filter * -SearchBase $userOUpath  -properties memberof, SID |
    where {! (MemberOfExcludedGroup $_.MemberOf $exclusionlist)} |
    foreach { $_.SID.Value }

#Get the full list of profile folders in the C:\Users folder and examine every one
Get-CimInstance win32_userprofile |
    where { $users.Contains($_.SID) -and
            !$_.Special -and
            !$_.Loaded } |
            foreach {"Deleting profile $($_.LocalPath)"; Remove-CimInstance $_} 

Points of Interest

I'm not a script guy. I am a software developer. This means that I am used to variables having concrete types, or functions always having a control path that ends up with a return value and things like that. In scripting, this is a lot less strict. I have no doubt that my background influences the way I approach scripting.

If you're a scripting expert and see something that's wrong / inefficient or whatever, I'd be happy to hear it.

This script is licensed under the MIT license so have fun with it.

History

  • 9th March, 2023: First version

License

This article, along with any associated source code and files, is licensed under The MIT License