Categories: PowerShell

Removing user from local administrator group based on data stored in Active Directory

Now, and then I get small requests to build seemingly simple PowerShell scripts. This one I had to build had requirements as seen below:

  • Has to work on Windows 7 and Windows 10 (meaning PowerShell 2.0 support – Yikes!)
  • Has to remove a user from the Administrators group on multi-language systems (meaning you can't really use names)
  • Has to remove a user from the Administrators group only if the user is domain user
  • Has to remove a user from the Administrators group only if ExtensionAttribute10 in Active Directory has data in it

Even with PowerShell 5.1 simple task isn't that simple. We need to deal with group names through SIDs rather than names because each group name is different in different languages. The second problem is to distinguish whether a user is a local or domain user. Finally, I need to connect to Active Directory to verify if the user I am about to remove has ExtensionAttribute10 (or any other field in AD) filled in or not. This is a similar topic to my earlier post. Create a local user or administrator account in Windows using PowerShell. In last few weeks I have been exposed to PowerShell 2.0 and well, I don't like it! When you work with PowerShell 5.1 or higher, you're getting spoiled every single day.

While the stuff above is easy if you have Windows 10 with RSAT installed, it's a different game with PowerShell 2.0 and no tools installed to connect to AD. I've created functions below that can help with this small task

function Get-InvariantGroup {
    [CmdletBinding()]
    param(
        [ValidateSet(
            'Access Control Assistance Operators',
            'Administrators'                     ,
            'Backup Operators'                   ,
            'Cryptographic Operators'            ,
            'Device Owners'                      ,
            'Distributed COM Users'              ,
            'Event Log Readers'                  ,
            'Guests'                             ,
            'Hyper-V Administrators'             ,
            'IIS_IUSRS'                          ,
            'Network Configuration Operators'    ,
            'Performance Log Users'              ,
            'Performance Monitor Users'          ,
            'Power Users'                        ,
            'Remote Desktop Users'               ,
            'Remote Management Users'            ,
            'Replicator'                         ,
            'System Managed Accounts Group'      ,
            'Users'
        )] $GroupName
    )
    $GroupSID = @{
        'Access Control Assistance Operators' = 'S-1-5-32-579'
        'Administrators'                      = 'S-1-5-32-544'
        'Backup Operators'                    = 'S-1-5-32-551'
        'Cryptographic Operators'             = 'S-1-5-32-569'
        'Device Owners'                       = 'S-1-5-32-583'
        'Distributed COM Users'               = 'S-1-5-32-562'
        'Event Log Readers'                   = 'S-1-5-32-573'
        'Guests'                              = 'S-1-5-32-546'
        'Hyper-V Administrators'              = 'S-1-5-32-578'
        'IIS_IUSRS'                           = 'S-1-5-32-568'
        'Network Configuration Operators'     = 'S-1-5-32-556'
        'Performance Log Users'               = 'S-1-5-32-559'
        'Performance Monitor Users'           = 'S-1-5-32-558'
        'Power Users'                         = 'S-1-5-32-547'
        'Remote Desktop Users'                = 'S-1-5-32-555'
        'Remote Management Users'             = 'S-1-5-32-580'
        'Replicator'                          = 'S-1-5-32-552'
        'System Managed Accounts Group'       = 'S-1-5-32-581'
        'Users'                               = 'S-1-5-32-545'
    }
    $GroupSID[$GroupName]
}
function Get-LocalUserFromDomain {
    [cmdletbinding()]
    param(
        [string] $NetBiosName,
        [Object] $Member
    )
    $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
    foreach ($Domain in $Forest.Domains) {

        #if ($Domain.Name -eq $DomainName) {
        $Root = $Domain.GetDirectoryEntry()
        $DomainDN = ($Root.distinguishedName)
        # Use the NameTranslate object.
        $objTrans = New-Object -comObject "NameTranslate"
        $objNT = $objTrans.GetType()

        # Invoke the Init method to Initialize NameTranslate by locating
        # the Global Catalog. Note the constant 3 is ADS_NAME_INITTYPE_GC.
        $objNT.InvokeMember("Init", "InvokeMethod", $Null, $objTrans, (3, $Null))

        # Use the Set method to specify the Distinguished Name of the current domain.
        # Note the constant 1 is ADS_NAME_TYPE_1779.
        $objNT.InvokeMember("Set", "InvokeMethod", $Null, $objTrans, (1, "$DomainDN"))

        # Use the Get method to retrieve the NetBIOS name of the current domain.
        # Note the constant 3 is ADS_NAME_TYPE_NT4.
        # The value retrieved includes a trailing backslash.
        $strDomain = $objNT.InvokeMember("Get", "InvokeMethod", $Null, $objTrans, 3)
        $strDomain = $strDomain -replace '\\'
        if ($strDomain -eq $NetBiosName) {
            break
        }
    }
    if ($domainDN) {
        #$domainDN = $domain.GetDirectoryEntry().distinguishedName

        $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$DomainDN")

        # Loop each user
        $Results = ForEach ($Name in $Member) {
            # $Searcher.filter = "(&(objectClass=user)(sAMAccountName= $SAMName))"
            $Searcher.filter = "(&(objectClass=user)(sAMAccountName=$($Name.SamAccountName)))"
            $User = $Searcher.FindOne()
            If ($User) {
                # Create PS Object
                $Object = New-Object PSObject -Property @{
                    Samaccountname      = (($User.Properties).samaccountname | Out-String)
                    Email               = (($User.Properties).mail | Out-String)
                    Mobile              = (($User.Properties).mobile | Out-String)
                    ExtensionAttribute10 = (($User.Properties).extensionattribute10 | Out-String)
                    LocalPath           = $Name.Path
                }
                $Object
            }
            $User
        }
        # Display results
        $Results
    }
}
function Remove-LocalUserFromGroup {
    [CmdletBinding()]
    param(
        [string] $Computer = $Env:ComputerName,
        [ValidateSet(
            'Access Control Assistance Operators',
            'Administrators'                     ,
            'Backup Operators'                   ,
            'Cryptographic Operators'            ,
            'Device Owners'                      ,
            'Distributed COM Users'              ,
            'Event Log Readers'                  ,
            'Guests'                             ,
            'Hyper-V Administrators'             ,
            'IIS_IUSRS'                          ,
            'Network Configuration Operators'    ,
            'Performance Log Users'              ,
            'Performance Monitor Users'          ,
            'Power Users'                        ,
            'Remote Desktop Users'               ,
            'Remote Management Users'            ,
            'Replicator'                         ,
            'System Managed Accounts Group'      ,
            'Users'
        )] $GroupName,
        [Array] $Users
    )
    foreach ($_ in $Users) {
        try {
            $User = $_.LocalPath
            $groupObj = [ADSI]"WinNT://$Computer/$GroupName,group"
            $groupObj.Remove($User)
        } catch {
            $ErrorMessage = $_.Exception.Message -replace [System.Environment]::NewLine
            if ($ErrorMessage -like '*An invalid directory pathname was passed*') {
                # This happens if you run the script more than once, and the object has already been removed from group but still in cache
            } elseif ($ErrorMessage -like '*Access is denied*') {
                Write-Warning "Remove-LocalUserFromGroup - Access denied. Are you running as administrator?"
            } else {
                Write-Warning "Remove-LocalUserFromGroup - $ErrorMessage"
            }
        }
    }
}
function Get-LocalUserFromGroup {
    [CmdletBinding()]
    param(
        [string] $Computer = $Env:ComputerName,
        [ValidateSet(
            'Access Control Assistance Operators',
            'Administrators'                     ,
            'Backup Operators'                   ,
            'Cryptographic Operators'            ,
            'Device Owners'                      ,
            'Distributed COM Users'              ,
            'Event Log Readers'                  ,
            'Guests'                             ,
            'Hyper-V Administrators'             ,
            'IIS_IUSRS'                          ,
            'Network Configuration Operators'    ,
            'Performance Log Users'              ,
            'Performance Monitor Users'          ,
            'Power Users'                        ,
            'Remote Desktop Users'               ,
            'Remote Management Users'            ,
            'Replicator'                         ,
            'System Managed Accounts Group'      ,
            'Users'
        )] $GroupName
    )
    # "Running Add-LocalUserToGroup" | Add-Content -Path 'C:\Temp\ScriptRunning.txt'
    $LocalUsers = Get-WmiObject Win32_UserAccount -Filter "LocalAccount=true"
    if ($LocalUsers) {
        $SID = Get-InvariantGroup -GroupName $GroupName
        $ObjSID = New-Object System.Security.Principal.SecurityIdentifier($SID)
        $GroupProvider = (($ObjSID.Translate([System.Security.Principal.NTAccount]) ).Value).Split("\")[1]
        try {
            #Write-Verbose " security principal: $User to the $GroupProvider group..."
            #"Add-LocalUserToGroup - Adding security principal: $User to the $GroupProvider group..." | Add-Content -Path 'C:\Temp\ScriptRunning.txt'
            $group = [ADSI]"WinNT://$Computer/$GroupProvider,group"
            $Users = $group.Invoke("Members") | ForEach-Object {
                #$_.GetType.Invoke().InvokeMember($null, 'GetProperty', $null, $_, $null)
                $LocalUser = ([ADSI]$_)
                $AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '')
                $IsGroup = ($LocalUser.SchemaClassName -like 'group')
                if (([regex]::Matches($AdsPath, '/')).count -eq 1) {
                    # DOMAIN\user
                    $MemberIsDomain = $True
                    $Name = $AdsPath.Replace('/', '\')
                } else {
                    # DOMAIN\machine\user
                    $MemberIsDomain = $False
                    $Name = $AdsPath.Substring($AdsPath.IndexOf('/') + 1).Replace('/', '\')
                }
                $NETBIOSName = $AdsPath.Split('/')[0]
                $SamAccountName = $Name.Split('\')[1]

                $Member = New-Object PSObject
                $Member | Add-Member Noteproperty 'GroupName' -Value  $GroupProvider
                $Member | Add-Member Noteproperty 'AccountName' -Value $Name
                $Member | Add-Member NoteProperty 'SamAccountName' -Value $SamAccountName
                $Member | Add-Member Noteproperty 'SID' -Value  ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'), 0)).Value)
                $Member | Add-Member Noteproperty 'IsGroup' -Value  $IsGroup
                $Member | Add-Member Noteproperty 'IsDomain' -Value  $MemberIsDomain
                $Member | Add-Member NoteProperty 'NetbiosName' -Value  $NETBIOSName
                $Member | Add-Member NoteProperty 'Path' -Value $LocalUser.Path
                $Member
            }
            foreach ($_ in $Users) {
                if ($_.IsDomain -eq $true -and $_.IsGroup -eq $false) {
                    Get-LocalUserFromDomain -NetBiosName $_.NetbiosName -Member $_
                }
            }


        } Catch {
            $ErrorMessage = $_.Exception.Message
            Write-Verbose $ErrorMessage
            #"Add-LocalUserToGroup - Adding $User to $GroupProvider failed: $ErrorMessage" | Add-Content -Path 'C:\Temp\ScriptRunning.txt'
        }
    } else {
        Write-Warning "Add-LocalUserToGroup - $User doesn't exists. Terminating."
        #"Add-LocalUserToGroup - $User doesn't exists. Terminating." | Add-Content -Path 'C:\Temp\ScriptRunning.txt'
    }
}

After you have those functions, you can do something like code below to remove user based on a non-empty ExtensionAttribute10.

# Code Execution
$DomainUsers = Get-LocalUserFromGroup -GroupName Administrators -Verbose
$UsersToRemove = foreach ($_ in $DomainUsers) {
    if (!$_.ExtensionAttribute10 -eq '') {
        $_
    }
}
Remove-LocalUserFromGroup -Users $UsersToRemove -GroupName Administrators

With small modifications, it's entirely possible to get other fields from AD and do your decisions based on that. With the help of ADSI, you don't need any other tools installed other than PowerShell 2.0. Hope this helps in case you ever get a similar task to do.

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

2 months ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

3 months ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 months ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

12 months ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 year ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 year ago