Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
924 views
in Technique[技术] by (71.8m points)

powershell - How to effectively use the `-Filter` parameter on Active Directory cmdlets?

All too often I see the following type of code on this site, specific to the AD cmdlets:

Get-ADUser -Filter * | Where-Object { $_.EmailAddress -eq $email }

The problem is that you are returning every single user object in Active Directory, and then processing it a second time. How can we improve upon this, not only to reduce the time it takes to run the script, but to also take the unnecessary load off of Active Directory, and possibly the network?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Note about Azure AD cmdlets

This answer is crafted around the Active Directory cmdlets installed and available from Remote Server Administration Tools (RSAT). However, the Azure AD cmdlets make use of Microsoft Graph (OData v4.0 specification) to run queries against Azure AD while the RSAT cmdlets[1] rely on an implementation of the PowerShell Expression Engine intended to replace LDAP filters.

As such, the filter examples below will not work with Azure AD cmdlets without some modification to be compatible with the Microsoft Graph specification, specifically, its filter syntax. However, the general practices mentioned here should still apply.

[1] - This is the most recent version of this document I could find.

What is so bad about -Filter *?

You are effectively selecting and returning every object that exists in AD, based on the cmdlet you are using (e.g. Get-ADUser, Get-ADComputer, Get-ADGroup, the generic Get-ADObject, etc.). This is an expensive thing to do, especially in larger AD environments. It is fine to do this if you legitimately need to operate on every possible object, but in most cases you do not need to return everything. On top of this, your script will end up processing far more data than it needs to, increasing execution time and used processing time when it just isn't necessary.

The -Filter parameter can do more than just match on everything, which is effectively what
-Filter * does. The -Filter string is very much like Powershell syntax (not quite, but most of the way there). You can use most of the same logical operators that Powershell supports, and they work much in the same way that Powershell operators do. This answer aims to clarify this and explain how to use this elusive parameter. These examples will use the Get-ADUser cmdlets but this also extends to the other Get-ADObject cmdlets which use filters as well.


Syntax

The syntax for the -Filter string is "PropertyName -comparisonoperator 'somevalue'", though you can string multiple conditions together with logical operators such as -and and -or. Note that there are no regex matching operators, so you will have to make do with -like and -notlike globbing.

Comparison Operators

MS calls these FilterOperators but they are used in the same way as PowerShell's comparison operators are (ignoring the fact that technically -bor and -band are arithmetic operators). These are used for comparing values:

Note: AD attributes in DistinguishedName format will not have globbing applied when
-like or -notlike are used, in other words you have to look for an exact match. If you need a DN to match any pattern, this cannot be performed with -Filter or
-LDAPFilter. You will have to -Filter where you can, and perform additional processing with the -like or -match operators once your Get-ADObject cmdlet returns.

-eq, -le, -ge, -ne, -lt, -gt, -approx, -bor, -band, -recursivematch, -like, -notlike

The only ones which are unique to the -Filter query syntax are -approx and -recursivematch. Don't worry about -approx, it is functionally equivalent to -eq in Active Directory.

Despite its name, -recursivematch is not a regex matching operator, it works like PowerShell's
-contains operator in that it will return $true if the collection contains the target value.

Logical Operators

MS calls these JoinOperators but they fill the same role as their PowerShell logical operator equivalent. These are used to join multiple conditions together in a single query:

-and, -or

Strangely enough, MS gives negation to a special operator type called NotOperator, which consists of a single operator:

-not


Matching on a property

To use the example in the question, let's find a user matching an email address, but without piping to Where-Object (crazy right???):

$email = 'box@domain.tld'
Get-ADUser -Filter "EmailAddress -eq '${email}'"

Done. Get-ADUser will return any accounts where the EmailAddress property equals whatever the $email variable is.

What if we want to find all user accounts that haven't been logged onto in the last 30 days? But a date string is more complex than an email! Who cares, still pretty simple!

# Get the date from 30 days ago

$notUsedSince = ( Get-Date ).AddDays( -30 )
Get-ADUser -Filter "LastLogonDate -lt '${notUsedSince}'"

This returns all users who have not logged on in the last 30 days.


Getting users who are members of a group

If you want to get all ADUsers who are members of a certain group, we can make use of the
-recursivematch operator for this:

Get-ADUser -Filter "memberOf -recursivematch 'CN=test_group,CN=Users,DC=exampledomain,DC=net'"

memberOf is an array of Distinguished Names, -recursivematch returns true if the array on the lefthand side contains the value on the righthand side.

You can alternatively avoid the use of Get-ADUser at all in this scenario and use Get-ADGroup to retrieve the members from that:

( Get-ADGroup group_name -Properties Members ).Members

While the Get-ADGroup example above is shorter to type, filtering on memberOf with Get-ADUser can be effective when you have multiple conditions and need to return users that , but not necessarily need to return it for local processing. It may be interactively inconvenient but it is a worthy technique in any automated process integrating with Active Directory and may become necessary in cases where you have extremely large groups.

One example is when enumerating Domain Users in a very large domain. You might want to rethink returning 32,000 users from ( Get-ADGroup ).Members to then have to apply additional filtering.

Note: Most users will actually have Domain Users set as their PrimaryGroup. This is the default and most times this doesn't need to be changed. However, you must use -Filter on PrimaryGroup instead as the PrimaryGroup is not stored under MemberOf for an ADUser. It is also a single value, not a collection, so use -eq:

Get-ADUser -Filter "PrimaryGroup -eq 'PRIMARY_GROUP_DN'"

What if the query term contains a quote?

Quotes in the query term will throw a wrench in your query in most cases. Consider the example of searching for users with O'Niel in the name. This can break either the query or your script logic depending on the quoting technique used:

# Our heroic search term
$term = "O'Niel"

# Dragons abound (results in a query parsing error)
Get-ADUser -Filter "Name -like '*${term}*'"

# Your princess is in another castle ($term is not expanded
# and the literal string ${term} is instead searched for) 
Get-ADUser -Filter 'Name -like "*${term}*"'

In this case you will have to use double-quoted strings in both places, but fortunately the escape-hell isn't too bad. Using the same value for $term as before:

# Your quest is over (this works as intended and returns users named O'Niel)
Get-ADUser -Filter "Name -like ""*${term}*"""

# Backticks are ugly but this also works
Get-ADUser -Filter "Name -like `"*${term}*`""

Note: If your query looks for a field value which contains both single and double quotes, I'm not sure how to facilitate this with one command when using the -Filter parameter. However, -LDAPFilter should be able to facilitate this, as parentheses (), not quotes, are used for the internal query bounds. See the Filter Examples in about_ActiveDirectory_Filter and the LDAP Filters section of this AD Escape Characters post for more information, as -LDAPFilter is beyond the scope of this answer.


Matching on multiple properties

Matching on multiple properties is not much different, but it's best to wrap each condition in parentheses (). Here's an example, let's find non-domain admin accounts (assuming we know this by the username nomenclature *-da) that don't have an email address associated with them.

Get-ADUser -Filter "(samaccountname -notlike '*-da') -and (EmailAddress -notlike '*')"

This one is a little trickier, because we can't pass in an empty value for the right side of a condition in the -Filter, as in the case for EmailAddress. But '*' matches any non-empty value, so we can leverage that behavior with the -notlike comparison operator to find empty values for EmailAddress. To break down the filter, make sure that any accounts ending in -da aren't matched by the filter, and then also only match accounts that do not have an EmailAddress value.


Things to avoid

  1. Don't try to use a { ScriptBlock } for your filter parameters. Yes, we are all more comfortable with writing a ScriptBlock than worrying about building a string and making sure it's escaped properly. There is definitely an attraction to using them. I've seen so many answers using a ScriptBlock as a -Filter argument, or people having problems (myself included) trying to do something like this, and SURPRISE!!! Nothing gets returned:

    Import-Csv C:userInfoWithEmails.csv | Foreach-Object {
      Get-ADUser -Filter { EmailAddress -eq $_.Email }
    }
    

    -Filter doesn't support ScriptBlocks, but


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...