ARS bugs and annoyances – Managed Unit not sorted

In reshaping my deployment of ARS 7 I’ve made extensive use of the dynamic objects ARS provides, i.e. Managed Units and Dynamic groups.  Both of these are defined by a set of membership rules.  In doing so I came across one limitation ( or bug ) and one annoyance.  I’d like these to be ‘Feature Requests’ for the next version of ARS.

  • The bug – objects in a Managed Unit are not sorted
  • The annoyance – You cannot rename the membership rules in a dynamic object

The bug ( although I suspect Quest / Dell / Quest / One Identity, never thought about this ) is that if I use a Custom Include Query that displays the OUs below a target ‘searchRoot’ the OUs are not displaed in any order and there is no control over this, e.g. If I target a users OU and under this OU there is an OU for each country the MU displays the countries in a random order.  If you want to try this out  use this query as a membership rule ‘(&(objectCategory=organizationalUnit)(street=DisplayOUInMU))’ where I tag the OUs street attribute with either ‘DisplayOUInMU’ or ‘Don’tDisplayInMU’  I also have a 3rd setting ‘DisplayObjectsinMU’ which allows me to also display the objects in the OU in the MU.

I think that the MU should by default always sort the objects it displays in alphabetical order.  In case you were wondering why I don’t just add the OUs implicitly there are two reasons, one, there are a lot of them and two, what if we add another country OU, I wanted to make the MU automatically pick it up.  I have a fix for this by the way, add the dynamic rule but also add the explicit OU objects that already exist in the OU that you want to display.  Any new OUs will get the correct ‘street’ attribute value as I use an ARS policy to update the street attributed based on the parent OU.  The new OUs won’t be sorted so you will need to go and update the MU membership rules although now I am writing this I could write an ARS Policy script to automate this but I’ll wait a little in case One Identiry decide to add this feature / bug fix to the next ARS version.

The annoyance – You cannot rename the membership rules in a dynamic object.  This should be an easy thing to allow in the same way as you can rename the PVG rules in an ARS Policy.  I have dynmaic objects with 3 of even 4 ‘custom searches’ wouldn’t it be nice to be able to give these a meaningful name so you don’t have to open each one when you need to modify it?

Advertisements

How to locate your ARS servers using the service connection point

In case you need to know which servers to connect to using Connect-QADService and you don’t want your script to have hard coded domain information in so the code is more portable, i.e. will run in any environment I came up with a script to locate the available ARS servers using the service connection points published into AD by the ARS servers.

Quest / Dell / Quset / On Identity still havent update the path and use the products original ( Enterprise Directory Manager) name so the SCP are located here: CN=Enterprise Directory Manager,CN=Aelita,CN=System,<Domain DN> Version 6 didn’t have a port number but ARS 7 did.  I don’t know if this is unique to my environment as I was running the two ( ARS 6.9 and 7.x ) services in parrallel or if this was a hard coded change from ARS 7.   If you use this script and find the port is different in your environment let me know.

Call the function like this 

 $ARSServerFQDN = Get-ARS7Servers | select -First 1 

or without the select statement if you want to see all the servers.  You can then use this to control which server you are connecting to 

 connect-QADService -service $ARSServerFQDN -proxy
Function Get-ARS7Servers { # Version 2.00
 $searchRoot = "CN=Enterprise Directory Manager,CN=Aelita,CN=System,$([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().GetDirectoryEntry() | select -ExpandProperty DistinguishedName)" 
 Get-QADObject  -SearchRoot  $searchRoot -Type serviceConnectionPoint | SELECT  -ExpandProperty Name | Where { $_.indexOf(":17228") -gt 0 } | Select @{name='serverName';expression={$_.split(":")[0]}} | select -ExpandProperty serverName
}            # Get-ARS7Servers                Version 2.00

Documenting ARS delegated permissions

One of the killer reasons to use ARS is the ease with which you can answer the two auditor questions, who can manage that group / OU / user and what can that user  manage.

ARS provides another layer between the security principle and the AD object.  This layer is an Access template.  An access template is a list of rights that are delegated to objects.  You delegate rights to a security principle via the template.   If you update the template then all the links where the template is used are also updated.  Lets say you create a telephone number template ( called user-telephone Numbers ) that allows security principles to edit the telephone number of user objects.  You delegate this right to the security principle, ‘Telephony Admins’ by linking the security principle via the new template ‘user-Telephone Numbers’ to 3 different user OUs in AD.   Doing this natively in AD is a bit of a chore because you need to select the same rights on each OU location separately.  If you then wanted to change the rights to include say the mobile number attribute in an AD only world you would first need to check where the original ‘right’ was delegated and then apply a second ACL to all the OUs, can be difficult, tedious and error prone for sure.  In an ARS managed environment you simply update the template and the rights will change on all of the location where you have used the template.

When it comes to answering the audit questions, just view the object you are interested and select the administration tab.  There are three buttons, but the interesting ones in the context of this blog are:

  • Security – shows who can administer the object
  • Delegation – shows what objects the user can administer

AdminTab

What if the auditor wants a document of the rights being delegated

They usually ask for screenshots although I’m not sure why.  Anyway I wanted a way to export this information into a CSV file so I could compare files later to see if anything had changed and also to use as a backup allowing me to restore rights if they had changed.  If I get to send these reports to the Auditors then thats a bonus.

ARS includes a commandlet that will make this really easy to do:

Get-QARSAccessTemplateLink

There is a trustee parameter that would make this faster I suspect but I could not get this working so I just added a where clause into the pipeline.

Get-QARSAccessTemplateLink -Proxy |
 select DirectoryObjectDN,
        AppliedTo,
        Trustee,
        TemplateDN,
        SynchronizedToAD,
        DN |
 where { $_.Trustee.NTAccountName -eq "MyDomain\adminlandrews" }

Now you can pipe that into the Export-Csv and you will have everything you need to show the auditors the delegated rights given to a user.

Remove the where clause and the report will include all trustees and it can then be manually filtered to show any rights delgated to or for objects in AD.

I have actually taken this a bit further and added a front end GUI to the commands using PrimalForms and in about 4 lines of code that I wrote, rather than the 1000s written by Primal Forms I have something that can:

  • Report on a trustees rights
  • Clone a trustee rights to another security principle
  • Remove a trustees rights
  • Replace a Trustee with another Trustee
  • Backup and Restore settings applied to a security Principle
  • Backup and Restore all permissions

Oh and just one more thing…….. ARS admins don’t show up in any of the delegation reports as they have full access to everything, so you need to make sure you tell the auditors this fact and a list of ARS admins of course.

 

PowerShell Profiles and why I don’t use them

The idea behind a PowerShell profile is that you can customise your PowerShell environment and have your system remember the setup the next time you open a PowerShell prompt / ISE.

What happens when you send your script to someone else?

It’s actually quite a cool idea and you can make sure all your PowerShell modules are loaded in the profile too.  The problem I see with this is that now your script has some hidden dependencies.  These are the modules etc. that you loaded into your PowerShell profile.

Unless the person you send the script to has the same modules loaded in their PowerShell profile the script won’t work

I find it better to just add the couple of lines to import the module into every script I write.  This makes sure that the script is portable assuming of course that the modules are available on the the users system where they are running the script.  You could of course write more code to download and install the module but lets not get carried away just write a message to the screen explaining why the script won’t run and let them source the required modules.

I use this function in my scripts and then handle the return value in my main script

 

function Get-ModuleStatus { # Version 2.00  
 [CmdletBinding()]
 param (
  [parameter(Mandatory=$true , HelpMessage="Enter the Module Name, e.g. ActiveRolesManagementShell")]
  [string]$name,
  [parameter(Mandatory=$false, HelpMessage="Optionally Enter the Version Number, e.g. 7.2")]
  [string]$Version,
  [switch]$forceVersion  
 )
 if ( $version ) { 
  if ( $forceVersion ) {
   if ( $module = Get-Module -Name $name | where { $_.version.ToString() -eq $Version } ) { Return $true }
  }
  else {
   if ( $module = Get-Module -Name $name | where { $_.version.ToString() -ge $Version } ) { Return $true }
  }
  if ( $module = Get-Module -name $name ) {
   # wrong version loaded so unload 
   Remove-Module -Name $name 
   $module = $null 
  }
 }
 elseif ( $module = Get-Module -name "$name" ) { Return $true }
 if ( $version ) { 
  try { Import-Module -Name $name  -MinimumVersion $version | Out-Null  }
  catch { return $false	}
 }
 else {
  try { Import-Module -Name "$name" } 
  catch { return $false	}
 }
 Return $true 
}           # Get-ModuleStatus           Version 2.00

here is an example of how to call and handle the error

if ( ( Get-ModuleStatus "ActiveRolesManagementShell" ) -eq $false ) { # load the quest cmdlets
 $message = "ActiveRolesManagementShell could not be loaded SCRIPT HALTING on $($Env:COMPUTERNAME) - Please investigate"
 $emailParameters.Add("body",$message)
 $emailParameters.Add("Subject","$scriptName Script FATAL ERROR - Unable to Load ARS COMMANDLETS on $($Env:COMPUTERNAME)")
 Stop-ScriptRun -emailParameters $emailParameters -sendMail -stop -throwMessage "FATAL ERROR - Unable to Load ARS COMMANDLETS"
} # throw if we are unable to load the Quest cmdlets

Forcing Admins to do the right thing

No I don’t mean enrolling them in the ‘Guido school of Admins‘ I mean getting them to do the right thing without even telling them a thing.  I mean preventing bad behaviour by using features of ActiveRoles Server.

Moving Objects around in AD can cause major issues!

Moving objects around in AD can cause major issues especially with LDAP based applications which always seem to , hard code the object DNs.

ARS Managed Units can present a virtualised OU structure – Bruce Lee might have referred to this by saying  “The art of Moving objects without moving objects”  or was it the “Art of fighting without fighting” – Enter the Dragon.

ARS Managed Units allow you to virtualise an AD OU Structure making it simpler to delegate rights and also provides a more logical view of AD and of course a less cluttered view for the admins as they only see the objects they need to manage.

Another advantage is that administrators cannot create new objects in a Managed Unit. A managed Unit can contain an OU but you can’t create objects in it.

What this means is that you can prevent an administrator creating a new parent OU and more importantly they can only move objects between the OUs you expose, i.e. when possible they can move the object from the ‘incorrect’ location to the correct OU location.

You could achieve similar results with a complex delegation model but lets stop for a minute here, the AD is already a mess and you can’t move the objects around for fear of breaking things so here’s a solution that makes them appear in the right place when they are not.

The solution then is to create a Managed Unit that has a membership rule that includes only the immediate child objects of the targeted OU.

This simple LDAP query achieves this

(&(objectCategory=organizationalUnit)(street=DisplayOUinMU))

All I need to do now is set the street attribute on all the objects in the target OU using a simple powershell script and create an ARS policy that ensures that any objects created or moved to the OU get this value too which is a very simple PVG policy.  The policy sets the attribute to the parentOU attribute value.

Then I set some additional membership rules to display any objects not in the target OU, i.e. are in the wrong OU path and Bobs your uncle.

Now when an admin users looks, he sees all the objects in the right location, almost anyway 🙂 The objects in the target OU ( the Managed Unit with the same name as the Target OU) are mostly from the wrong locations BUT the admin can no longer create more objects in that location only in the correct OUs exposed via the MU.

This is driving the correct behavior with little management or even training overhead. Without this I found that admins, despite being told time and time again, would create more objects in the wrong OUs because related groups or service accounts were already in the wrong OU location.

Putting objects in an OU ‘just because’ is up there with cloning a user object for the new start ARRRRGGGHHHHH don’t do it! ( Please start working on a roles based design today)

I’ve requested a Product ENHANCEMENT with OneIdentity ( Quest ) as I think it would be good to have this as an an include rule in a similar way to the include group member rule.   Then you would not need the PVG policy to set the attribute you would just include immediate child objects of the targeted OU.  Even more useful would be the ability to further filter that to specific object types, e.g. just the users or groups or perhaps just the OUs.  Maybe they will include it in the next release you never know.

Extracting Photos from AD

This post is actually about the ‘DontConvertValuesToFriendlyRepresentation’ switch on get-qaduser but I came across this because I was trying to extract the photos from AD so that’s how I ended naming the post Extracting Photos from AD as most people will probably be search for this and not the command line switch.

Getting the photo from AD is pretty simple but there are a couple of things to know. When you upload the photo to AD it’s converted from a jpeg to a array of bytes so you can’t just download it you have to convert it back. the Quest commandlets are helpful and covert lots of the raw data stored in AD into more readable formats. What this means is that sometimes the help is more of a hindrance because the value you wanted for the photo has been converted so then the byte conversion fails ‘[System.Io.File]::WriteAllBytes( $Filename,$photoAsBytes )’

There are two solutions to this. The first is to just access the directory entry like this ‘$user.DirectoryEntry.thumbnailPhoto.Value’ and the second is to tell the commandlet not to convert the values by using the ‘DontConvertValuesToFriendlyRepresentation’ switch.

And as I was comparing the speed of the AD commandlets I extracted the thumbnailPhoto attribute using both the AD and Quest commandlets. The AD commandlets are faster but not by much as long as you use the ‘-DontUseDefaultIncludedProperties’ The quest commandlets pull down lots of attributes which is why it takes longer so when getting lots of AD objects it’s worth using this switch too.


cls
$ldapFilter = "(&(employeeID=*)(sAMAccountType=805306368)(thumbnailPhoto=*)(!(|(userAccountControl:1.2.840.113556.1.4.803:=2))))"
$searchRoot = "OU=User Accounts,DC=MyADDomain,DC=com"
$useADCommandlets = $false 
$sizelimit = 0
$OutputPath = 'c:\Temp\Photos'
Function ConvertTo-Jpeg {
 param ($userName,$photoAsBytes,$path='c:\temp')
 if ( ! ( Test-Path $path ) ) { New-Item $path -ItemType Directory }
 $Filename="$($path)\$($userName).jpg"
 [System.Io.File]::WriteAllBytes( $Filename,$photoAsBytes )
}

if ( $useADCommandlets ) {
 #Import-Module ActiveDirectory
 $Users = GET-ADUser -LDAPFilter $ldapFilter  -Properties thumbnailPhoto # | select -First $sizelimit # remove the select to get all users 
 ForEach ( $User in $Users ) {
  ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.thumbnailPhoto -path $OutputPath 
 }
}
else {
 $Users = get-qaduser  -LdapFilter $ldapFilter -SearchRoot $searchRoot -DontUseDefaultIncludedProperties -DontConvertValuesToFriendlyRepresentation  -IncludedProperties thumbnailphoto -SizeLimit $sizelimit   # set sizelimit to 0 to get all users
 ForEach ( $User in $Users ) {
  #ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.DirectoryEntry.thumbnailPhoto.Value -path $OutputPath # if you didn't use the -DontConvertValuesToFriendlyRepresentation switch 
  ConvertTo-Jpeg -userName $user.SamAccountName -photoAsBytes $user.thumbnailPhoto -path $OutputPath
 }
}

Checking your ARS Scheduled tasks ran OK

How do you know that all of your ARS scheduled tasks have completed successfully?

Well the only way provided by Quest is to look at the scheduled task information provided in the MMC.   You’ll notice there are three columns

  • name
  • Last Run Time
  • Last Run Message

Sadly the Last Run Message will generally be empty as it only displays errors so perhaps this should have been named Last Error Message!

The Last Run Time is useful but do you know when it was last supposed to run? To find that out you need to open the properties and read the schedule.

All of that aside though you have to remember to check!

For my ARS scheduled tasks I have always used the Last Run Message as a way of easily telling that the task ran without errors.

“All of my scheduled task scripts THROW an error on purpose!

This might seem a little odd but I force errors using “throw” so that I can write a message on the Last Run Message column.

This has worked well, but I still had to check that the script ran on schedule so I decided to write an audit script that will read the Last Run Messages, the last run time and understand the next run time and email me if it finds an unexpected error or that the script missed the last scheduled run time.

Getting the Last Run Message is simple, a task object has a property called ‘edsaLastActionMessage’ which contains the droid we are looking for.  ( A very sad reference to Star Wars – sorry couldn’t resist )

It would therefore be very easy to code an audit script to look for specific text to confirm the task had completed successfuly.

If I hard code the messages though I’ll need to update the audit script every time
I modify the scheduled task script or added a new scheduled task script.

I also didn’t want to be limited to specific text I could THROW in my scheduled task script so I decided to use a new parameter that I add to each scheduled task I want to audit, called LastRunLine.  This is dynamically updated by the script when it runs so is always correct even when I edit the script, i.e. I don’t need to remember to update it manually.

The first task is to get all of the scheduled tasks like this :

$taskParameterList = @(
 'name'
 'edsaDisableSchedule'
 'edsaXMLSchedule'
 'edsaLastRunTime'
 'edsvaNextRunTime'
 'edsaLastActionMessage'
 'edsaTaskState'
 'edsvaIsReadyToTerminate'
 'edsvaServerNameToExecute'
 'edsvaTaskStateString'
 'DN'
 'ParentContainerDN'
)
$tasksOU  = "CN=Clan8,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration" ' this is actually a parameter
$tasks     = Get-QADObject -SearchRoot $tasksOU -IncludedProperties $taskParameterList -Type edsScheduledTask | select $taskParameterList

We can then loop around the tasks we found like this : foreach ( $taskInfo in $tasks ) {

In each loop we can check if the task is currently running and it it is we have to ignore it as the last run message will be blank

if ( $taskInfo.edsvaTaskStateString -eq 'Task started its execution' ) {
Continue
}

We get the parameters of the task like this:

$strParameters = Get-ScheduledTaskParameters $taskInfo.DN
$lastRunTime = $taskInfo.edsaLastRunTime
$nextRunTime = $taskInfo.edsvaNextRunTime

I’m only interested in a task if I have a parameter called lastrunLine
if ( ! ( $lastRunLineNo = [string]$xmlParameters.parameters.lastRunLine ) ) {
Continue
}

Now we can check if we got a last run message and compare this with the expected string lastRunLineNo
Based on this outcome I can raise an alert or not.

An example last run message is: ‘At Line:425 Char:1 Found 60 users’ The first part of this string is automatically created by ARS.

I check the Line number the script ‘failed’ on with the LastRunLine parameter. If it matches then the task completed succesfully.

When getting the task details I also get the last run time and the next run time, ‘edsaLastRunTime’ and ‘edsvaNextRunTime’

This allows me to check if the task has missed it’s run time and raise an alert.

Lastly because I want my audit script to run every hour it would repeatedly report the same errors and ideally I only want this to be reported once.
To solve this issue I manually update the last run message prefixing it with ‘ERROR:’ using:

Set-QADObject $taskInfo.DN -ObjectAttributes @{edsalastactionmessage="ERROR: '$($taskInfo.edsaLastActionMessage.Trim())'"} -proxy

Here’s the full script. If you run this in an editor it will automatically set the debug parameters and display progress messages to screen as it goes.
This prevents you accidentily uploading the script to ARS with the debug parameter still set to true

#region Script Header : Audit-ScheduledTasks 
$script:scriptVersion = "2.00" 
$script:scriptName    = "Audit-ScheduledTasks_v$script:scriptVersion"
$script:Owner         = "lee.Andrews@mydomain.com" # used to send script specific error messages
$script:taskDN        = "CN=Audit-ScheduledTasks,CN=Audit,CN=Clan8,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration"
$thisLastRLNo         = 441 # update this line when you update the script
$errorInRun           = $true 
if ( $Task.DirObj ) {
 $script:ShowDebug = $false
}
else {
 $script:ShowDebug = $true
 cls 
 $padright = 100 
}
#endregion Script Header : Audit-ScheduledTasks
#region Set default SMTP variables
$emailParameters =@{
 "smtpServer"="smtp.,mydomain.com"
 "To"=$script:Owner
 "From"="NoReply@mydomain.com"
}
#endregion Set default SMTP variables
#region Version Control Information
<# 
  Version 1.00 Original
  Version 1.01 Automatically sets debug variable using the task object 
  Version 1.02 Adds Report Header and simplifies audit check 
  Version 1.03 Updated debug routines and auto update of the last run message if script is in Debug mode
  Version 2.00 Now uses the ARS 7.0 commandlets 
#>
#endregion Version Control Information
#region Helper functions
function Get-CurrentLineNumber {
 $MyInvocation.ScriptLineNumber
}
function Get-ModuleStatus { # http://www.ehloworld.com/938
	[CmdletBinding(SupportsShouldProcess=$true)]
	param (
		[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, HelpMessage="Enter the Module Name, e.g. Lync")]
		[string]$name
	)
	if(!(Get-Module -name "$name")) {
		if( Get-Module -ListAvailable | ? {$_.name -eq "$name"} ) {
			try { 
				Import-Module -Name "$name" 
				return $true
			}
			catch { return $false	}
		} 
		else { return $false }
	}
	else { return $true	}
} # end function Get-ModuleStatus
function Get-TaskParameters {
 function Convert-OctetStringToGuid {
  param (
   [Parameter(Mandatory=$True,Position=1)][string]$Guid
  )
  if(32 -eq $guid.Length) {
   [UInt32]$a = [Convert]::ToUInt32(($guid.Substring(6, 2) + $guid.Substring(4, 2) + $guid.Substring(2, 2) + $guid.Substring(0, 2)), 16)
   [UInt16]$b = [Convert]::ToUInt16(($guid.Substring(10, 2) + $guid.Substring(8, 2)), 16)
   [UInt16]$c = [Convert]::ToUInt16(($guid.Substring(14, 2) + $guid.Substring(12, 2)), 16)

   [byte]$d = ([Convert]::ToUInt16($guid.Substring(16, 2), 16) -as [byte])
   [byte]$e = ([Convert]::ToUInt16($guid.Substring(18, 2), 16) -as [byte])
   [byte]$f = ([Convert]::ToUInt16($guid.Substring(20, 2), 16) -as [byte])
   [byte]$g = ([Convert]::ToUInt16($guid.Substring(22, 2), 16) -as [byte])
   [byte]$h = ([Convert]::ToUInt16($guid.Substring(24, 2), 16) -as [byte])
   [byte]$i = ([Convert]::ToUInt16($guid.Substring(26, 2), 16) -as [byte])
   [byte]$j = ([Convert]::ToUInt16($guid.Substring(28, 2), 16) -as [byte])
   [byte]$k = ([Convert]::ToUInt16($guid.Substring(30, 2), 16) -as [byte])

   [Guid]$g = New-Object Guid($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)
   return $g.Guid;
  }
  else {
   throw Exception("Input string is not a valid octet string GUID")
  }
 } # end function Convert-OctetStringToGuid 
 try {
  $Task.DirObj.GetInfo() 
  $Script:taskDN         = $($Task.DirObj.Get('distinguishedName')) # set to the actual task DN in case we trigger the catch clause
  $Task.DirObj.GetInfoEx(@('edsaParameters'),0)  #  if no parameters are specified this command will trigger the catch clause
  $strParameters         = $Task.DirObj.Get('edsaParameters')
  $Script:taskDNDefault  = "Scheduled Task...: $($Task.DirObj.Get('distinguishedName')). `r`n"  
  $byteString            = $Task.DirObj.Get('edsaModule')
  $byteString            = [BitConverter]::ToString($byteString)
  $OctetString           = $byteString.replace('-','')
  $taskModule            = Convert-OctetStringToGuid -Guid $OctetString 
  $script:scriptName     = $(Get-QADObject $taskModule -proxy  ).name 
 }
 catch {
  try {
   $Task                 = Get-QADObject -Identity $script:TaskDN  -proxy  -IncludedProperties edsaParameters,edsaModule 
   $strParameters        = $Task.edsaParameters
   $Script:taskDNDefault = "Scheduled Task...: $script:taskDN`r`n"  
   $taskModule           = Convert-OctetStringToGuid $Task.edsaModule 
   $script:scriptName    = $(Get-QADObject $taskModule -proxy  ).name 
  }
  catch {
   return 'Unable to get task parameters'
  }
 }
 $strParameters = '<parameters>' + $strParameters + '</parameters>' 
 if ( Test-Path variable:Task        ) { Remove-Variable -Name Task        -ErrorAction SilentlyContinue }
 if ( Test-Path variable:byteString  ) { Remove-Variable -Name byteString  -ErrorAction SilentlyContinue }
 if ( Test-Path variable:OctetString ) { Remove-Variable -Name OctetString -ErrorAction SilentlyContinue }
 return $strParameters
}
function Set-TaskParameters {
 param (
  [Parameter(Mandatory=$True,Position=1)]$htParameters,
  [Parameter(Mandatory=$True,Position=2)]$taskDN,
  [Parameter(Mandatory=$True,Position=3)]$connection 
 )
 $htParameterList = @()
 foreach ( $htParameter in $htParameters.GetEnumerator() ) {
  $htParameterList += $('<'+$htParameter.key+'>'+$htParameter.value+'</'+$htParameter.key+'>')
 }
 $null = Set-QADObject $taskDN -ObjectAttributes @{edsaParameters=$htParameterList} -proxy 
}
function Stop-ScriptRun {
 param (
  [Parameter(Mandatory=$True ,Position=1)][hashtable]$emailParameters,
  [Parameter(Mandatory=$True ,Position=2)][string]$throwMessage, 
  [Parameter(Position=3)][allowNull()][string]$logfile,
  [switch]$sendMail,
  [switch]$stop 
 )
 $emailBody = $emailParameters.item("body").split("`r`n")
 if ( ( $logfile ) -and ( Test-Path $logfile ) ) {
  foreach ( $line in $emailBody ) {
   $line | Out-File -FilePath $logfile -Append 
  }
 }
 try {
  if ($sendMail) {
   Send-MailMessage @emailParameters -Encoding ([Text.Encoding]::UTF8)
  }
  $returnMessage = "$throwMessage" + " " + $emailParameters.Item("Subject") + " :"
 }
 catch { $returnMessage += "$throwMessage UNABLE TO SEND EMAIL - $($emailParameters.Item('Subject')) :" }
 if ( $stop ) { 
  throw $returnMessage 
 }
 return $returnMessage
}
function Get-ScheduledTaskParameters {
 param ( $taskDN ) 
 try {
  $Task                 = Get-QADObject -Identity $taskDN  -proxy  -IncludedProperties edsaParameters,edsaModule 
 }
 catch {
  return 'Unable to get task parameters'
 }
 $strParameters        = $Task.edsaParameters
 $strParameters = '<parameters>' + $strParameters + '</parameters>' 
 if ( Test-Path variable:Task        ) { Remove-Variable -Name Task        -ErrorAction SilentlyContinue }
 return $strParameters
}
#endregion Helper functions
#region Connect to ARS service
if ( ( Get-ModuleStatus "ActiveRolesManagementShell" ) -eq $false ) { # load the quest cmdlets
 $message = "ActiveRolesManagementShell could not be loaded SCRIPT HALTING - Please investigate"
 $emailParameters.Add("body",$message)
 $emailParameters.Add("Subject","$scriptName Script FATAL ERROR - Unable to Load ARS COMMANDLETS")
 Stop-ScriptRun $emailParameters -stop -throwMessage "FATAL ERROR - Unable to Load ARS COMMANDLETS" -sendMail
} # throw if we are unable to load the Quest cmdlets
$script:hostname = $env:COMPUTERNAME
$script:hostCode = "$($script:hostName.substring(0,1))$($script:hostName.substring($($script:hostName.length-2),2))"
try { $proxy = Connect-QADService -Proxy -Service "PLONWBQSTS20.Clan8.ad.Clan8.com"  }
catch { 
 $subject = "FATAL ERROR: $script:hostname '$script:scriptName' Line No. $($error[0].InvocationInfo.ScriptLineNumber) - Unable to Connect to ARS server"
 $emailParameters.Add('body',"************* $script:hostname : Line $($error[0].InvocationInfo.ScriptLineNumber) - FATAL ERROR Unable to Connect to ARS server ******************" )
 $emailParameters.Add('Subject',$subject)
 Send-MailMessage @emailParameters -Encoding ([Text.Encoding]::UTF8)
 throw $subject
}
#endregion Connect to ARS service
#region Get Task Parameters
$strParameters      = Get-TaskParameters # returns an XML object containing all of the scheduled task parameters
if ( $strParameters -eq "Unable to get task parameters" ) {
 $emailParameters.Add("body","************* Line $($error[0].InvocationInfo.ScriptLineNumber) - FATAL ERROR: Failed to get script parameters ******************")
 $emailParameters.Add("Subject","$scriptName Script FATAL ERROR - Unable to get TASK PARAMETERS")
 Stop-ScriptRun $emailParameters -stop -throwMessage "FATAL ERROR - Unable to get TASK PARAMETERS" -sendMail 
}
$xmlParameters = [xml]$strParameters     
$emailAlert    = [string]$xmlParameters.parameters.emailAlert
$emailFrom     = [string]$xmlParameters.parameters.emailFrom 
$latestRunDate = [string]$xmlParameters.parameters.latestRunDate
$tasksOU       = [string]$xmlParameters.parameters.tasksOU
$smtpServer 	  = [string]$xmlParameters.parameters.smtpServer
$lastRunLine   = [string]$xmlParameters.parameters.lastRunLine
#endregion Get Task Parameters
#region set defaults if any paramters are missing
$defaultUsed = $false 
if ( $emailAlert  -eq '' ) { $defaultUsed = $true ; $emailAlert  = $script:Owner } else { $emailAlert = $emailAlert.split(",") }
if ( $emailFrom   -eq '' ) { $defaultUsed = $true ; $emailFrom   = $script:Owner } 
if ( $latestRunDate -eq '' ) { $defaultUsed = $true ; $latestRunDate = (Get-Date -Hour 0 -Minute 00 -Second 00).addDays(-1) } else { $latestRunDate = $(Get-Date $latestRunDate -Format "dd/MM/yyyy HH:mm:ss") }
if ( $lastRunLine -eq '' ) { $defaultUsed = $true ; $lastRunLine = $thisLastRLNo } 
if ( $tasksOU     -eq '' ) { $defaultUsed = $true ; $tasksOU     = "CN=Clan8,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration" } 
if ( $smtpserver  -eq '' ) { $defaultUsed = $true ; $smtpserver  = "smtp.Clan8.com" }
if ( [string]$xmlParameters.parameters.scriptOwner -eq "" ) {
 $defaultUsed = $true 
}
else {
 if ( $script:Owner -ne [string]$xmlParameters.parameters.scriptOwner ) {
  $script:Owner = [string]$xmlParameters.parameters.scriptOwner
 }
}
#endregion set defaults if any paramters are missing
#region Upload any missing default parameters to the calling scheduled task
if ( $defaultUsed ) {
 $taskParameters = @{
  'emailAlert'  = $($emailAlert -join ",")
  'emailFrom'   = $emailFrom
  'lastRunLine' = $lastRunLine
  'latestRunDate' = $(Get-Date $latestRunDate -Format "dd/MM/yyyy HH:mm:ss")
  'ScriptOwner' = $script:Owner 
  'smtpserver'  = $smtpserver
  'tasksOU'     = $tasksOU 
 }
 $Error.Clear()
 Set-TaskParameters -htParameters $taskParameters -taskDN $script:taskDN -connection $proxy
 $emailParameters.Add("body","Missing Parameter - Scheduled Task Updated and script halted.  Please run again after checking the parameters are set correctly")
 $emailParameters.Add("Subject","$scriptName Script Missing Parameter")
 Stop-ScriptRun $emailParameters -stop -throwMessage "Missing Parameter - Please run again after checking the parameters are set correctly" -sendMail
}
#endregion Upload any missing default parameters to the calling scheduled task
#region Set Script Runtime Parameters
$taskParameterList = @(
 'name'
 'edsaDisableSchedule'
 'edsaXMLSchedule'
 'edsaLastRunTime'
 'edsvaNextRunTime'
 'edsaLastActionMessage'
 'edsaTaskState'
 'edsvaIsReadyToTerminate'          
 'edsvaServerNameToExecute'
 'edsvaTaskStateString'
 'DN'
 'ParentContainerDN'
)
$tasks  = Get-QADObject -SearchRoot $tasksOU -IncludedProperties $taskParameterList -Type edsScheduledTask | select $taskParameterList
$report = ""
$fullReport = ""
$updateRunDate = $false 
if ( $script:showDebug ) {	Write-Host "".padright($padright,"=") -ForegroundColor Green -BackgroundColor Black }
$fullReport  = "================================================================================================================= `n"
$fullReport += "Script Name.......: '$script:scriptName' `n"
$fullReport += "Script Version....: '$script:scriptVersion' `n"
$fullReport += "Host Server.......: '$($env:COMPUTERNAME)' `n" 
$fullReport += "================================================================================================================= `n"
foreach ( $taskInfo in $tasks ) {
 if ( $taskInfo.Name -eq "Update-ManagedDomains" ) {
  $bp = 0 
 }
 if ( $taskInfo.edsvaTaskStateString -eq 'Task started its execution' ) { 
  $skippingMsg = "Skipping task $($taskInfo.name) as it is currently running"
  $fullReport += "$skippingMsg `n"
  $fullReport += "----------------------------------------------------------------------------------------------------------------- `n"
  if ( $script:showDebug ) {
   Write-Host "$skippingMsg".padright($padright) -ForegroundColor Gray -BackgroundColor Black 
   Write-Host "".padright($padright,"-") -ForegroundColor Green -BackgroundColor Black 
  } # if ( $script:showDebug ) {
  continue 
 } # if ( $taskInfo.edsvaTaskStateString -eq 'Task started its execution' ) {
 #Region Reset loop variables 
 $loopReport = ""
 $NumberStart = $null
 $NumberEnd   = $null 
 $errorInLoop = $false 
 #EndRegion Reset loop variables 
 if ( $taskInfo.edsaDisableSchedule ) { 
  $skippingMsg = "Skipping $($taskInfo.name) as it's disabled" 
  $fullReport += "$skippingMsg `n"
  $fullReport += "----------------------------------------------------------------------------------------------------------------- `n"
  if ( $script:showDebug ) {	
   Write-Host "$skippingMsg".padright($padright) -ForegroundColor Gray -BackgroundColor Black 
   Write-Host "".padright($padright,"-") -ForegroundColor Green -BackgroundColor Black 
  }
  continue 
 }  
 #Region  Set task variables
 $xmlData           = [xml]$taskInfo.edsaXMLSchedule
 $lastRunTime       = $taskInfo.edsaLastRunTime 
 $nextRunTime       = $taskInfo.edsvaNextRunTime 
 #if ( ( Get-Date (Get-Date) -Format d ) -eq (get-date $nextRunTime -Format d ) ) {
 $strParameters     = Get-ScheduledTaskParameters $taskInfo.DN  # returns an XML object containing all of the scheduled task parameters
 $xmlParameters     = [xml]$strParameters     
 if ( ! ( $lastRunLineNo     = [string]$xmlParameters.parameters.lastRunLine ) ) { 
  $skippingMsg = "Skipping task $($taskInfo.name) as it does not have the lastRunNo parameter so is out of scope"
  $fullReport += "$skippingMsg `n"
  $fullReport += "----------------------------------------------------------------------------------------------------------------- `n"
  if ( $script:showDebug ) {	
   Write-Host "$skippingMsg".padright($padright) -ForegroundColor Gray -BackgroundColor Black 
   Write-Host "".padright($padright,"-") -ForegroundColor Green -BackgroundColor Black 
  }
  continue 
 }
 $taskInfoNameMsg  = "Task Name.........: '$($taskInfo.Name)'"
 $taskContainerMsg = "Task Folder.......: '$($taskInfo.ParentContainerDN.Replace("",$tasksOU"",'').substring(3))'"
 $taskServerMsg    = "Task Server.......: '$($taskInfo.edsvaServerNameToExecute)'"
 if ( $script:showDebug ) {	Write-Host "$taskInfoNameMsg".padright($padright)  -ForegroundColor Green -BackgroundColor Black }
 if ( $script:showDebug ) {	Write-Host "$taskContainerMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
 if ( $script:showDebug ) {	Write-Host "$taskServerMsg".padright($padright)    -ForegroundColor Green -BackgroundColor Black }
 $loopReport += "$taskInfoNameMsg `n"
 $loopReport += "$taskContainerMsg`n"
 $loopReport += "$taskServerMsg`n"
 try { $lastActionMessage = "'$($taskInfo.edsaLastActionMessage.Trim())'" } 
 catch { 
  $bp = 0 
 }
 #EndRegion  Set task variables
 #Region Get LastRunLineNo Position  
 if ( $lastActionMessage.length -gt 0  ) { 
  if ( ( $lastActionMessage.IndexOf("ERROR") -ge 0 ) -and ( $taskInfo.DN -ne $script:taskDN ) ) {
   $taskErrorMsg = "Task ERROR already reported so skipping"
   if ( $script:showDebug ) {	Write-Host "$taskErrorMsg".padright($padright) -ForegroundColor Yellow -BackgroundColor Black }
   if ( $script:showDebug ) {	Write-Host "".padright($padright,"-")          -ForegroundColor Green  -BackgroundColor Black }
   $fullReport += $loopReport
   $fullReport += $taskErrorMsg
   $fullReport += "----------------------------------------------------------------------------------------------------------------- `n"
   continue
  }
  $lastRunMessage = "Last Run Message..: $lastActionMessage"
  if ( $script:showDebug ) {	Write-Host "$lastRunMessage".padright($padright) -ForegroundColor Green -BackgroundColor Black }
  $loopReport += "$lastRunMessage `n"
  $NumberStart = $lastActionMessage.IndexOf(":")+2
  $NumberEnd   = $lastActionMessage.IndexOf("char:")-$($NumberStart+1)
  try   { $derivedLineMsg = "Derived last line.: '$($lastActionMessage.Substring($NumberStart,$NumberEnd))'" }
  catch { $derivedLineMsg = "Derived last line.: None" }
  if ( $script:showDebug ) {	Write-Host "$derivedLineMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
  $loopReport += "$derivedLineMsg `n" 
 } # if ( $lastActionMessage.length -gt 0  ) {
 else {
  $lastRunMessage = "Last Run Message..: 'ERROR: no last run message'"
  if ( $script:showDebug ) {	Write-Host "$lastRunMessage".padright($padright) -ForegroundColor Green -BackgroundColor Black }
  $loopReport += "$lastRunMessage `n"
 } # else if ( $lastActionMessage.length -gt 0  ) {
 $lastRunLineNoMsg = "LastRunLine was...: '$lastRunLineNo'"
 if ( $script:showDebug ) {	Write-Host "$lastRunLineNoMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
 $loopReport += "$lastRunLineNoMsg `n"
 #EndRegion Get LastRunLineNo Position  
 #Region Check if task ran as expected
 if ( $xmlData.Schedule.Daily ) { 
  if ( $xmlData.Schedule.Daily.every ) {
   $unit = $xmlData.Schedule.Daily.every
   $units = "'Every $unit day(s) @ $time'" 
  }
  if ( $xmlData.Schedule.Daily.Start ) {
   if ( $xmlData.Schedule.Daily.Start.time ) {
    $time = Get-Date ([datetime]($xmlData.Schedule.Daily.Start.time)) -f T  
   } # if ( $xmlData.Schedule.Daily.Start.time ) {
  } # if ( $xmlData.Schedule.Daily.Start ) {
 } # if ( $xmlData.Schedule.Daily ) { 
 elseif ( $xmlData.Schedule.Monthly ) { 
  $DayOfWeek = $xmlData.Schedule.Monthly.days.weekday.'#text'
  $which = $xmlData.Schedule.Monthly.days.weekday.which
  $units = "'The $which $DayOfWeek of each Month'"
 }
 $scheduledUnitsMsg = "Schedule..........: $units "
 if ( $script:showDebug ) {	Write-Host "$scheduledUnitsMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
 $loopReport += "$scheduledUnitsMsg `n"  
 try   { $lastRunMsg = "Last Run..........: '$(get-date $lastRunTime -Format 'ddd, MMM dd, yyyy @ HH:mm' )'" }
 catch { $lastRunMsg = "Last Run..........: Not Available" }
 $loopReport += "$lastRunMsg `n"   
 if ( $script:showDebug ) {	Write-Host "$lastRunMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
 try   { $nextRunMsg = "Next Run..........: '$(get-date $nextRunTime -Format 'ddd, MMM dd, yyyy @ HH:mm')'" }
 catch {$nextRunMsg = "Next Run..........: Not Available" }
 if ( $script:showDebug ) {	Write-Host "$nextRunMsg".padright($padright) -ForegroundColor Green -BackgroundColor Black }
 $loopReport += "$nextRunMsg `n"  
 #EndRegion Check if task ran as expected
 if ( ( (Get-Date) -lt $nextRunTime ) -and ( ( $taskInfo.DN -eq $script:taskDN  ) -or ( ( $lastActionMessage.Length -gt 0  ) -and ( $lastRunLineNo -eq $($lastActionMessage.Substring($NumberStart,$NumberEnd)) ) ) ) ) {
  if ( $script:showDebug ) {	Write-Host "Last Run..........: 'Succesfull'".padright($padright) -ForegroundColor Cyan -BackgroundColor Black }
  $loopReport += "Last Run..........: 'Succesfull' `n"
 } # if ( ( (get-date) -lt $nextRunTime ) -and ( ( $taskInfo.DN -eq $script:taskDN  ) -or ( ( $lastActionMessage.Length -gt 0  ) -and ( $lastRunLineNo -eq $($lastActionMessage.Substring($NumberStart,$NumberEnd)) ) ) ) ) {
 else {
  $errorInLoop = $true
  if ( (Get-Date) -gt $nextRunTime ) {
   try { Set-QADObject $taskInfo.DN -ObjectAttributes @{edsalastactionmessage="ERROR: Missed last runtime"} -proxy }
   catch {
    $bp = 0 
   }
  }
  elseif ( $lastActionMessage.Length -gt 0 ) {
   try { Set-QADObject $taskInfo.DN -ObjectAttributes @{edsalastactionmessage="ERROR: '$($taskInfo.edsaLastActionMessage.Trim())'"} -proxy }
   catch {
    $bp = 0 
   }
  } # if ( $lastActionMessage.Length -gt 0 ) {
  else {
   Set-QADObject $taskInfo.DN -ObjectAttributes @{edsalastactionmessage="ERROR: task does not appear to have run please check any relevant logs"} -proxy 
   if ( $script:showDebug ) {	Write-Host "ERROR: task does not appear to have run please check any relevant logs".padright($padright) -ForegroundColor Yellow -BackgroundColor Black }
   $loopReport += "ERROR: task does not appear to have run please check any relevant logs `n"
  }
 } # else if ( ( (get-date) -lt $nextRunTime ) -and ( ( $taskInfo.DN -eq $script:taskDN  ) -or ( ( $lastActionMessage.Length -gt 0  ) -and ( $lastRunLineNo -eq $($lastActionMessage.Substring($NumberStart,$NumberEnd)) ) ) ) ) {
 $loopReport += "----------------------------------------------------------------------------------------------------------------- `n"
 if ( $errorInLoop )  {
  $report += $loopReport
 }
 $fullReport += $loopReport 
 if ( $script:showDebug ) {	Write-Host "".padright($padright,"-") -ForegroundColor Green -BackgroundColor Black  } 
} # ForEach ( $taskInfo in $tasks ) {
if ( $report.Length -gt 0 ) {
 if ( $script:showDebug ) {	Write-Host "".padright($padright,"=") -ForegroundColor Green -BackgroundColor Black }
 $report = "=============================================================================================================================== `n" + $report  
 # send an error report 
 Send-MailMessage -To $emailAlert -From $emailFrom -Body $report -SmtpServer $smtpServer -Subject "ERROR in ARS Scheduled Task Please investigate - $env:COMPUTERNAME" 
}
else {
 $errorInRun = $false 
}
$thisLastRLNo =  (Get-CurrentLineNumber) + 25
if ( $script:showDebug ) {	 Write-Host  "Line No = $thisLastRLNo".padright($padright) -ForegroundColor  Yellow -BackgroundColor Black } 
if ( $thisLastRLNo -ne $lastRunLine ) {
 $taskParameters = @{
  'emailAlert'  = $($emailAlert -join ",")
  'emailFrom'   = $emailFrom
  'lastRunLine' = $thisLastRLNo  
  'latestRunDate' = $(Get-Date $latestRunDate -Format "dd/MM/yyyy HH:mm:ss")
  'ScriptOwner' = $script:Owner 
  'smtpserver'  = $smtpserver
  'tasksOU'     = $tasksOU 
 }
 $Error.Clear()
 Set-TaskParameters -htParameters $taskParameters -taskDN $script:taskDN -connection $proxy
}
if ( $errorInRun -eq $false ) {
 $throwMessage = "Completed succesfully - $env:COMPUTERNAME"  
}
else {
 $throwMessage = "One or more tasks failed to complete please investigate - $env:COMPUTERNAME"
}
if ( $script:ShowDebug ) {
 $msg = "At line: $thisLastRLNo char:1. $throwMessage"
 Set-QADObject $script:TaskDN  -ObjectAttributes @{edsalastactionmessage=$msg} -proxy | Out-Null 
}
throw $throwMessage