Slow Get-ADUser query - powershell

Something I do not unterstand. See the following two code examples.
$LDAPResult1 = Get-ADUser -LDAPFilter "(&(objectCategory=user)(sAMAccountName=*))" -Properties #("distinguishedName","sAMAccountName","extensionAttribute13") -SearchBase "ou=test,dc=test,dc=ch"
$LDAPElements1=#{}
$LDAPResult1 |% {$LDAPElements1.Add($_.SAMAccountName, $_.extensionattribute13)}
compared with (adding a specific server to ask "-Server 'dc.test.test.ch'"):
$LDAPResult1 = Get-ADUser -LDAPFilter "(&(objectCategory=user)(sAMAccountName=*))" -Properties #("distinguishedName","sAMAccountName","extensionAttribute13") -SearchBase "ou=test,dc=test,dc=ch" -Server 'dc.test.test.ch'
$LDAPElements1=#{}
$LDAPResult1 |% {$LDAPElements1.Add($_.SAMAccountName, $_.extensionattribute13)}
The first code takes 30 seconds, the second about 5 minutes. The problem ist not the AD query. This takes around 30 seconds in both cases. But filling the result into the hash table is what is differnet. It seems as if in the second case while filling the hash sill some data is requested from the DC.
What is also interesting. When I wait for five minutes after doing the AD query in case two and then execute the filling into the hash table, then the command takes a second.
I rather would likt to define to what server the command connects in order to execute the folloing commands on the same DC, but this does not make sense if it takes that long.
Can anyone enlighten me …
Addition: We are Talking about 26'000 accounts.

I was able to replicate this. The behaviour does change when you specify the -Server parameter vs. when you don't.
I used Process Monitor to watch network activity and it definitely is talking to the DC when looping through the results returned from using the -Server parameter.
I can't explain why, but it seems like the ADUser objects returned are not populated with the properties from the search. So when they are accessed, it loads the properties from the DC. I could see this when accessing one particular element in the array:
$LDAPResults1[1000]
It displayed the properties, but I also saw network activity in Process Monitor. Whereas I do not see network activity when accessing one element from the results returned when not using the -Server parameter.
So that kind of explains what is happening, but not why. And I really don't know why.
However, I have learned that if you want performance when talking to AD, you have to scrap all the "easy" ways and do things yourself. For example, use the .NET DirectoryEntry and DirectorySearcher classes directly, which can be done in PowerShell using the "type accelerators" [adsi] and [adsisearcher]. For example, this will do the same and will perform consistently:
$dc = "dc.test.test.ch"
$searchBase = "ou=test,dc=test,dc=ch"
$searcher = [adsisearcher]::new([adsi]"LDAP://$dc/$searchBase", "(objectCategory=user)")
$searcher.PropertiesToLoad.Add("sAMAccountName") > $null
$searcher.PropertiesToLoad.Add("extensionAttribute13") > $null
$searcher.PageSize = 1000
$LDAPElements1=#{}
foreach ($result in $searcher.FindAll()) {
$LDAPElements1.Add($result.Properties["sAMAccountName"][0], $result.Properties["extensionAttribute13"][0])
}

I found the following code to be extremely slow.
$user = Get-ADUser -LDAPFilter $filter -Server "xyc" -Properties "sAMAccountName"
I was able to rewrite it as:
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchRoot = [ADSI]'LDAP://xyz'
[void]$directorySearcher.PropertiesToLoad.Add('cn')
[void]$directorySearcher.PropertiesToLoad.Add('sAMAccountName')
$directorySearcher.Filter = "(cn=abcd efg)"
$results = $directorySearcher.FindOne()
Write-Host $results.Properties["samaccountname"] -as [String]
and it was a lot faster (by an order of magnitude) than using GetAd-User (but still slow).

Export Get-ADUser results into a temporary CSV file and import it back to some objects.
Get-ADUser -LDAPFilter (....) | Export-Csv -Path "TempCSV.csv" -Encoding UTF8 -Delimiter ","
$ADUsers = Import-Csv -Path "TempCSV.csv" -Encoding UTF8 -Delimiter ","
Now you can loop the users object.
foreach ($ADUser in $ADUsers) { (....) }

Related

AD GUI shows properties that PowerShell returns empty

I want to get a list of all AD Users and their creation time and last logon time. First I used the Active Diretory Users and Computers app and activated the Advanced Features. In the Attribute Editor I can see the properties are called LastLogon and WhenCreated.
So I did this:
$allUsers = Get-ADUser -Filter * -Properties SamAccountName,LastLogon,WhenCreated
$allUsers | select SamAccountName,LastLogon,WhenCreated
However LastLogonand WhenCreated are only filled for 13 of 500 Users. In the Attribute Editor these values are filled for a lot more...
When I query one user only that has these values in the Attribute Editor with Get-ADUser -Identity $User -Properties * I see that the attributes are called LastLogonDateand Created (values are shown empty).
So I searched for those attributes:
$allUsers2= Get-ADUser -Filter * -Properties SamAccountName,LastLogonDate,Created
$allUsers2 | select SamAccountName,LastLogonDate,Created
Then again those 13 have the info the rest doesn't.
Has anyone an idea how I get those values? (I am going to export them with Export-CSV so another way to get those in Excel is ok, too )
As requested my comments as answer.
First attempt:
Add the -Server switch on Get-ADUser and have it query the same Domain Controller you are currently connected to with Active Directory Users and Computers. It may be that you are asking for properties that have not yet been synchronized (especially the lastLogon time stamp which I believe is synced only once every 14 days unless you have specified a different value for the ms-DS-Logon-Time-Sync-Interval attribute on the domain default naming context.)
--> didn't apply because you're running this on the DC itself
Second attempt:
Try ADSI as in $searcher = [adsisearcher]'(&(objectCategory=person)(objectClass=user))'; $searcher.FindAll()
--> same results as with Get-ADUser; still empty values
Third attempt:
Check PowerShell version(s)
--> apparently the DC had PS version 4. With version 5.1 it works
First, look at what properties your cmdlet has:
$a = Get-ADUser -server 'DomenNameTest.en' -Identity 'makarovayu' -Properties *
$a | Get-Member
I recommend copying the received data into a notepad in order to copy the available field names later.
2-Let's declare an array and use the cmdlet to try to collect information on the required fields
$userList = Get-ADUser -server 'DomenNameTest.en' -Properties SamAccountName,Name -Filter * |
#Do not forget that the comanlet has a limitation and can fall off on timeout.See how I work with each property in [select]
Select #{Name = "SamAccountName"; Expression={$_.SamAccountName}},#{Name = "Name"; Expression={$_.Name}} |
#Uploading data to [csv]
Export-Csv -Path "D:\Users\userTest\Desktop\userList.csv" -Append -NoTypeInformation -Encoding Default -Delimiter ';'
Remove-Variable a,userList #Clear the variables

How to move multiple users to multiple OUs importing users from CSV and filtering by department

New to powershell and scripting in general. Trying to improve automation in our onboarding process, we have to move multiple user accounts to multiple OUs every couple of months.
Import-Module ActiveDirectory
$dept1 = "OU=Path1,DC=SOMEWHERE,DC=OUTTHERE"
$dept2 = "OU=Path2,DC=SOMEWHERE,DC=OUTTHERE"
Import-Csv "C:\Scripts\Incoming.csv" | ForEach-Object {
$samAccountName = $_."samAccountName"
Get-ADUser -Identity $samAccountName -Filter {Department -eq "Dept1"} | Move-ADObject -TargetPath $dept1
Get-ADUser -Identity $samAccountName -Filter {Department -eq "Dept2"} | Move-ADObject -TargetPath $dept2
}
This actually moves ALL users with the department marked into the path I have set.. but I only want it to look at those users in the csv and then filter their departments from AD - not the CSV. I feel like I'm missing a step but I've searched everywhere and tried the get-help. I feel like I need to get the identity, then search/filter against something else but I'm not quite sure how. Would appreciate any help.
edit
Okay, if I run the following:
Get-ADUser -Filter {Department -eq "Dept1"} -Properties Department
It returns everyone that fits that property but how do I compare those to the $samAccountName and ONLY try to move those accounts or run the commands against the accounts on the list? When I ran the second half of the command it tried to move them all and failed.
Move-ADObject $samAccountName -Target $dept1
I feel dumb.
It's ok to feel dumb. You're not and everyone feels that way at times when trying to learn a new thing. You're also here asking for help, so you're ahead of the game compared to a lot of others.
#Lee_Daily's comment is correct that Get-ADUser doesn't support using both -Identity and -Filter in the same command. They're part of different parameter sets. You can tell from the syntax output of Get-Help Get-ADUser or the online docs. Both show 3 different sets of parameters and Identity and Filter are not in the same one. What's odd is that your original script should have thrown an error because you tried to use both in the same command. No need to worry about that now though.
Here's a typical way one might approach this task. First, you query the user's details including the department you want to make a decision on. Then, you write your condition and perform the appropriate action. Doing it this way means you're only querying AD once for each user in your CSV rather than twice like your original script which is good for script performance and load on your AD. The inside of your ForEach-Object loop might look something like this. Note the addition of -Properties department in Get-ADUser. We need to ask for it explicitly because department isn't returned in the default result set.
# all of this goes inside your existing ForEach-Object loop
$u = Get-ADUser -Identity $_.samAccountName -Properties department
if ($u.Department -eq 'Dept1') {
$u | Move-ADObject -TargetPath $dept1
} elseif ($u.Department -eq 'Dept2') {
$u | Move-ADObject -TargetPath $dept2
}
Now let's talk about some alternative ways you might approach this.
The first way sort of flips things around so you end up only calling Get-ADUser once total, but end up doing a lot more filtering/processing on the client side. It's not my favorite, but it sets things up to understand my preferred solution. In particular, the Get-ADUser call uses the -LDAPFilter parameter. LDAP filter syntax is a little strange if you've never seen it before and this particular example could use the more common -Filter syntax just as easily. But in the next example it would be much more difficult and learning LDAP filter syntax enables you to query AD from anything rather than just PowerShell.
# instead of immediately looping on the CSV we imported, save it to a variable
$incoming = Import-Csv "C:\Scripts\Incoming.csv"
# then we make a single AD query for all users in either Dept1 or Dept2
$users = Get-ADUser -LDAPFilter '(|(department=Dept1)(department=Dept2))' -Properties department
# now we filter the set of users from AD by department and whether they're in the CSV and do the moves as appropriate
$users | Where-Object { $_.department -eq 'Dept1' -and
$_.samAccountName -in $incoming.samAccountName } | Move-ADObject -TargetPath $dept1
$users | Where-Object { $_.department -eq 'Dept2' -and
$_.samAccountName -in $incoming.samAccountName } | Move-ADObject -TargetPath $dept2
The benefit of this method is the single AD round trip for users rather than one for each in the CSV. But there are a lot more local loops checking the samAccountNames in the results with the samAccountNames from the CSV which can get expensive cpu-wise if your CSV and/or AD is huge.
The final example tweaks the previous example by expanding our LDAP filter and making AD do more of the work. AD is ridiculously good at returning LDAP query results. It's been fine-tuned over decades to do exactly that. So we should take advantange of it whenever possible.
Essentially what we're going to do is create a giant 'OR' filter out of the samAccountNames from the CSV so that when we get our results, the only check we have to do is the check for department. The way I verbalize this query in my head is that we're asking AD to "Return all users where SamAccountName is A or B or C or D, etc and Department is Dept1 or Dept2. The general form of the filter will look like this (spaces included for readability).
(& <SAM FILTER> <DEPT FILTER> )
# Where <DEPT FILTER> is copied from the previous example and
# <SAM FILTER> is similar but for each entry in the CSV like this
(|(samAccountName=a)(samAccountName=b)(samAccountName=c)...)
So let's see it in action.
# save our CSV to a variable like before
$incoming = Import-Csv "C:\Scripts\Incoming.csv"
# build the SAM FILTER
$samInner = $incoming.samAccountName -join ')(samAccountName='
$samFilter = "(|(samAccountName=$samInner))"
# build the DEPT FILTER
$deptFilter = '(|(department=Dept1)(department=Dept2))'
# combine the two with an LDAP "AND"
$ldapFilter = "(&$($samFilter)$($deptFilter))"
# now make our single AD query using the final filter
$users = Get-ADUser -LDAPFilter $ldapFilter -Properties department
# and finally filter and move the users based on department
$users | Where-Object { $_.department -eq 'Dept1' } | Move-ADObject -TargetPath $dept1
$users | Where-Object { $_.department -eq 'Dept2' } | Move-ADObject -TargetPath $dept2
There are more efficient ways to build the LDAP filter string, but I wanted to keep things simple for readability. It's also a lot easier to read with better PowerShell syntax highlighting than StackOverflow's. But hopefully you get the gist.
The only limitation with using this method is when your incoming CSV file is huge. There's a maximum size that your LDAP filter can be. Though I'm not sure what it is and I've never personally reached it with roughly ~4000 users in the filter. But even if you have to split up your incoming CSV file into batches of a few thousand users, it's still likely to be more efficient than the other examples.

Get-ADUser Filter parameter matching second time around

I'm going to have to explain this step by step. First, here's some code that I'm trying to get working:
$Users = Get-Content c:\textfile.txt
foreach ($user in $users) {Get-ADuser -Filter {UserPrincipalName -like "$user*"}}
So when I run this, it returns nothing, which I know is wrong. So I used break points to look a little closer. I found that the variable $user is indeed populating with the correct values. As a test, I replaced $user in the filter with one value (so that it looks like code below) from the text file and stepped through it.
$Users = Get-Content c:\textfile.txt
foreach ($user in $users) {Get-ADuser -Filter {UserPrincipalName -like "1234567890*"}}
This is where it gets interesting. I put the break point on the foreach line and as I let it run through the first time, nothing wrote to the screen except a blank line. When I stepped through it the second time, output as if the command had run twice came out, i.e. the user object with the user principal name like 1234567890* wrote to the screen twice.
As another clue, I tried the following line:
Get-Content c:\textfile.txt | Get-ADuser -Filter {UserPrincipalName -like "$_*" }
And it pops out an error saying that the Filter parameter doesn't take pipeline input. I don't know if that means anything, but I suspect it doesn't since I'm not really using the pipeline in my original code. It's probably something super basic with the Get-ADUser cmdlet that I'm not tracking, even though I've been using Powershell for a while now.
Using the filter is a little finicky within AD. It is actually -filter <string>
Get-ADUser -Filter "UserPrincipalName -like '$($user)*'"
where-object is the other type of filter syntax you were attempting to use. E.G. where-object {UserPrincipalName -like "$user*"}

Powershell pipeline access to ADComputer object "Info"

I've been trying to work with the following query to get AD info for my servers, but "Info" just comes back as blob of stuff. To me it looks like I should be able extract specific parts, but I've had little success. I've even tried to use regex to get what I need out, but I'm terrible with regex. Any help with how to extract the SN,MAC and IP from "Info" would be much appreciated.
Get-ADComputer -Filter * -Property * -SearchBase $server | select -Property name,info
You could try using '-split':
$comp = Get-ADComputer -Filter * -Property * -SearchBase $server
$info = $comp.info
$SerialNumber = ($info -split 'SN=')[1] #replace with 'VMWare-' if desired
$SerialNumber = ($SerialNumber -split ';')[0]
This works with serial number assuming the output is formatted exactly as you have displayed it in the comments. It does, however, bring in that 'VMware' string included. Use the code in my comment if you would like just the #s.
You can copy that -split code out for the remaining fields you would like to extract.

How to pass a variable to -Filter

I have come across a very strange situation in PS.
In a script I have, there is a cmdlet (Get-Mailbox) which pulls back a few mailboxes and stores them in $mailboxes.
I then loop through this as follows to find a matching AD account.
foreach ($user in $mailboxes) {
Get-ADUser -Filter {UserPrincipalName -eq $user.UserPrincipalName}
}
When I run this it errors saying that it can't find the property UserPrincipalName on $user.
I have debugged the script and tested it thoroughly. At the point where it errors if I type $user.UserPrincipalName it outputs a list of UPNs and their date type is string so the property is there and has data.
I came to the conclusion that for some reason -Filter can't see the $user variable - as if it is isolated inside the {} brackets which I have heard can be the case with functions. However if I modify the code like so it works.
foreach ($user in $mailboxes) {
$name = $user.UserPrincipalName
Get-ADUser -Filter {UserPrincipalName -eq $name}
}
Although this fixes my problem I'd like to learn why the first example doesn't work. Can anyone explain?
Something worth noting is the get-mailbox actually connects to Exchange Online first and returns a data type of:
Deserialized.Microsoft.Exchange.Data.Directory.Management.Mailbox
but when the Get-ADUser command errors it says the the object is of type PSCustomobject. I think this maybe part of the problem.
Get-ADUser -Filter "userprincipalname -eq '$($user.userprincipalname)'"
I don't know why, but there's some more discussion here about which syntaxes do and don't work with Get-ADUser, and how the scriptblock syntax you are using works with a full user object but not with a PSCustomObject, here:
http://www.powershellish.com/blog/2015-11-17-ad-filter