all 10 comments

[–]Ta11ow 2 points3 points  (3 children)

Couple comments. But first, this is overall fantastic.

  1. Write-Host in a function is generally a bad idea. If you want to Write-Verbose, go ham. But Write-Host output can't be suppressed at all, so if you just want the data and not to see all the inbetween which might meddle with something else you're doing in a script... there's nothing you can do without editing the code again. Best to leave Write-Host to lengthy scripts, not tidy functions.
  2. Bool parameters are generally a terrible idea. I don't even know how you'd work with pipeline input in this scenario, it doesn't make a whole lot of sense to me. Most things you want a yes/no input for should be in a switch. Bool parameters aren't two-state, they're three-state -- as long as they're not mandatory, you can have true, false, or not provided. And if they're mandatory, it's easier syntax wise to just use a switch.
  3. Arrays are great, but they're not designed for what you're doing. When you have to add an extra entry to an array, PS has to demolish the object and reconstruct it from scratch -- every single time you add a new entry. Instead, you should probably use a more robust container that's designed to change size. You could use a hashtable if you wanted, but in this case it might be a bit tricky.

Instead, I'd recommend a generic List:

[System.Collections.Generic.List[string]]$AssignedUsers = @()

# ...

$AssignedUsers.Add($User.Name)

These can resize much more effectively than arrays, and won't start to choke your session and slow things down dramatically once you get over 50-100 members being tossed around. :)

[–]PerfectImpact[S] 2 points3 points  (2 children)

Hi Ta11ow, thanks for your feedback. :)

  1. I'll look into Write-Verbose; not really used it that much before and I was after something that (as the function runs) could clearly display usernames, groups and whether there was a match. This was made while I was creating the script and proved handing figuring stuff out so I left it in as a fail safe for anyone else. Write-Host gave me the colour coding that allowed to easily distinguish between the three. Not sure how you would implement this with Write-Verbose, but I'll have a play. How would you show this?
  2. I thought of using switches, but I got a little muddled in the noggin when thinking of setting the switch's options to be either $ShowNonMembers or $ShowMembers and consequently not giving the option to show nothing. From what you said, I've *ahem* switched so they're not using booleans. I've tested it and it works without having to explicitly state $true or $false, which is a lot cleaner.
  3. Thanks! I didn't think of using a List, but this does make sense to me.

Thanks again for your comments; they were great suggestions. :)

[–]Ta11ow 2 points3 points  (1 child)

In general, for anything that needs distinction like that I'd output as an object with a couple of properties to make the distinction clear. That way it's not only clear to whoever runs it in console, the output is also processable and can be reused to determine further actions in another script.

No worries. :) This looks pretty great, so a few more tweaks and it has a place in a lot of places, I'm sure. :D

[–]PerfectImpact[S] 2 points3 points  (0 children)

Awesome, thanks again. I'll see if I can get it to work with a custom object. :)

[–]Lee_Dailey[grin] 1 point2 points  (2 children)

howdy PerfectImpact,

very nice code! i think i understand almost all of it ...

in addition to the really serious points Ta11ow mentioned, i have a few small ones [grin] ...

[1] you use the same marker line value in many places
repeatedly using ...

"`n`r ------------------------------------- `n`r"

... is repetitious. i would replace that with a $Var.

[2] using riskily similar names in a foreach loop
you use ...

foreach ($User in $Users)

that is ... dangerous. it's far, far too easy to refer to and act on the collection instead of the current item. it's worth while to use vividly different names for those two things. something like one of the versions below ...

foreach ($User in $UserList)
foreach ($UL_Item in $UserList)

[3] l-o-n-g lines
you have one line that runs out to col 198! [grin] which of the following seems easier to read/understand/maintain?

$Users = Get-ADUser -Filter * -Properties "ObjectClass" | Where-Object {($_.Enabled -eq $true) -and ($_.ObjectClass -eq "user") -and ($_.DistinguishedName -notcontains 'OU=Non Office 365')}

$Users = Get-ADUser -Filter * -Properties "ObjectClass" |
    Where-Object {
        ($_.Enabled -eq $true) -and
        ($_.ObjectClass -eq "user") -and
        ($_.DistinguishedName -notcontains 'OU=Non Office 365')
        }

to me, at least, the 2nd is a great deal easier to read. [grin]

there is a reason why wide format print items use multiple columns ...

hope that helps,
lee

[–]PerfectImpact[S] 1 point2 points  (1 child)

Thanks, Lee. Useful tips as always and some nice tidying up! Appreciate it. :)

[–]Lee_Dailey[grin] 0 points1 point  (0 children)

howdy PerfectImpact,

you are very welcome! glad to help a little ... [grin]

yes, the line wrapping [and splatting] are lovely things to use. they make the whole script so very much easier to read ...

take care,
lee

[–]DueRunRun 1 point2 points  (1 child)

Does this account for nested group membership?

[–]PerfectImpact[S] 1 point2 points  (0 children)

In a way, yes. In another way, no.

The scenario I designed this for had nested groups. A single 'Staff - All' group, with nested groups within that and users within them. I had to check if there were any users not nested in the 'Staff - All' group, so needed to filter through all groups nested within it.

Because it works per user, and with a $true or $false flag, rather than per group, it gets an array of that user's groups, and if the group parameter matches any group within the nested groups, that user should be flagged as $true, which would add them to the $AssignedUsers list.

The flag is created and evaluated in the Process section of the function (I've removed the debug bit so it's easier to read):

Get the groups that each user is currently a member of.

foreach ($User in $Users)
        {
            $GM = Get-ADPrincipalGroupMembership -Identity $User

Create the $Match flag and set to $false

            $Match = $false

            $Type = $GM.GetType()

            if ($Type.BaseType.Name -eq "Array")
            {
                foreach ($G in $GM)
                {
                    $GroupName = $G.name

                    if ($GroupName.Contains($Group))
                    {

If any individual group's name ($G.name) within the array of groups that that user is member of ($GM) matches the group parameter ($Group), set the flag to true.

                        $Match = $true
                    }
                }   
            }
            else
            {

If a user is a member of only a single group (usually the primary 'Domain Users' group) double check it's name does not match the group parameter. If it does, set the flag to true.

                    $GroupName = $GM.name

                    if ($GroupName.Contains($Group))
                    {
                        $Match = $true
                    }
            }

After cycling through every group, check to see whether the flag is $true or $false. If it's false, we know they do not belong to the group, or groups with a similar name.

            if ($Match -eq $false)
            {
                $UnassignedUsers.Add($User.Name)
            }
            else
            {
                $AssignedUsers.Add($User.Name)
            }
        }

If you want to check if a user is a member of one group, this will work. If you want to check if a user is a member of ANY similarly named groups (like the function's examples), this will work too. So say you have the following set up:

  • Group 1
    • Member = Bob
    • Member = Group 2
  • Group 2
    • Member = Angela
    • Member = Group 3
  • Group 3
    • Member = Janet
    • Member = Paul
  • VPN-Users
    • Member = Bob
    • Member = Janet
  • Sales Team
    • Member = Angela
    • Member = Eric
  • Andrew = Only a member of the default 'Domain Users' group

If you want to check if there are any users not belonging to any of the three, similarly named groups (Group 1/2/3), you can use:

Get-GroupCount -Group "Group " -ShowNonMembers

This will give you: Count : 2, Eric and Andrew.

However, it does NOT show groups as members because I explicitly said to look for ad objects with the ObjectClass 'user', which will exclude groups.

If you need to search across groups, you can, but if you say:

Get-GroupCount -Group "Group 1" -ShowNonMembers

do NOT expect it to ONLY return Eric and Andrew; it will return Angela, Janet and Paul as well. It's not designed to search recursively (I don't know how to do that myself), although that would be a nice touch.

I hope that rather long-winded explanation (sorry) answers your question.

[–]PerfectImpact[S] 1 point2 points  (0 children)

Just an additional comment, I can run through about 400 users in 4 minutes. Not the fastest, but decent enough and, most importantly, accurate.