Saturday, May 11, 2013

"Dynamic" AD Security Groups

Have you ever needed a Security Group to be dynamically populated? Unfortunately this isn't a standard feature in AD DS... Powershell to the rescue!

In this example we will populate a Security Group with all enabled users in a specific OU. Users that are disabled or moved to another OU since the last time the script ran will be removed from the group.

You could fairly easily modify the script to match other situations e.g. you need to populate a group with users who have the Office attribute set to Brussels or something... You could use any property available.

So first we need to import the Active Directory module

Import-Module ActiveDirectory

Note that in Powershell V3 (which is required for this script to work) you do not have to explicitly load modules... see Arcane Code's post on this.

Next we define the variables for the Security Group and the OU we want to interrogate

$SGroup = "BE - Users"
$Searchbase = "OU=xxx,OU=xxx,OU=xxx,DC=xxx,DC=xxx"

Note: You can easily find the distinguished name of an OU in the properties of the OU in ADUC or ADSI Edit (Attribute Editor tab - DistinguishedName)

Next we retrieve the members of the AD Group and the OU and we keep the properties we need

$UserGroup = Get-ADGroupMember -Identity $SGroup -recursive | Select-Object Name,SamAccountName

$UserOU = Get-ADUser -Filter * -Properties Name,SamAccountName,Enabled -SearchBase $Searchbase | Select-Object Name,SamAccountname,Enabled

For reporting purpose let's add two empty collections, one for the objects removed and one for the ones added

$ReportRemoved = @()
$ReportAdded = @()

In the first If statement in the Foreach ($User in $UserOU) loop we remove the users who are disabled from the Security Group, create a PSObject for reporting purposes and add it to the $ReportRemoved collection

    If ($user.Enabled -eq $False -AND $UserGroup.SamAccountname -contains $user.SamAccountname)
    {
    Remove-ADGroupMember -Identity $SGroup -Members $user.SamAccountname -WhatIf

    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountname
        Enabled = $user.Enabled
        Action = "Removed from $SGroup"
        Reason = "User is disabled"
        }

    $ReportRemoved += $Entry
    }

Notice the "WhatIf" parameter for the Remove-ADGroupMember cmdlet... it is not a bad idea to verify what will be removed first. I suggest you to test on a test group first as well and making sure everything works as expected. Also note that for the ($UserGroup.SamAccountname -contains $user.SamAccountname)condition we need Powershell V3.

Next we will add users from the $UserOU collection that are enabled and don't already belong to the Security Group

    If ($user.Enabled -eq $True -AND $UserGroup.SamAccountname -notcontains $user.SamAccountname)
    {
    Add-ADGroupMember -Identity $SGroup -Members $user.SamAccountName -WhatIf
    
    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountName
        Enabled = $user.Enabled
        Action = "Added to $SGroup"
        Reason = "User is present in the searchbase and is enabled" }

    $ReportAdded += $Entry
    }
}

Next let's iterate through the Security Group and remove the user objects that are not in the Organizational Unit. So in a foreach ($user in $UserGroup) loop

    If($UserOU.SamAccountName -notcontains $user.SamAccountName)
    {
    Remove-ADGroupMember -Identity $SGroup -Members $user.SamAccountname -WhatIf

    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountname
        Enabled = "n.a"
        Action = "Removed from $SGroup"
        Reason = "User didn't exist in the searchbase"
        }
    $ReportRemoved += $Entry
    }

If something is added or removed I like to know what... so for the reporting part

If ($ReportRemoved.count -ge 1 -or $ReportAdded.count -ge 1)
{
    $color = '"#347235"'

    if ($ReportRemoved.count -lt 1)
    {
    $ReportRemoved = "<font color=$color>No users have been removed from $SGroup</font>"
    }
    else
    {
    $ReportRemoved = $ReportRemoved | ConvertTo-Html -Fragment
    }

    if ($ReportAdded.count -lt 1)
    {
    $ReportAdded = "<font color=$color>No users have been added to $SGroup</font>"
    }
    else
    {
    $ReportAdded = $ReportAdded | ConvertTo-Html -Fragment
    }

$HTMLmessage = @"  
    <html>
    <head> 
    <style type="text/css">
    <!--
    body {
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
    font-size: 12px;
    }
        table{
           border-collapse: collapse;
           border: none;
           font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
           color: black;
           margin-bottom: 10px;
    }

        table td{
           font-size: 12px;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }

        table th {
           font-size: 12px;
           font-weight: bold;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }    
    -->
    </style>
    </head>

    <body>
    <h4><b>Security Group "$SGroup" Maintenance</b></h4>
    Searchbase = $Searchbase<br>
    <h5>Users added to the Security Group</h5>
    $ReportAdded
    <h5>Users removed from the Security Group</h5>
    $Reportremoved    
    <br><br><br>
    <small>This automated report ran on $env:computername at $((get-date).ToString())</small>
    </body>
    </html>
"@

If the variable $ReportRemoved is empty we add the string "No users have been removed from $SGroup" in a nice green color... If $ReportAdded is empty we add "No Users have been added to $SGroup".
If they are not empty we convert their content to HTML with the ConvertTo-Html cmdlet.

In the body part of the $Htmlmessage we compose our HTML message.
Notice the "<small>This automated report ran on $env:computername at $((get-date).ToString())</small>" part... I always add this to HTML reports so I know on which machine the script is scheduled to run on.

Next we just need to send the HTML mail and we are done!

Send-MailMessage -From "Security Group $SGroup Maintenance<sender@yourdomain>" -To "Your Name<youremailprefix@yourdomain>"`
-Subject "Security Group $SGroup Maintenance" -Smtpserver yoursmtpserver -body $HTMLmessage -BodyAsHtml
}

Above will of course only work if your mail server accepts SMTP connections from the host the script is run on and it accepts messages from the sender specified but that is another topic. In my environment it is very simple... the ip range IT uses is allowed to relay unauthenticated ;-) If you need to authenticate you will need to add the -Credential parameter.

The complete script looks like this (because of the code that has got to do with reporting it is quite lengthy... leave it out if you don't need it)

# -----------------------------------------------------------------------
#
#       Author    :   Baldwin D.
#       Description : Security Group Maintenance
#       Requirements:   Powershell V3
#       
# -----------------------------------------------------------------------

##Modules
Import-Module ActiveDirectory

##Variables
$SGroup = "BE - Users"
$Searchbase = "OU=xxx,OU=xxx,OU=xxx,DC=xxx,DC=xxx"

##Initialize
$UserGroup = Get-ADGroupMember -Identity $SGroup -recursive | Select-Object Name,SamAccountName
$UserOU = Get-ADUser -Filter * -Properties Name,SamAccountName,Enabled -SearchBase $Searchbase | Select-Object Name,SamAccountname,Enabled

$ReportRemoved = @()
$ReportAdded = @()

##Main
Foreach ($user in $UserOU) 
{
    #Verify if the user is disabled and if so remove him from the Security Group
    if ($user.Enabled -eq $False -AND $UserGroup.SamAccountname -contains $user.SamAccountname)
    {
    Remove-ADGroupMember -Identity $SGroup -Members $user.SamAccountname -WhatIf

    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountname
        Enabled = $user.Enabled
        Action = "Removed from $SGroup"
        Reason = "User is disabled"
        }

    $ReportRemoved += $Entry
    }

    #Add users that are enabled and don't already belong to the group
    if ($user.Enabled -eq $True -AND $UserGroup.SamAccountname -notcontains $user.SamAccountname)
    {
    Add-ADGroupMember -Identity $SGroup -Members $user.SamAccountName -WhatIf
    
    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountName
        Enabled = $user.Enabled
        Action = "Added to $SGroup"
        Reason = "User is present in the searchbase and is enabled" }

    $ReportAdded += $Entry
    }
}#end foreach

foreach ($user in $UserGroup)

    #if the user isn't in the searchbase OU, remove him from the Security Group
    if($UserOU.SamAccountName -notcontains $user.SamAccountName)
    {
    Remove-ADGroupMember -Identity $SGroup -Members $user.SamAccountname -WhatIf

    $Entry = New-Object PSObject -Property @{
        Name = $user.Name
        SamAccount = $user.SamAccountname
        Enabled = "n.a"
        Action = "Removed from $SGroup"
        Reason = "User didn't exist in the searchbase"
        }
    $ReportRemoved += $Entry
    }
}#end foreach

#Send Report if users are added or removed
if ($ReportRemoved.count -ge 1 -or $ReportAdded.count -ge 1)
{
    $color = '"#347235"'

    if ($ReportRemoved.count -lt 1)
    {
    $ReportRemoved = "<font color=$color>No users have been removed from $SGroup</font>"
    }
    else
    {
    $ReportRemoved = $ReportRemoved | ConvertTo-Html -Fragment
    }

    if ($ReportAdded.count -lt 1)
    {
    $ReportAdded = "<font color=$color>No users have been added to $SGroup</font>"
    }
    else
    {
    $ReportAdded = $ReportAdded | ConvertTo-Html -Fragment
    }

$HTMLmessage = @"  
    <html>
    <head> 
    <style type="text/css">
    <!--
    body {
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
    font-size: 12px;
    }
        table{
           border-collapse: collapse;
           border: none;
           font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
           color: black;
           margin-bottom: 10px;
    }

        table td{
           font-size: 12px;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }

        table th {
           font-size: 12px;
           font-weight: bold;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }    
    -->
    </style>
    </head>

    <body>
    <h4><b>Security Group "$SGroup" Maintenance</b></h4>
    Searchbase = $Searchbase<br>
    <h5>Users added to the Security Group</h5>
    $ReportAdded
    <h5>Users removed from the Security Group</h5>
    $Reportremoved    
    <br><br><br>
    <small>This automated report ran on $env:computername at $((get-date).ToString())</small>
    </body>
    </html>
"@

Send-MailMessage -From "Security Group $SGroup Maintenance<sender@yourdomain>" -To "Your Name<youremailprefix@yourdomain>"`
-Subject "Security Group $SGroup Maintenance" -Smtpserver yoursmtpserver -body $HTMLmessage -BodyAsHtml
}

A report looks like this



If you need the script for this situation you only need to modify the lines colored in red.

If everything is as you expect it to be you can remove the -Whatif parameters to effectively perform the operations.

If you would have to perform the operations on multiple groups or OU's you can pour the main part in a function or you can modify the script to accept command line parameters.

You can then schedule the script using Task Scheduler to run once a day for example... if something has changed since the last time the script has run the Security Group gets updated and you will receive a mail ;-)

For your scheduled task:
Program: "C:\Windows\System32\WindowsPowerShell\v1.0\Powershell.exe"
Arguments: &'C:\Scripts\AD - Group Maintenance.ps1'

If you should have any questions or comments... let me know.

Grts!

2 comments:

  1. Hi
    This is almost same that I want to achieve, but with Computer objects.
    Would it be hard to change the script to do this for Computer objects?
    I'm a amateur in PowerShell, therefor I ask.
    Thank you for great blog.

    ReplyDelete

My Blog List