Powershell and Active Directoy

I have a powershell script that returns various user details (to be used in a report). User details will include ID, Name, email, manager’s name, managers email. IT has been requested that I also include the manager’s manager name and email as well. What would be the easiest to do that? I have included the query below that I am using so far.

$UserDetails =  $AllmembersData | Get-AdUser -Properties mail, samaccountname, manager | Select-object SamAccountName, Name, Mail, @{Name='Manager';Expression={ (($_ |Get-ADUser -Properties manager).manager | Get-ADUser).Name}}, @{name="ManagerMail";expression={get-aduser $_.manager -properties mail | select -ExpandProperty mail}}| Sort-Object -Property SamAccountName -Unique

Any help would be appreciated.

To make your life (potentially) easier - and for the code to be easier to read, i would push the properties into an object:

$UserDetails = @()
foreach($user in $AllmembersData){
    $manager = Get-aduser -Identity $user.manager
    $ManagersManager = get-aduser -Identity $user.manager
    $UserDetails += new-object PSCustomObject -Property @{
        SamAccountName = $user.samaccountname
        Mail = $user.mail
        Manager = $manager.name
        ManagersEmail = $manager.mail
        ManagersManager = $ManagersManager.name
        ManagersManagerEmail = $ManagersManager.mail
    }
}

Note: I have not tested that code (not on a DC right now) so take it with a pinch of salt.

If you have never used objects before, take a read of the documentation, as its super handy to use:
about PSCustomObject - PowerShell | Microsoft Docs

Although @alex-innes approach is better than OP’s one-liner, it still queries AD twice for each user to get the manager details. This is not efficient, particularly if we’re working with thousands of users. Once we have the manager’s details we should store them and reuse them and give our poor AD servers a rest.

My approach would be something like this:

$Level1Managers = @{}
$Level2Managers = @{}

$UserDetails = foreach ($Member in $AllMembersData) {

    $User = Get-ADUser $Member -Properties Mail,Manager
    $L1Manager = $User.Manager

    if ($L1Manager -notin $Level1Managers.Keys) {
        $L1ManagerDetails = Get-ADUser $L1Manager -Properties Mail,Manager
        $Level1Managers.Add($L1Manager,$L1ManagerDetails)
    }
    
    $L2Manager = $Level1Managers.$($L1Manager).Manager
    
    if ($L2Manager -notin $Level2Managers.Keys) {
        $L2ManagerDetails = Get-ADUser $L2Manager -Properties Mail,Manager
        $Level2Managers.Add($L2manager,$L2ManagerDetails)
    }

    [PSCustomObject] @{
        Name           = $User.Name
        sAMAccountName = $User.SamAccountName
        Mail           = $User.Mail
        Manager        = $Level1Managers.$($L1Manager).Name
        ManagerEmail   = $Level1Managers.$($L1Manager).Mail
        L2Manager      = $Level2Managers.$($L2Manager).Name 
        L2ManagerEmail = $Level2Managers.$($L2Manager).Mail    
    }

}
1 Like

Matt, you are absolutely right. My one-liner gets a little busy, and inefficient. Both of these are great. We are teaching an old dog new tricks. I have recently just started Powershell, so learning something everyday. I put yours in, and it appeared to work flawlessly, for the most part. I got a number of errors during the running of the code. There were various identity errors, as well as various null errors. I suspected I new what was causing it, just not sure how to fix it. I suspected it was accounts that did not have a manager listed. Therefore, it would error on those. (I imaging it is possible there could be also the manager could be listed, but maybe that manager might not of had a manager listed). To test this theory, I simply added two lines of code for write hosts to write the $User on one line and their $L1Manager on the second line. Sure enough, I would get an error on those that the manager wasnt listed. On these records, I would get errors at various places within the L1Managers and L2Manager lines of code.

My question, if this occurs (at either the 1st level, or up into the second level)m how can I output the nulls as “No Data Found” for allowing the code to run. The records where the data is complete, they are exporting with no issue; the records where there are errors, the record is not exporting.

Again, I do appreciate this.

Matt’s suggestion is still inefficient as it is needlessly querying AD again. Take all users and create a lookup table. Since the manager attribute is a DistinguishedName, we will make this property the lookup key.

$alluserlist = Get-ADUser -Filter * -Properties mail, manager

$userlookup = $alluserlist |
    Group-Object -Property DistinguishedName -AsHashTable

Now we just break out the logic. Check for manager, if so check for their manager, each time just referencing the very efficient hashtable. Any user that doesn’t have a manager or a l2manager will end up with N/A for that property.

$results = foreach($user in $alluserlist){
    if($user.manager){
        $manager = $userlookup[$user.manager]

        $l2manager = if($manager.manager){
            $userlookup[$manager.manager]
        }
        else{
            @{
                Name = 'N/A'
                Mail = 'N/A'
            }
        }
    }
    else{
        
        $manager = @{
            Name = 'N/A'
            Mail = 'N/A'
        }
        
        $l2manager = @{
            Name = 'N/A'
            Mail = 'N/A'
        }
    }

    [PSCustomObject]@{
        Name           = $User.Name
        sAMAccountName = $User.SamAccountName
        Mail           = $User.Mail
        Manager        = $manager.Name
        ManagerEmail   = $manager.Mail
        L2Manager      = $l2manager.Name 
        L2ManagerEmail = $l2manager.Mail    
    }
}

$results | Format-Table -Auto

$results | Export-Csv \path\to\file.csv -NoType
1 Like

You’ve assumed the OP wants to report on all users, I assumed they’re reporting on a subset of users ($allMembersData is already populated in OP’s example.

I do agree your approach is much more efficient if a report for all users is required.

You are correct, Matt, the variable is already a populated subset of users (essentially this script is a reporting tool I created for VDI users). I thought I was thru, until my Dir stated it woukd be nice to return the 2nd level managers for the users (those 2nd level managers would essentially be his peers) which would give more visibility as to the usage across departments and divisions.

So, at this point I am still needing to factor for null. Would the logic above work for your script format, or would there need to be other considerstions?

The same concept would work with a portion of ad users. It doesn’t have to be all users.

That only works if the managers are in the list of users though, doesn’t it?

Take OP’s example, where he’s auditing VDI users and has a list of those users, and wants to get the details of their manager, and the manager’s manager.
If the managers don’t use VDI, they won’t be in his initial list so he won’t have their e-mail details - you’ll need an extra AD look up to get those, won’t you?

Sure, @krzydoug’s logic works for that. Just check if the property exists, and if it doesn’t add your own. You could just add a dummy entry to the hashtable to keep the object creation the same:

$Level1Managers = @{}
$Level2Managers = @{}

$NoManagerDetails = @{
    Name = 'N/A'
    Mail = 'N/A'
}
$Level1Managers.Add('NoManager',$NoManagerDetails)
$Level2Managers.Add('NoManager',$NoManagerDetails)

$UserDetails = foreach ($Member in $AllMembersData) {

   $User = Get-ADUser $Member -Properties Mail,Manager

   if ($User.Manager) {

       $L1Manager = $User.Manager

        if ($L1Manager -notin $Level1Managers.Keys) {
            $L1ManagerDetails = Get-ADUser $L1Manager -Properties Mail,Manager
            $Level1Managers.Add($L1Manager,$L1ManagerDetails)
        }

        if ($Level1Managers.$($L1Manager).Manager) {
    
          $L2Manager = $Level1Managers.$($L1Manager).Manager
    
            if ($L2Manager -notin $Level2Managers.Keys) {
                $L2ManagerDetails = Get-ADUser $L2Manager -Properties Mail,Manager
                $Level2Managers.Add($L2manager,$L2ManagerDetails)
            }
        }
        else {
            $L2Manager = 'NoManager'
        }
    }
    else {
        $L1Manager = 'NoManager'
        $L2Manager = 'NoManager'
    }

    [PSCustomObject] @{
        Name           = $User.Name
        sAMAccountName = $User.SamAccountName
        Mail           = $User.Mail
        Manager        = $Level1Managers.$($L1Manager).Name
        ManagerEmail   = $Level1Managers.$($L1Manager).Mail
        L2Manager      = $Level2Managers.$($L2Manager).Name 
        L2ManagerEmail = $Level2Managers.$($L2Manager).Mail    
        
    }

}

MAtt, I beleive that will work. I havent run it yet, but I will later when I am home. One thing I do notice, I believe that this will return duplicate values. We do have users that are members of multiple VDI entitlements. $UserDetails just supposed to users that are entitled. Not duplicate if they are in multiple groups. Period. I am aware of this, because when I first created this, I ran into it with that single line that you have gotten me away from . The way that I solved it was if you look at the end of the single line is the Select statement. The solution was adding this to the end of the Select statement:

Sort-Object -Property SamAccountName -Unique

How would I incorporate that in this format?

By the way, guys, thanks. I will be definitely changing my other code into this format going forward.

Ideally, do it before you start processing users by having only unique accounts in $AllMembersData.

Otherwise, you could pipe $UserDetails to Sort-Object -Unique

$UserDetails = $UserDetails | Sort-Object -Property sAMAccountName -Unique

I see. However you’re not realizing what I’m saying. I’m saying you still get all users (or at least at the scope from where all users/managers will be included) and then you iterate over your small list referencing the lookup table. You make one call to ad and you forego any additional collecting and referencing of managers. Unless OP has AD users more than say 10k, this would be much more efficient imo.

And so was there a final result for @tomlon ?
:-0)
I was just curious as to how PS is used in AD…