Active Directory has been one of my favourite tools in which I could automate repetitive processes. Starting from user additions to user deletions, clean-up, audits, all the way to new hire and user account termination process. Active Directory is a fantastic avenue for automation. And, who does not like hyper-efficient Active Directory administrators?

I thought I would start a series on Active Directory automations. Today, we look at adding users to and removing users from Active Directory groups.

As usual, our scripts must have loose coupling. This means no hard-coding anything. Okay, where did that come from? Over the last decade that I have been dealing with scripts, I have seen those that have credentials hard-coded more than I could tolerate.

Never hard-code credentials.

Or any data.

While keeping the code separate from data is the ideal setup, it may not always be possible to do so. In case of scripts what may be acceptable, is write functions for every piece of work that you want the script to do, and then, write a master function that ties them all together. This is the approach I have been using for some time now, and I find it flexible and scalable.

## The problem

Now that we have the couple of pieces of philosophy out of the way, let us look at what we started this post for: Addition or removal of users from AD groups.

Here is what we want:

1. A script that:
1. Searches for users and groups across all domains in the environment
2. Adds a user to the specified groups
3. Tells if the user is already part of a specified group
2. A script that:
1. Searches for users and groups across all domains in the environment
2. Removes a user from the specified groups
3. Tells if the user does not exist in a specified group

## The script

I am not going to keep you waiting for the script. You can also find the script in my GitHub repository.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187  function Find-ADUser { [CmdletBinding()] param ( # User's SAM account name [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [string] $Identity, # Names of domains [Parameter(Position = 1)] [string[]]$Domains = $env:USERDNSDOMAIN ) foreach ($Domain in $Domains) { try { Write-Verbose "Looking for$Identity in $Domain"$UserDetails = Get-ADUser $Identity -Server$Domain -ErrorAction Stop Write-Verbose "Found $Identity in$Domain" break } catch { Write-Verbose "$Identity not found in$Domain" } } $UserDetails } function Find-ADGroup { [CmdletBinding()] param ( # Group's SAM account name [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [string]$Identity, # Names of domains [Parameter(Position = 1)] [string[]] $Domains =$env:USERDNSDOMAIN ) foreach ($Domain in$Domains) { try { Write-Verbose "Looking for $Identity in$Domain" $GroupDetails = Get-ADGroup$Identity -Server $Domain -ErrorAction Stop Write-Verbose "Found$Identity in $Domain" break } catch { Write-Verbose "$Identity not found in $Domain" } }$GroupDetails } function Add-ADUserToGroup { [CmdletBinding()] param ( # User's SAM account name [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [string] $Identity, # Names of groups [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string[]]$GroupName, # Names of domains [Parameter()] [string[]] $Domains =$env:USERDNSDOMAIN, # Credential to use [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) begin { Write-Verbose "Importing the Active Directory module" Import-Module ActiveDirectory -ErrorAction Stop } process { # Find which domain the user is in$UserDetails = Find-ADUser $Identity -Domains$Domains if (-not $UserDetails) { Write-Error "$Identity not found in any of the specified domains" -Category ObjectNotFound -ErrorAction Stop } Write-Verbose "Getting the group membership of $Identity"$UserMembership = ( Get-ADUser $UserDetails -Properties Memberof ).MemberOf foreach ($Group in $GroupName) { Write-Verbose "Processing$Group" if ($UserMembership -match "^CN=$Group,") { Write-Warning "$Identity is already part of$Group" } else { $GroupDn = Find-ADGroup$Group -Domains $Domains if ($GroupDn) { Write-Verbose "Adding $Identity to$($GroupDn.Name)" Add-ADGroupMember$GroupDn -Members $UserDetails -Credential$Credential -ErrorAction Stop } else { Write-Error "Unable to find $($Group)" -ErrorAction Continue } } } } end { Remove-Module ActiveDirectory } } function Remove-ADUserFromGroup { [CmdletBinding()] param ( # User's SAM account name [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [string] $Identity, # Names of groups [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string[]]$GroupName, # Names of domains [Parameter()] [string[]] $Domains =$env:USERDNSDOMAIN, # Credential to use [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) begin { Write-Verbose "Importing the Active Directory module" Import-Module ActiveDirectory -ErrorAction Stop } process { # Find which domain the user is in$UserDetails = Find-ADUser $Identity -Domains$Domains if (-not $UserDetails) { Write-Error "$Identity not found in any of the specified domains" -Category ObjectNotFound -ErrorAction Stop } Write-Verbose "Getting the group membership of $Identity"$UserMembership = (Get-ADUser $UserDetails -Properties Memberof).MemberOf foreach ($Group in $GroupName) { Write-Verbose "Processing$Group" if ($UserMembership -match "^CN=$Group,") { $GroupDn = Find-ADGroup$Group -Domains $Domains if ($GroupDn) { Write-Verbose "Adding $Identity to$($GroupDn.Name)" Remove-ADGroupMember$GroupDn -Members $UserDetails -Credential$Credential -ErrorAction Stop } else { Write-Error "Unable to find $($Group)" -ErrorAction Continue } } else { Write-Warning "$Identity is not part of$Group" } } } end { Remove-Module ActiveDirectory } }

## How to use it

Okay, glad you are back here. If you read the script, you would see that there are four functions in it. If you run the script by hitting F5 or ‘Run with PowerShell’, you will see that nothing happens. Well, welcome to the world of PowerShell!

To see how to run PowerShell scripts, visit this post. In this case, you will have to save the script somewhere on your PC and run:

. '\\path\to\ADUserGroupManipulation.ps1'


This would load the functions into the session (that leading dot is important). Then, you would need to run commands like:

Add-ADUserToGroup -Identity 'JohnDoe' -GroupName 'GroupOne', 'GroupTwo' -Domains 'first.domain.com', 'second.domain.com' -Credential 'DOM\U739937'


Or:

Remove-ADUserFromGroup -Identity 'JohnDoe' -GroupName 'GroupOne', 'GroupTwo' -Domains 'first.domain.com', 'second.domain.com' -Credential 'DOM\U739937'


## How it works

If you look at the script, you will see that it has four functions, which broadly have two actions:

1. Finding the object (user or group)
2. Adding / removing the user to / from the groups

Why have we written four functions for what a single script or function can handle? Loose coupling. You can—when you decide at a later date—reuse these functions without modifications.

The general rule of thumb is to make one function do no more than one task.

### Finding the object

Most environments that I have worked with have more than one domain. In environments with a single domain, finding users should not be an issue at all. But in other environments, the Get-ADUser cmdlet may cause errors, when you do not specify the domain name.

If you know the list of domains to look in, you can handle this with a simple trycatch block. The function loops through the domains and tries to find the user in the domain, using the Get-ADUser cmdlet with the Server parameter. When the execution reaches the catch block, it merely writes a verbose message, after which, it goes to the next item in the loop.

 15 16 17 18 19 20 21 22 23 24 25 26  foreach ($Domain in$Domains) { try { Write-Verbose "Looking for $Identity in$Domain" $UserDetails = Get-ADUser$Identity -Server $Domain -ErrorAction Stop Write-Verbose "Found$Identity in $Domain" break } catch { Write-Verbose "$Identity not found in $Domain" } }$UserDetails

When it finds the user, the operation breaks out of the loop.

 15 16 17 18 19 20 21 22 23 24 25 26  foreach ($Domain in$Domains) { try { Write-Verbose "Looking for $Identity in$Domain" $UserDetails = Get-ADUser$Identity -Server $Domain -ErrorAction Stop Write-Verbose "Found$Identity in $Domain" break } catch { Write-Verbose "$Identity not found in $Domain" } }$UserDetails

This brings us to a caveat:

If a user exists with the same SAM in more than one domain, the script will assume that you are talking about the user object it finds first. To work around this, you can specify the domains in the sequence that you want to run the search in. If you have a group with the same name in more than one domain, this problem gets compounded. Ideally, you should not create groups with the same name in different domains.

Once the function finds the object (user or group), it returns the entire object for use by the calling function. (You need not use the return keyword.)

 15 16 17 18 19 20 21 22 23 24 25 26  foreach ($Domain in$Domains) { try { Write-Verbose "Looking for $Identity in$Domain" $UserDetails = Get-ADUser$Identity -Server $Domain -ErrorAction Stop Write-Verbose "Found$Identity in $Domain" break } catch { Write-Verbose "$Identity not found in $Domain" } }$UserDetails

### Adding or removing the user

One of the requirements is that the script tell us when it finds that the user is already in a particular group in case of user addition, or is not in a particular group in case of removal.

This function first finds the user by calling the Find-ADUser function. The function breaks out of execution if it does not find the user. Yes, you can use an if branch with a positive condition, but I think the negative condition is much more readable.

 92 93 94  if (-not $UserDetails) { Write-Error "$Identity not found in any of the specified domains" -Category ObjectNotFound -ErrorAction Stop }

Next, the function looks for the user membership. Yes, you can incorporate the Properties parameter in the Find- functions, but when you are looking to reuse it, the operation would be unnecessary. I like to keep the functions generic. No harm in performing another query into the Active Directory.

Also, this time, you do not need to use the Server parameter, because the returned object has the information that the Get-ADUser cmdlet needs.

## Summary

Hopefully, this gives you a picture of how you can handle addition or removal of users to or from AD groups. We look at how we can find an AD user or group across more than one domains. We use a modular approach to handle the tasks, and create functions with loose coupling, to enable us to reuse the functions. If you would like to know how modularity dramatically improves scalability, click here to see it in practice.

Of course, we can tune these functions according to the requirements. Every environment is different. Efficiency is all about tuning the script to the environment that it would run in. I have kept this solution as generic as possible.

Still, if you have any questions or have a better way to handle the request, please reach out to me on Twitter and share your thoughts.

Take care.