Monthly Archives: January 2016

Backup User GroupMembership

‘Sup PSHomies,

When implementing or updating a Role Based Access Control (RBAC) model, being able to do a roll back has to be part of your process.

I’ve seen junior admins break out in a sweat when asked to roll back a user’s membership. “Eh… which groups was the user a member of again?”

wp-1454085190357.jpg

It’s a rookie mistake… Hehe…

Here’s how to make a JIT backup before you start changing user membership.


<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Backup User group membership to file on a per user base
#>
[CmdletBinding()]
param(
[string]
$csvFile='users.csv',
[string]
$exportFolder = '.\export\dsa\UserMemberOf\backup\',
[Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]
$Encoding = 'UTF8',
$Delimiter = "`t"
)
#region Verify folder exists
$LogDate = get-date -uformat '%d-%m-%Y'
if(!(test-path "$exportFolder\$logDate")) {
$null = New-Item "$exportFolder\$logDate" -ItemType Directory -Force
}
#endregion
#region Define Hashtables for splatting
$csvParam = @{
Path = ".\source\csv\$csvFile"
Delimiter = $Delimiter
Encoding = $Encoding
}
$exportParam = @{
Delimiter = $Delimiter
Encoding = $Encoding
NoTypeInformation = $true
}
#endregion
#region Main
Import-Csv @csvParam |
ForEach-Object{
Get-ADUser -Identity $_.SamAccountName -Properties MemberOf |
Select-Object -ExpandProperty Memberof |
Get-ADGroup |
Select-Object SamAccountName, DistinguishedName |
Export-Csv @exportParam -Path "$exportFolder\$logDate\$($_.SamAccountName).csv"
}
#endregion

The csv should have a SamAccountName column.

$csvContent = @'
SamAccountName
user1
user2
user3
'@

This will get you all the direct group memberships and save them to a file named SamAccountName.csv, per user.

Next blog I’ll show you how to restore! 😉

Hope it’s worth something to you.

Ttyl,

Urv

Report a user’s nested groupmembership

‘Sup PSHomies,

“The old RBAC…” Role Based Access Control. When done right, pretty awesome! Am I right??? Get it wrong and you’re in a world of pain!

With RBAC, a user is given access to multiple resources using group nesting. It also helps when revoking said access (just remove user from group). I’ve seen many RBAC implementations that started off good, only to be derailed by exceptions. RBAC is an all-or-nothing kinda deal.

There’s a catch to RBAC, while it is awesome, you won’t be able to derive how a user gained access to a resource just by looking at a user’s memberof AD property. Luckily for us there are ways to find out! 😉

Use LDAPFilter, specifically LDAP_MATCHING_RULE_IN_CHAIN


<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Get User nested group membership report
#>
[CmdletBinding()]
param(
[string]
$Identity
)
Get-ADUser -Identity $Identity |
ForEach-Object {
$ldapFilter = '(member:1.2.840.113556.1.4.1941:={0})' -f $_.DistinguishedName
Get-ADGroup -LDAPFilter $ldapFilter |
Select-Object SamAccountName,DistinguishedName
}

If you need to do a quick assessment of all the resources a user has access to either direct or nested then this will get the job done! Another advantage or disadvantage depending on your perspective, is that this approach doesn’t suffer from circular group nesting, but it doesn’t report it either…

I did notice however that I couldn’t derive the relationship of the groups. Was the group directly linked or is it nested in another group? Using the LDAPFilter technique will get you all the unique groups the user is a member of, direct or nested.

This could lead to misinterpretations:

  • Was the group a direct member?
  • Was the group nested via another group?
  • Was the group nested via multiple groups?

I decided to give it another try!


<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Get User nested group membership report
#>
[CmdletBinding()]
param(
[string]
$Identity
)
function Get-ADUserGroups {
param ($Group,$indent,$Parent)
Get-ADGroup –Identity $Group –Properties MemberOf |
Select-Object -ExpandProperty MemberOf |
ForEach-Object {
$GroupName = ($_).Split(',')[0].Split('=')[1]
if(!(($Parent).Contains($GroupName))){
$Newparent = "$Parent/$GroupName"
[PSCustomObject]@{
Group = $GroupName.PadLeft($GroupName.Length + ($indent*5),'_')
Name = $GroupName
NestedLevel = $indent
InheritedFrom = $Parent
}
Get-ADUserGroups -Group $_ -indent ($indent+1) -Parent $Newparent
}
else{
[PSCustomObject]@{
Group = $GroupName.PadLeft($GroupName.Length + ($indent*5),'_')
Name = $GroupName
NestedLevel = $indent
InheritedFrom = "$GroupName/Circulair. Please review"
}
}
}
}
#region Main
Get-ADUser -identity $Identity -Properties MemberOf |
Select-Object -ExpandProperty MemberOf |
ForEach-Object {
$GroupName = ($_).Split(',')[0].Split('=')[1]
[PSCustomObject]@{
Group = $GroupName
Name = $GroupName
NestedLevel = 0
InheritedFrom = $null
}
Get-ADUserGroups -Group $_ -indent 1 -Parent $GroupName
} |
Out-GridView
#endregion

This will get you all the groups, direct and nested, no filter!

UserGroups

The user has access to APL-FTP-Client twice! The user will still have access even if APL-FTP-Client is removed from ROL-Technisch Consultant. APL-FTP-Client is also nested via APL-Total-Commander.

Aha! So that’s why a user still has access…

Now you can do fun stuff like figuring out multiple entry points a user has access…

This is my take on how to get a report on a user’s nested group membership.

I don’t think I need to point out that the script does nothing for reporting where the user is granted access directly, eh? 😉 This is why you always manage resources using groups even for just one user…

Hope it’s worth something to you…

Ttyl,

Urv

 

Pester to validate ADUser properties

‘Sup PSHomies,

See if you can relate. You’re in the middle of a migration, the users need to be created asap. You get a xlsx file with all the necessary properties. A quick Copy/Paste to csv file, Import-Csv  user.csv -Delimiter "`t" | New-ADUser and presto! Whew! Glad we got that out of the way 😉

Feels pretty awesome right? 15 minutes after, your project manager comes asking: “Say, which file did you use?” The one you sent me last week, why? “Uh, there’s a new version on sharepoint, did you use that one?” Well I did ask which file I should use (in my defense I did, that’s why I always email, written proof!). “Well there’s an updated version, could you make sure the users get updated? Thanks!!!” Sigh, here we go again…

At this point I can do two things:

  1. Just delete and recreate. Thing is you’ll loose SIDs and access to homedirectory etc etc. Not exactly ideal.
  2. Update the user properties. Definitely a better option. Still tricky especially using the Set-ADUser cmdlet, but that’s another story.

But before you go off to update the user settings, how about validating what has been changed? Maybe the damage isn’t that bad. I mean if it’s under five changes, I just might  do it manually… Oh who am I kidding? Wait, gimme a minute to catch my breathe from laughing! 😛

Enter Pester for ADUser validation!

With a Pester script to validate your ADUser settings, you’ll never have to second guess if the settings are as they should.


<#
Author: I.C.A. Strachan
Version: 1.0
Version History:
Purpose: Pester script to validate ADUser properties.
#>
[CmdletBinding()]
Param(
[string]
$csvFile = 'Users.csv',
[Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]
$Encoding = 'UTF8'
)
$csvParam = @{
Path = ".\source\csv\$csvFile"
Delimiter = "`t"
Encoding = $Encoding
}
$csvADUsers = Import-Csv @csvParam
$ADPropertiesToVerify = ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name
Foreach ($user in $csvADUsers){
#Get AD User attirbutes
try{
$verify = Get-ADUser -Identity $user.SamAccountName -Properties *
if ($verify) {
Describe "AD User operational readiness for $($user.DisplayName)" {
Context 'Verifying ADUser Attributes'{
ForEach($attribute in $ADPropertiesToVerify){
if (([string]::isNullOrEmpty($user.$attribute))) {
$user.$attribute = $null
}
if($attribute -eq 'Path'){
it "User is located in $($user.$attribute)" {
$verify.DistinguishedName.Contains($user.$attribute)
}
}
else{
it "User property $($attribute) value is $($verify.$attribute)" {
$user.$attribute | Should be $verify.$attribute
}
}
}
}
Context 'Verifying ADUser HomeDirectory Security'{
it 'User HomeDirectory attribute is not empty'{
$user.HomeDirectory | Should not be $null
}
It "Homedirectory $($user.HomeDirectory) exists"{
Test-Path $user.HomeDirectory | Should be $true
}
It "User is owner of $($user.HomeDirectory)"{
(Get-Acl $user.HomeDirectory).Owner| Should be "$($env:USERDOMAIN)\$($user.sAMAccountName)"
}
}
}
}
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
Write-Error -Message "User $($user.SamAccountName) account NOT present"
}
catch {
Write-Error -Message "Unhandled exception looking up $($user.SamAccountName)) account."
throw $_
}
}

Here’s the result:ADPesterResults

Here’s a quick rundown of the script:

First I’ll just get all the user settings using $verify = Get-ADUser -Identity $user.SamAccountName -Properties *.

$ADPropertiesToVerify =  ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name will get me all the properties in the csv file. No need to map properties manually. Now I can loop through any amount of properties!

Next up, making sure empty properties get $null

if (([string]::isNullOrEmpty($user.$attribute))) {
   $user.$attribute = $null
}

$null isn’t equal to empty (Ofcourse you already knew that!)

Now compare what’s in the csv to what Get-ADUser found:

if($attribute -eq 'Path'){
   it &quot;User is located in $($user.$attribute)&quot; {
      $verify.DistinguishedName.Contains($user.$attribute)
   }
}
else{
   it &quot;User property $($attribute) value is $($verify.$attribute)&quot; {
      $user.$attribute | Should be $verify.$attribute
   }
}

Quick note: I used Path to create the user in a specific OU. There’s no Path property in Get-ADUser. So I did the next best thing, just verify that path is part of the user’s distinguishedname 😉

I also added a little bonus to verify the user’s homedirectory exists and that the user is also the owner.

Being able to validate will definitely give you peace of mind…

Hope it’s worth something to you

Ttyl,

Urv

Enumerations in PowerShell

‘Sup PSHomies,

I’m trying to get a grip on PowerShell Classes and all the new stuff we have available in version 5.

So June Blender posted this link in one of her tweets by @psCookieMonster. Guess Enum is a good place to start!

Using .NET enumerations to improve a user’s experience is definitely worth investing in, less error prone. I got a great tip how to create Custom Intellisense experience by @pcgeek86

I’ve used validation sets in the past. My “Aha” moment was the fact that you can just as easily typecast using the enumeration. You should check out powershell.com tip on the subject.

So I can create my own Enum and use it as a validation set? Let’s give it a try!


Enum PizzaSize {
Small = 10
Medium
Regular
Large = 3
ExtraLarge = 4
}
function Get-PizzaSize{
param(
[PizzaSize]
$PizzaSize
)
"You chose $PizzaSize"
}

view raw

Enum.ps1

hosted with ❤ by GitHub

Hey! who doesn’t love a good pizza eh? 😉

Ok, there’s an upside and downside to this approach. The upside is that it keeps your code clean. The downside is that it has to be available prior to using. Another point to consider is that this is Syntax is only available in version 5.

Note: Enumerations are case-sensitive.

As luck would have it, I was perusing Microsoft’s pester scripts GitHub repository when I came across Pester for Classes! Here’s one from the pester scripts on Enumeration found in scripting.enums.tests.ps1

enum E3{
   e0
   e1 = 5
   e2 = ([int]::MaxValue) - 1
   e4
   e3 = 1    # This shouldn't be an error even though previous member was max int
}

So what’s going here? Well for starters I never paid much attention to the value being assigned. The first entry is assigned the value 0 if it hasn’t been given one. The second will be an increment of +1 of the first, that is, if it hasn’t been assigned another value.

So what are the current values?

[Enum]::GetValues('E3') | ForEach-Object {'{0} {1}' -f $_, ([E3]$_).value__ }

E3Values
e2 has the MaxValue of [Int] minus one. e4 wasn’t assigned a value yet. Its value is an increment of e2 (the MaxValue of [Int]). If you try to add a new entry after e4, say e5, you’ll receive an error.

enum E3{
   e0
   e1 = 5
   e2 = ([int]::MaxValue) - 1
   e4
   e5
   e3 = 1    # This shouldn't be an error even though previous member was max int
}

e5error

Have a look at the PizzaSize enumeration, what are the values actually?

[Enum]::GetValues('PizzaSize') | ForEach-Object {'{0}: {1}' -f $_, ([PizzaSize]$_).value__ }

PizzaSizeValues
Medium and Regular are incrementals of Small, Who knew! 😛

Want to make sure the enumeration has been defined?

[enum]::IsDefined([PizzaSize],'large') #Will be False case-sensitive
[enum]::IsDefined([PizzaSize],'Large') #True
[PizzaSize]10 #Returns Small
[PizzaSize]3  #Returns Large

#These will all return Large
[PizzaSize]::Large
[PizzaSize]'Large'
[PizzaSize]3

How about if I wanted the value of the day of the week, say Saturday?

([System.DayOfWeek]'Saturday').value__

[Enum]::GetValues('System.DayofWeek') | ForEach-Object {'{0}: {1}' -f $_, ([System.DayofWeek]$_).value__ }

Well, off to a good start when it comes to Enumerations!

Hope it’s worth something to you…

Ttyl,

Urv