Monthly Archive

Categories

PowerShell and Active Directory

Putting user information into computer description

I was recently asked how to add user information – specifically first and last name – into computer description in Active Directory.

First get your user

$user = Get-ADUser -Identity FredBrown

 

Then add the information to the computer’s description

Set-ADComputer -Identity W10ProIp -Description "Used by $($user.Givenname) $($user.Surname)"

 

You need to use the subexpression – $() – syntax to resolve the values rather than getting references to the $user object.

 

Then test the description has been set

Get-ADComputer -Identity W10ProIp -Properties Description

 

If it was me I’d add the samAccountName or other unique identifier as name isn’t sufficient to uniquely identify the user

Moving FSMO roles in PowerShell v6.1.1

With the Windows Server 2019 media now being available again it’s time to move my test lab over to the new version. I’d built a Windows Server 2019 VM and installed PowerShell v6.1.1. I discovered that in Server 2019 and the Windows 10 October 2018 update that the AD module worked in PowerShell v6.1.1. I decided to try moving FSMO roles in PowerShell v6.1.1 as I updated the domain and removed the old Server 2016 domain controller.

 

The usual schema update went smoothly – updated the schema version to 88 from 87. Installing AD domain services and DNS on the new DC worked. Promoting the Windows 2019 system to be a DC worked with no problems.

 

Time to move the FSMO roles. They would move automatically when the old DC was removed but its always better to control the action.

Import-Module ActiveDirectory

will load the AD module into PowerShell v6.1.1.

 

There are 5 FSMO roles – 2 at the forest level

PS> Get-ADForest | Format-List Name, *master

Name : Manticore.org
DomainNamingMaster : W16DC01.Manticore.org
SchemaMaster : W16DC01.Manticore.org

 

And 3 at the domain level – I only have a single domain to worry about.

PS> Get-ADDomain | Format-List *master, PDC*

InfrastructureMaster : W16DC01.Manticore.org
RIDMaster : W16DC01.Manticore.org
PDCEmulator : W16DC01.Manticore.org

 

The forest level FSMO roles moved:

PS> Move-ADDirectoryServerOperationMasterRole -Identity W19DC01 -OperationMasterRole DomainNamingMaster -Confirm:$false

PS> Move-ADDirectoryServerOperationMasterRole -Identity W19DC01 -OperationMasterRole SchemaMaster -Confirm:$false

PS> Get-ADForest | Format-List Name, *master

Name : Manticore.org
DomainNamingMaster : W19DC01.Manticore.org
SchemaMaster : W19DC01.Manticore.org

 

For the domain level FSMO roles I decided to get ambitious

PS> Move-ADDirectoryServerOperationMasterRole -Identity W19DC01 -OperationMasterRole RIDMaster, InfrastructureMaster, PDCEmulator -Confirm:$false

PS> Get-ADDomain | Format-List *master, PDC*

InfrastructureMaster : W19DC01.Manticore.org
RIDMaster : W19DC01.Manticore.org
PDCEmulator : W19DC01.Manticore.org

 

Moving FSMO roles in PowerShell v6.1.1 was successful

Active Directory cmdlets in PowerShell v6.1.1

Just discovered that you can run the Active Directory cmdlets in PowerShell v6.1.1 BUT there is a huge caveat.

 

The Windows 10 October 2018 (Windows 10 1809) update includes the RSAT tools (including the AD tools) as optional features. This means that you can easily install the AD tools:

Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 –Online

 

The AD PowerShell module now has a 1.0.1.0 version with the PSEdition set as Core,Desk

 

I’m running the Windows 10 Insider Preview build 18282 which has moved on a bit from Windows 10 1809 and PowerShell Core v6.1.1.

 

The AD module can be imported and the commands that I’ve tried have worked.

 

For the AD cmdlets to work in PowerShell Core looks like you need Windows 10 October 2018 update or later and PowerShell v6.1.1 or later. I’m going to say you should upgrade to v6.1.1 if you have v6.1 as the later version fixes a .NET security bug.

 

This is a big step forward for PowerShell Core being an acceptable replacement for Windows PowerShell though there are still a few gaps to be filled.

Windows Compatibility Pack

As reported last month the Windows Compatibility Pack for .NET core is available. This adds back some of the functionality missing from .NET core. This functionality is ONLY of relevance on Windows machines.

 

A PowerShell module based on the Compatibility Pack is in the works – this will add a number of cmdlets including the WMI cmdlets back into PowerShell v6 on Windows. There’s no ETA on the module at this time.

 

There is a module on the PowerShell gallery that will add the .NET components of the Compatibility Pack into your PowerShell v6 session.

PS>  Find-Module -Name PSCoreWindowsCompat | ft -a

Version Name                Repository Description 
 ------- ----                ---------- ----------- 
 0.1.0.2 PSCoreWindowsCompat PSGallery  Provides the Microsoft.Windows.Compatibility Pack to PowerShell Core.

 

If you want to inspect the module

PS>  Save-Module -Name PSCoreWindowsCompat -Repository PSGallery -Path C:\Source\ -Force

 

To install the module:

PS>  Install-Module -Name PSCoreWindowsCompat -Repository PSGallery -Verbose -Force 
 VERBOSE: Repository details, Name = 'PSGallery', Location = 'https://www.powershellgallery.com/api/v2/'; IsTrusted = 'False'; IsRegistered = 'True'. 
 VERBOSE: Using the provider 'PowerShellGet' for searching packages. 
 VERBOSE: Using the specified source names : 'PSGallery'. 
 VERBOSE: Getting the provider object for the PackageManagement Provider 'NuGet'. 
 VERBOSE: The specified Location is 'https://www.powershellgallery.com/api/v2/' and PackageManagementProvider is 'NuGet'. 
 VERBOSE: Searching repository 'https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PSCoreWindowsCompat'' for ''. 
 VERBOSE: Total package yield:'1' for the specified package 'PSCoreWindowsCompat'. 
 VERBOSE: Performing the operation "Install-Module" on target "Version '0.1.0.2' of module 'PSCoreWindowsCompat'". 
 VERBOSE: The installation scope is specified to be 'AllUsers'. 
 VERBOSE: The specified module will be installed in 'C:\Program Files\PowerShell\Modules'. 
 VERBOSE: The specified Location is 'NuGet' and PackageManagementProvider is 'NuGet'. 
 VERBOSE: Downloading module 'PSCoreWindowsCompat' with version '0.1.0.2' from the repository 'https://www.powershellgallery.com/api/v2/'. 
 VERBOSE: Searching repository 'https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PSCoreWindowsCompat'' for ''. 
 VERBOSE: InstallPackage' - name='PSCoreWindowsCompat', version='0.1.0.2',destination='C:\Users\Richard.MANTICORE\AppData\Local\Temp\51711061' 
 VERBOSE: DownloadPackage' - name='PSCoreWindowsCompat', version='0.1.0.2',destination='C:\Users\Richard.MANTICORE\AppData\Local\Temp\51711061\PSCoreWindowsCompat\PSCoreWindowsCompat.nupkg', uri='https://www.powershellgallery.com/api/v2/package/PSCoreWindowsCompat/0.1.0.2' 
 VERBOSE: Downloading 'https://www.powershellgallery.com/api/v2/package/PSCoreWindowsCompat/0.1.0.2'. 
 VERBOSE: Completed downloading 'https://www.powershellgallery.com/api/v2/package/PSCoreWindowsCompat/0.1.0.2'. 
 VERBOSE: Completed downloading 'PSCoreWindowsCompat'. 
 VERBOSE: InstallPackageLocal' - name='PSCoreWindowsCompat', version='0.1.0.2',destination='C:\Users\Richard.MANTICORE\AppData\Local\Temp\51711061' 
 VERBOSE: Catalog file 'PSCoreWindowsCompat.cat' is not found in the contents of the module 'PSCoreWindowsCompat' being installed. 
VERBOSE: Module 'PSCoreWindowsCompat' was installed successfully to path 'C:\Program Files\PowerShell\Modules\PSCoreWindowsCompat\0.1.0.2'.

 

Notice the installation path is OUTSIDE of the current version of PowerShell v6 so should remain available through any upgrades.

PS>  Import-Module -Name PSCoreWindowsCompat

 

Now you’ve got it how do you use it? The module DOESN’T have any functions – it just loads the .NET namespaces.

 

Its back to PowerShell v1 days – everything is a script instead of a cmdlet. For instance the Compatibility pack contains the System.DirectoryServices so you can script against AD.

 

Let’s say you want to see all the users in the domain:

$dom = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 
 $root = $dom.GetDirectoryEntry()

$search = [System.DirectoryServices.DirectorySearcher]$root 
 $search.Filter = "(&(objectclass=user)(objectcategory=user))" 
 $search.SizeLimit = 3000 
 $result = $search.FindAll()

foreach ($user in $result){ 
    $user.Properties.distinguishedname 
 }

 

Use System.DirectoryServices.ActiveDirectory.Domain to get the current domain. Create a System.DirectoryServices.DirectorySearcher object and set the filter to users. Find all of the users and display their distinguishedname

 

Its a lot more complicated than using the cmdlet:

PS>  Get-ADUser -Filter * | select DistinguishedName

 

but it gets the job done.

 

If you need to administer AD and you need the cross platform capabilities of PowerShell maybe you should use the  PSCoreWindowsCompat module (with aplogies to A-Team fans everywhere)

Get an AD user’s manager

Interesting question on the forum about finding the manager for a given user in AD – assuming the Manager field is populated of course. If you’ve not worked with the AD cmdlets this is a good introduction to some of their quirks. This is how you get an AD user’s manager.

 

You need the manager property on the AD user account but that’s not one of the default properties that’s returned so you need to use the –Propertie parameter to ensure you get your data. Assuming you have a csv file with userids that looks like this

id userid 
-- ------ 
1  DonBrown 
2  DonFox 
3  JamesBrown 
4  JamesBlack

 

You can use this code

Import-Csv -Path .\names.csv | 
foreach { 
   $user = Get-ADUser -Identity $_.userid -Properties Manager 
   $_ | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $user.Manager 
   $_ 
 }

 

This returns the distinguished name of the manager

id userid     Manager                                           
-- ------     -------                                           
1  DonBrown   CN=HARRIS Fred,OU=UserAccounts,DC=Manticore,DC=org 
2  DonFox     CN=HARRIS Fred,OU=UserAccounts,DC=Manticore,DC=org 
3  JamesBrown CN=HARRIS Fred,OU=UserAccounts,DC=Manticore,DC=org 
4  JamesBlack CN=HARRIS Fred,OU=UserAccounts,DC=Manticore,DC=org

 

if you prefer to have their name then you need an extra step

Import-Csv -Path .\names.csv | 
foreach { 
   $user = Get-ADUser -Identity $_.userid -Properties Manager 
   $manager = Get-ADUser -Identity $user.Manager 
   
   $_ | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $manager.Name 
   $_ 
 }

 

Which gives output like this

id userid     Manager    
-- ------     -------    
1  DonBrown   HARRIS Fred 
2  DonFox     HARRIS Fred 
3  JamesBrown HARRIS Fred 
4  JamesBlack HARRIS Fred

 

If you need to output the data to a csv file then just add Export-Csv

Import-Csv -Path .\names.csv | 
foreach { 
   $user = Get-ADUser -Identity $_.userid -Properties Manager 
   $manager = Get-ADUser -Identity $user.Manager 
   
   $_ | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $manager.Name 
   $_ 
 } | Export-Csv -Path names2.csv –NoTypeInformation

Comparing AD group membership on EmployeeId

Back in this post - http://itknowledgeexchange.techtarget.com/powershell/comparing-group-membership/ I showed how to compare the membership of two groups using Compare-Object. The comparison was based on the samAccountName. A question raised the issue of comparing AD group membership on EmployeeId

In the case in particular users have multiple accounts BUT the EmployeeId is correct on all and will therefore show matching users. Assuming the EmployeeId is correct on all accounts it still leaves a problem.

When you run Get-ADGroupMember you get a very limited number of properties returned:

PS>  Get-ADGroupMember -Identity Testgroup1

distinguishedName : CN=JONES James,OU=UserAccounts,DC=Manticore,DC=org 
 name              : JONES James 
objectClass       : user 
objectGUID        : 027cb406-a3b0-4f45-9bbd-db47ccfb9212 
 SamAccountName    : JamesJones 
 SID               : S-1-5-21-759617655-3516038109-1479587680-1225

First thing I needed to do was set up some users with an EmployeeId

$ei = 1 
 Get-ADUser -Filter {Name -like "*Jones*"} -Properties EmployeeId | 
foreach { 
  $id =  23945 + $ei 
  
  $psitem | Set-ADUser -EmployeeID $id

  $ei = $ei + (Get-Random -Minimum 3 -Maximum 12) 
 }

Get a set of users – including the EmployeeId – and forech of them set the id. The id is randomly generated based on a starting value and increment.

Now that the users have an Employeeid you can use that for comparison purposes

$group1 = Get-ADGroupMember -Identity Testgroup1 | 
foreach { 
  Get-ADUser -Identity $psitem.distinguishedName -Properties EmployeeId | 
  select -ExpandProperty EmployeeId 
 }

$group2 = Get-ADGroupMember -Identity Testgroup2 | 
foreach { 
  Get-ADUser -Identity $psitem.distinguishedName -Properties EmployeeId | 
  select -ExpandProperty EmployeeId 
 }


 Compare-Object -ReferenceObject $group1 -DifferenceObject $group2 -IncludeEqual |             
 where SideIndicator -eq "==" |            
foreach {            
  $id = ($_.InputObject)        
            
  Get-ADUser -Filter {EmployeeId -eq $id} -Properties EmployeeId            
             
 }

Get the membership of the first group and for each member use Get-ADUser to return the EmployeeId. Repeat for the second group.

Use  Compare-Object to compare the two sets of group members – you’re looking for matches indicated by “==”

Foreach match get the AD user account filtering on the EmployeeID.

The PROBLEM with this approach is that you’ll get all user accounts returned that have the particular EmployeeId.   You can replace the line

Get-ADUser -Filter {EmployeeId -eq $id} -Properties EmployeeId

with

Get-ADUser -Filter {EmployeeId -eq $id} -Properties EmployeeId, MemberOf | where {$_.MemberOf -like "*Testgroup1*" -AND $_.MemberOf -like  "*Testgroup2*"}

Which should resolve the problem

Get-ADUser filtering

Saw a question on the forums that revolved around Get-ADUser filtering.

Initial code was like this

Import-Csv .\users.txt |
foreach {
  Get-ADUser -Filter {Name -like $_.Name}
}

which on the face of it seems reasonable. However, you get errors like this

Get-ADUser : Property: 'Name' not found in object of type:
'System.Management.Automation.PSCustomObject'.
At line:3 char:3
+   Get-ADUser -Filter {Name -like $_.Name}
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-ADUser], ArgumentException
    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:System.ArgumentException,Microsoft
   .ActiveDirectory.Management.Commands.GetADUser

Change –like to –eq and you’ll still get the error.

This won’t work either:

Import-Csv .\users.txt |
foreach {
  Get-ADUser -Filter {Name -like $($_.Name)}
}

You get messages about path errors.

Import-Csv .\users.txt |
foreach {
  Get-ADUser -Filter {Name -like "$($_.Name)"}
}

will run but won’t return any data.

This will run and return the correct data.

Import-Csv .\users.txt |
foreach {
  $name = $_.Name
  Get-ADUser -Filter {Name -like $name}
}

Alternatively, you can use quotes so that the filter is a string

Import-Csv .\users.txt |
foreach {
  Get-ADUser -Filter "Name -like '$($_.Name)'"
}

Another option is to use the LDAP filter syntax

Import-Csv .\users.txt |
foreach {
  $name = $_.Name
  Get-ADUser -LDAPFilter "(Name=$name)"
}

Import-Csv .\users.txt |
foreach {
  Get-ADUser -LDAPFilter "(Name=$($_.Name))"
}

The help file about_activedirectory_filter is well worth reading. It doesn’t seem to be installed with the AD module on Windows Server 2016. You can read it on line at https://technet.microsoft.com/en-us/library/hh531527(v=ws.10).aspx

You’ll also see links to

about_ActiveDirectory  - overview of module

about_ActiveDirectory_Identity

about_ActiveDirectory_ObjectModel

Get-ADUser filtering isn’t the most obvious of topics to get your head round but these examples should help you make your filters work. If you’re building a complex filter build it up a step at a time so you can test each stage.

Are your domain controllers real?

A question on the forum asked about discovering if domain controllers are physical or virtual machines.

This will do the job

foreach ($domain in (Get-ADForest).domains) {
 Get-ADDomainController -filter * -server $domain |
 sort hostname  |
 foreach {
 Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName $psitem.Hostname |
 select PSComputerName, Manufacturer, Model
 }
 }

 

Get the domains in your forest and then for each domain get the domain controllers. Get-ADDomainController outputs an object with a property of hostname – but you need a computername for Get-CimInstance. So, use a foreach-object and use the Hostname property as shown (you could create a property ComputerName on the pipeline object but its more work) and get the results. A virtual machine will show under the Model. You can sort or whatever once you have the results.

Name mismatch

Ever wondered why you can’t do this:

Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org' |
Get-CimInstance -ClassName Win32_OperatingSystem

The –ComputerName parameter on get-CimInstance accepts pipeline input BUT its by property name.

PS> Get-Help Get-CimInstance -Parameter ComputerName

-ComputerName [<String[]>]
Specifies computer on which you want to run the CIM operation. You can specify a fully qualified domain name
(FQDN), a NetBIOS name, or an IP address.

If you do not specify this parameter, the cmdlet performs the operation on the local computer using Component
Object Model (COM).

If you specify this parameter, the cmdlet creates a temporary session to the specified computer using the WsMan
protocol.

If multiple operations are being performed on the same computer, using a CIM session gives better performance.

Required?                    false
Position?                    named
Default value                none
Accept pipeline input?       True (ByPropertyName)
Accept wildcard characters?  false

If you look at the output of Get-ADComputer it has a Name property.

PS>  Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org'

DistinguishedName : CN=W16PWA01,OU=Servers,DC=Manticore,DC=org
DNSHostName       : W16PWA01.Manticore.org
Enabled           : True
Name              : W16PWA01
ObjectClass       : computer
ObjectGUID        : 8d137004-1ced-4ff1-bcf4-f0671652fc8c
SamAccountName    : W16PWA01$
SID               : S-1-5-21-759617655-3516038109-1479587680-1322
UserPrincipalName :

So you have a Name mismatch between the property and the parameter.

There are a number of ways to deal with this.

First use foreach

Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org' |
foreach {
Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $psitem.Name |
select CSName, Caption
}

Use $psitem.Name (or $_.Name) as the input to –ComputerName. Simple coding and works very nicely.

If you have a lot of computers you may want to use a foreach loop instead

$computers = Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org' | select -ExpandProperty Name
foreach ($computer in $computers) {
Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $computer|
select CSName, Caption
}

Create an array of computer names and iterate through them.

Second use select

Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org' |
select @{N='ComputerName';E={$_.Name}} |
Get-CimInstance -ClassName Win32_OperatingSystem |
select CSName, Caption

In this case use select-object to create a property with the name ComputerName (case DOESN’T matter) and pipe that into Get-CimInstance.

This option is a bit more advanced as you have understand how select-object works and how to create extra properties on the object you’re passing down the pipeline. It looks cooler and should get you a few extra “ace powerShell coder” points.

The third option takes advantage of the fact that _Computername accepts an array of computer names

Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName (Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=Manticore,DC=org' | select -ExpandProperty Name) |
select CSName, Caption

You run Get-ADComputer and use select –Expand to only return the VALUE of the computer name (a string). This gives you an array of computer names. Because its in () its treated as an input object to the parameter.

Very clever and gets you maximum points.

Modifying AD users in bulk

Modifying AD users in bulk involves either setting one or more properties to the same value for a set of users or reading in the values you need from a data source of some kind.

We prepared some test data in the last post so lets see how we use it.

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
Get-ADUser -Identity $user.Id |
Set-ADUser -Division $user.Division -EmployeeNumber $user.EmployeeNumber
}

The simplest way is to read in the data and store as a collection of objects. Use foreach to iterate through the set of user information. Get-ADUser gets the appropriate AD account which is piped to Set-ADUser. Set-ADUser is a great cmdlet because it has parameters for most of the user properties.

In this case though we know that some of the users don’t have employee numbers. This means a bit more work. Two approaches are possible – use splatting and the parameters used above or use the –Replace option

Lets look at splatting first

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
$params = @{
Division = $user.Division
EmployeeNumber = 0
}

if ($user.EmployeeNumber) {
$params.EmployeeNumber = $user.EmployeeNumber
}
else {
$params.Remove('EmployeeNumber')
}

Get-ADUser -Identity $user.Id |
Set-ADUser @params
}

As before read the user information into the $users variable. Iterate over the users with foreach. Create a hashtable for the parameters and their values. Division is always present so that can be set directly. Employeenumber should be tested and if  present the place holder value should be overwritten with the correct value otherwise Employeenumber is removed from the hashtable.

The user account is found and Set-ADUser sets the correct values. Notice how the hashtable is specified to the cmdlet.

Splatting is a great way to dynamically set the parameters you’re using on a particular cmdlet.

Set-ADUser has an alternative – the –Replace parameter.

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
$params = @{
division = $user.Division
employeeNumber = 0
}

if ($user.EmployeeNumber) {
$params.EmployeeNumber = $user.EmployeeNumber
}
else {
$params.Remove('EmployeeNumber')
}

Get-ADUser -Identity $user.Id |
Set-ADUser -Replace $params
}

This is very similar to the splatting example but instead of splatting the hashtable you use it as the value input to the Replace parameter. If you wrote  out the command it would look like this:

Set-ADUser –Replace @{division = ‘Division B’; employeeNumber  = 100}

With –Replace you’re using the LDAP names of the properties rather than the GUI or PowerShell name – there are differences for instance surname is sn in LDAP.

Modifying AD users in bulk is straightforward with PowerShell and its relatively easy to deal with missing values if you adopt one of the above ideas. Splatting is probably the easiest in this case.