.NET Assemblies In PowerShell – Part 2: Manage Active Directory group members and user accounts

In the first part of this series, I described how you can add and remove members to and from Active Directory groups in PowerShell, without using the ActiveDirectory module, but just by using the .Net namespace System.DirectoryServices.AccountManagement.

In this part I want to show how you can work with the members of a group, as well as with Active Directory accounts in general using this namespace in PowerShell. For the prerequisites, please check out the first part of this series.

The members property

In the last article, we created a GroupPrincipal object for an Active Directory group. One cool thing about this object type: you can easily work with any of the group members by accessing the Members property (type PrincipalCollection) of the group object.

By calling the Members property, a list of all group members will be shown, in which any group member is presented as a PrincipalContext object.

In the following example, we’re accessing the Members property of a group that contains two user accounts.

PS C:\Users> $group.Members


GivenName                         : Test
MiddleName                        : 
Surname                           : 001
EmailAddress                      : Test.001@lab.com
VoiceTelephoneNumber              : 
EmployeeId                        : 
AdvancedSearchFilter              : System.DirectoryServices.AccountManagement.AdvancedFilters
Enabled                           : True
AccountLockoutTime                : 
LastLogon                         : 02.05.2018 11:52:56
PermittedWorkstations             : {}
PermittedLogonTimes               : {255, 255, 255, 255...}
AccountExpirationDate             : 
SmartcardLogonRequired            : False
DelegationPermitted               : True
BadLogonCount                     : 0
HomeDirectory                     : \\FileServer01\TEST001$
HomeDrive                         : H:
ScriptPath                        : 
LastPasswordSet                   : 
LastBadPasswordAttempt            : 02.05.2018 11:51:39
PasswordNotRequired               : False
PasswordNeverExpires              : True
UserCannotChangePassword          : False
AllowReversiblePasswordEncryption : False
Certificates                      : {}
Context                           : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType                       : Domain
Description                       : 001 Test
DisplayName                       : 001 Test
SamAccountName                    : TEST001
UserPrincipalName                 : Test.001@lab.com
Sid                               : S-1-5-21-4025995969-482537669-3923321509-3972
Guid                              : de16cf7f-79c4-4257-9de7-adb687ba5fe7
DistinguishedName                 : CN=001 Test,OU=Standard,OU=Users,DC=lab,DC=com
StructuralObjectClass             : user
Name                              : 001 Test

GivenName                         : TEST0500
MiddleName                        : 
Surname                           : TestAccount
EmailAddress                      : TEST0500@lab.com
VoiceTelephoneNumber              : 
EmployeeId                        : 
AdvancedSearchFilter              : System.DirectoryServices.AccountManagement.AdvancedFilters
Enabled                           : True
AccountLockoutTime                : 
LastLogon                         : 
PermittedWorkstations             : {}
PermittedLogonTimes               : {255, 255, 255, 255...}
AccountExpirationDate             : 
SmartcardLogonRequired            : False
DelegationPermitted               : True
BadLogonCount                     : 0
HomeDirectory                     : 
HomeDrive                         : 
ScriptPath                        : 
LastPasswordSet                   : 
LastBadPasswordAttempt            : 
PasswordNotRequired               : False
PasswordNeverExpires              : False
UserCannotChangePassword          : False
AllowReversiblePasswordEncryption : False
Certificates                      : {}
Context                           : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType                       : Domain
Description                       : 
DisplayName                       : TestAccount TEST0500
SamAccountName                    : TEST0500
UserPrincipalName                 : 
Sid                               : S-1-5-21-4025995969-482537669-3923321509-16697
Guid                              : 45e85f63-3d47-452f-b071-0572f423aa99
DistinguishedName                 : CN=TestAccount TEST0500,OU=Standard,OU=Users,DC=lab,DC=com
StructuralObjectClass             : user
Name                              : TestAccount TEST0500

Well, this is really nice. First, we see a lot more of the members’ attributes than the Get-ADGroupMember cmdlet provides and second, we can directly work with these objects from here on. In my opinion, this is a great advantage over the ActiveDirectory PowerShell module. 

The group members 

Through the Members property we are not only able to list and manage who the members of a group are, we can even directly modify some attributes of the members (all attributes that are properties of the UserPrincipal class).

This could make some daily business tasks way easier for us.

For example: Let’s say you want to change the description for all members of a specific Active Directory group to “Test account”. Using the PowerShell ActiveDirectory module cmdlets, you would have to do something like this: 

  1. Run the Get-ADGroupMembers cmdlet.
    2. Iterate through the result.
    3. Pass each item of the result to the Set-ADUser cmdlet. 

Of course, you could also just pipe the result of the Get-ADGroupMember cmdlet to Set-ADUser.

Using the System.DirectoryServices.AccountManagement namespace, the steps would be the following:

First, let’s check the the current description of the group members (optional):

PS C:\Users> $group.Members | select SAMAccountName,Description

SamAccountName Description    
-------------- -----------    
TEST001        Something      
TEST0500       Something else

Now, let’s change it (this is actually all we need to do):

foreach ($user in $group.Members)
{
    #Set the new description
    $user.Description = "Test account"
    #Save the user to the Active Directory
    $user.Save()
}

There will be no confirmation if the task was successful. In this case, no news is good news.

Finally, if we want to verify the effects of our command, we can just run the first command again (optional):

PS C:\Users> $group.Members | select SAMAccountName,Description

SamAccountName Description 
-------------- ----------- 
TEST001        Test account
TEST0500       Test account

As we can see, the description of all group members was successfully changed to “Test account”. Pretty cool, huh?

Hint: Instead of using a For Each loop, we can even save some lines by calling the ForEach method of the Members property. See the following example:

$group.Members.ForEach({$_.Description = "Test account"})
$group.Members.ForEach({$_.Save()})

Speed

Again, I was curious about how the execution times would compete against the cmdlets of the ActiveDirectory module. Here are the results of my measuring:

#Using "Get-ADGroupMember" and "Set-ADUser"
PS C:\> Measure-command {Get-ADGroupMember test-group001 |%{Set-ADUser $_ -Description "Test account"}}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 213
Ticks             : 2131780
TotalDays         : 2.46733796296296E-06
TotalHours        : 5.92161111111111E-05
TotalMinutes      : 0.00355296666666667
TotalSeconds      : 0.213178
TotalMilliseconds : 213.178

#Using the "Members" property and a for each loop
PS C:\> Measure-Command {$group.Members |%{$_.Description = "Test account";$_.Save()}}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 69
Ticks             : 695725
TotalDays         : 8.05237268518518E-07
TotalHours        : 1.93256944444444E-05
TotalMinutes      : 0.00115954166666667
TotalSeconds      : 0.0695725
TotalMilliseconds : 69.5725

#Using the "Members" property and the "ForEach" method of the group object
PS C:\> Measure-command {$group.Members.ForEach({$_.Description = "Test account"});$group.Members.ForEach({$_.Save()})}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 41
Ticks             : 414158
TotalDays         : 4.79349537037037E-07
TotalHours        : 1.15043888888889E-05
TotalMinutes      : 0.000690263333333333
TotalSeconds      : 0.0414158
TotalMilliseconds : 41.4158

Sure, we already had the group object created before.

Ok, then let’s see how the creation of the group object will affect the execution time:

#Create the group object first
#then run the command

PS C:\> Measure-Command {
$group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($pc, "test-group001")
$group.Members |% {$_.Description = "Test account";$_.Save()}
}



Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 67
Ticks             : 678370
TotalDays         : 7.85150462962963E-07
TotalHours        : 1.88436111111111E-05
TotalMinutes      : 0.00113061666666667
TotalSeconds      : 0.067837
TotalMilliseconds : 67.837

#Create all necessary objects and add the assembly
PS C:\> Measure-Command {
$am = Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$pc = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::`
Domain)
$IDType = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName
$group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($pc, "test-group001")
$group.Members |% {$_.Description = "Test account";$_.Save()}
}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 97
Ticks             : 979199
TotalDays         : 1.13333217592593E-06
TotalHours        : 2.71999722222222E-05
TotalMinutes      : 0.00163199833333333
TotalSeconds      : 0.0979199
TotalMilliseconds : 97.9199

And…we have a winner. Even including all necessary code, we were still more than double as fast as the cmdlets of the ActiveDirectory module are.

 

The UserPrincipal object

Alright. Now, we know how to modify attributes of users that are members of a specific group. But, how do we work with an Active Directory user directly?

For this, we first need to create an object for the user which we want to manage.

This is done in the same way as for a group, except that we’re now using the UserPrincipal class of the System.DirectoryServices.AccountManagement namespace, instead of the GroupPrincipal class. Following, an example how to create such an object.

#load the assembly (will NOT throw any error if already loaded)
$am = Add-Type -AssemblyName System.DirectoryServices.AccountManagement
#create the PrincipalContext object
$pc = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::`
Domain)
#Set the username (SAMAccountName)
$username = "TEST001"
#load the user from AD / create the user object 
$user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($pc,$username) 

And, here’s our user:

PS C:\> $user


GivenName                         : Test
MiddleName                        : 
Surname                           : 001
EmailAddress                      : Test.001@lab.com
VoiceTelephoneNumber              : 
EmployeeId                        : 
AdvancedSearchFilter              : System.DirectoryServices.AccountManagement.AdvancedFilters
Enabled                           : False
AccountLockoutTime                : 
LastLogon                         : 02.05.2018 11:52:56
PermittedWorkstations             : {}
PermittedLogonTimes               : {255, 255, 255, 255...}
AccountExpirationDate             : 
SmartcardLogonRequired            : False
DelegationPermitted               : True
BadLogonCount                     : 0
HomeDirectory                     : \\fileserver1\TEST001$
HomeDrive                         : H:
ScriptPath                        : 
LastPasswordSet                   : 
LastBadPasswordAttempt            : 02.05.2018 11:51:39
PasswordNotRequired               : False
PasswordNeverExpires              : True
UserCannotChangePassword          : False
AllowReversiblePasswordEncryption : False
Certificates                      : {}
Context                           : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType                       : Domain
Description                       : Test account
DisplayName                       : 001 Test
SamAccountName                    : TEST001
UserPrincipalName                 : Test.001@lab.com
Sid                               : S-1-5-21-4025995969-482537669-3923321509-3972
Guid                              : de16cf7f-79c4-4257-9de7-adb687ba5fe7
DistinguishedName                 : CN=001 Test,OU=Users,DC=lab,DC=com
StructuralObjectClass             : user
Name                              : 001 Test 

Once created, we can access any of the UserPrincipal object’s properties and methods by using the PowerShell standard “.” notation.

Here’s an example how you can display only the DisplayName of the user object:

PS C:\> $user.DisplayName
001 Test

And here’s how you can change it:

#Set a new display name
PS C:\> $user.DisplayName = "001 Test / msb365"
#save the user object to Active Directory
PS C:\> $user.Save() 

Display the new value:

#Do a fresh load of the user from AD
PS C:\> $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($pc,$username)
#Show the DisplayName property
PS C:\> $user.DisplayName
001 Test / msb365 

Keep in mind: For any changes of properties to take effect, the object has to be saved by using the Save method.

MemberOf

To list all Active Directory groups that a user is a member of, the UserPrincipal object provides the methods: GetGroups and GetAuthorizationGroups.

For a listing of groups of which the user is a direct member, we can use the GetGroups method, as shown below:

PS C:\> $user.GetGroups()


IsSecurityGroup       : True
GroupScope            : Global
Members               : {Administrator, DefaultAccount, AdminIIEM, krbtgt...}
Context               : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType           : Domain
Description           : All domain users
DisplayName           : 
SamAccountName        : Domain Users
UserPrincipalName     : 
Sid                   : S-1-5-21-4025995969-482537669-3923321509-513
Guid                  : 0c8e9be6-0271-4c93-a670-8a081a87406b
DistinguishedName     : CN=Domain Users,CN=Users,DC=iamlab,DC=net
StructuralObjectClass : group
Name                  : Domain Users

IsSecurityGroup       : True
GroupScope            : Global
Members               : {001 Test, TEstACCName TEST0500}
Context               : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType           : Domain
Description           : 
DisplayName           : 
SamAccountName        : test-group001
UserPrincipalName     : 
Sid                   : S-1-5-21-4025995969-482537669-3923321509-16717
Guid                  : 924c1135-80c0-4e29-aeae-b6ff5f42c9bf
DistinguishedName     : CN=test-group001,OU=GROUPS,DC=iamlab,DC=net
StructuralObjectClass : group
Name                  : test-group001

We see that the user is a member of the two groups “Domain Users” and “test-group001”. To make the output more readable, we can use the dot notation to show only the SAMAccountNames of those groups:

PS C:\> $user.GetGroups().samaccountname
Domain Users
test-group001 

We can also include indirect memberships; groups in which the user is member of by group nesting.

For this, we need to use the GetAuthorizationGroups method. See the example below:

PS C:\> $user.GetAuthorizationGroups().samaccountname
Everyone
Authenticated
UsersDomain 
Users
test-group001
Users

Now, the list contains some more groups. The additional groups are groups of which the user is a indirect member of. For example, we see the built-in group “Users” here, because the user is a member of the “Domain Users” group, which again is a member of the group “Users”.

Other useful methods

The UserPrincipal object provides some more pretty useful methods for daily business tasks on Active Directory user accounts. Following, there’s a brief description of some of them.

Check if an account is a member of a specific group

For this, we first need to create a GroupPrincipal object for the Active Directory group that we want to check. Once we’ve created it, we can check if the user is a member of that group, by calling the IsMemberOf method of the UserPrincipal object and passing the GroupPrincipal as an overload. Return values are “True”, or “False” (type Boolean).

#create the group object
PS C:\> $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($pc, "test-group001")
#Call the MemberOf method with the group object as overload
PS C:\> $user.IsMemberOf($group)
True 
Check if an account is locked out

Return values: “True”, or “False” (type Boolean).

PS C:\> $user.IsAccountLockedOut()
False
Unlock an account
PS C:\> $user.UnlockAccount()
Refresh an expired password
PS C:\> $user.RefreshExpiredPassword()
Set a user’s password to expired
PS C:\> $user.ExpirePasswordNow()
Set a new password
PS C:\> $user.SetPassword('NewP@$$w0rd')
Delete a user account

Careful! There will be no confirmation question, or second approval option. It just deletes the account right away.

PS C:\> $user.Delete()

 

Further info

The “System.DirectoryServices.AccountManagement” namespace

The PrincipalContext class

The Add-Type cmdlet

How to work with .Net classes in PowerShell

 

In the next parts of this series, I will focus on creating Active Directory objects. So, stay tuned 😉


Leave a Reply

Your email address will not be published. Required fields are marked *