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
}

Readability is important.

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.

96
97
98
99
Write-Verbose "Getting the group membership of $Identity"
$UserMembership = (
   Get-ADUser $UserDetails -Properties Memberof
).MemberOf

The function then loops through each of the specified groups. It checks if the user is already part of the group, and if they are, it shows a warning. If not, it tries to find the group in each of the specified domains. Once found, the function adds the user to the group. This is where it uses the credentials.

103
104
105
106
107
108
109
110
111
112
113
114
115
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
      }
}

A note on credentials

No, I have already said you should not hard-code credentials in a script.

When you make a function accept the credential as a parameter, you must remember the following:

  1. The credential should be a credential object. We do this by specifying the type as [System.Management.Automation.PSCredential].
  2. We must check if the user passes a null value. We do this using [ValidateNotNull()]. When you do that, the user will get an error when they try to pass a null value.
  3. If we want to be able to pass in the username and make the function prompt for a password, we specify the type also as [System.Management.Automation.Credential()].
  4. And in the end, we also want to specify the default value as Empty, so that you avoid the error caused by [ValidateNotNull()].
143
144
145
146
147
[Parameter()]
[ValidateNotNull()]
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential = [System.Management.Automation.PSCredential]::Empty

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.