Hello,
Our script guys have studied your script and have found several places where performance can be improved.
First of all, in your script you find the subordinates / managed objects in a search, and then bind to each subordinate / managed object and get the properties of each object. This is not optimal since each such action is a separate call. Instead, you can fetch all the required properties within the search and have only one call instead of 7-9 calls. What is more contributing to the script execution time is that you perform all the actions on a remote service. Each such operation will be a pipelined ('slow') call if you perform it on a remote Adaxes service.
So, what we suggest is to perform the search on the local service (in order to avoid pipelined calls) and fetch all the properties by the search. This is going to be extremely fast and almost no load on the service. Here's an example on how to do this for managed objects:
$searcher = New-Object "Softerra.Adaxes.Adsi.Search.DirectorySearcher" $NULL, $False
$searcher.SearchParameters.BaseObjectPath = $Context.TargetObject.AdsPath
$searcher.SearchParameters.PageSize = 500
$searcher.SearchParameters.SearchScope = "ADS_SCOPE_BASE"
$searcher.SearchParameters.Filter = "(ObjectClass=*)"
$searcher.SearchParameters.AttributeScopeQuery = "managedObjects"
$searcher.SetPropertiesToLoad(@("name","objectGUID","sAMAccountType"))
$searcherResult = $searcher.ExecuteSearch()
$result = $searcherResult.FetchAll()
$searcherResult.Dispose()
if ($result.Count -ne 0)
{
$htmlbody += "<table border='0' width='850' cellpadding='0'><font color=#FFFFFF face='Segoe UI, Arial' size=2>
<tr><td width='500' bgcolor=#2A6AA6><h3>Active Directory Non-User Objects</h3></td><td width='350' bgcolor=#2A6AA6></td></tr>
<tr><td width='500' bgcolor=#5E84B8><h4>Object Name (Object Type)</h4></td><td width='350' align=center bgcolor=#5E84B8><h4>Review Due Date \ Current Status</h4></td></tr>
</font>
<font face='Segoe UI, Arial' size=2>"
foreach ($managedObject in $result)
{
$objType = $managedObject.Properties["sAMAccountType"].Value
if ($objType -eq "268435457")
{
$objTypeName = "Distribution List"
}
else
{
$objTypeName = "Security Group"
}
$entryname = $managedObject.Properties["name"].Value
$entryGUIDByte = $managedObject.Properties["objectGUID"].Value
$entryGUID = New-Object "System.Guid" (,$entryGUIDByte)
$entryGUID = $entryGUID.ToString("D")
$htmlbody += "<tr><td width='500'><b>$entryname</b><i> (<a href='$urlprefix $entryGUID'>$objTypeName</a>)</i></td><td width='350' bgcolor=#FFFFFF><i></i></td></tr>"
}
$htmlbody += "</font>
</table>"
}
Here we have the following line:
$searcher.SetPropertiesToLoad(@("name","objectGUID","sAMAccountType"))
The SetPropertiesToLoad method of the IAdmDirectorySearcher interface sets, which properties should be fetched by the search.
One more issue with the script is the part where you deal with Approval Requests. First of all, you bind to each Approval Request using the Request GUID. This is a very expensive operation that requires the service to search all Approval Requests and find the Request with the necessary GUID. Moreover, you get all pending Approval Requests, and then iterate through them to find the Approval Request, where the Target Object is the subordinate, and the Request Initiator is the Scheduled Task that triggers user review. If you have, say, 20 subordinates and 500 Approval Request, you will have to bind to Approval Requests 20*500 = 1000 times and get the Target Object of an Approval Request 1000 times. This is very expensive.
Here, you should remember that each Approval Request is represented as an AD object in Adaxes. Each Approval Request has the adm-ApprovalState property that indicates the state of the Approval Request (Pending, Approved, Denied, Cancelled), the adm-ApprovalRequestorGuid property that indicates the GUID of the Approval Request initiator, and the adm-TargetObjectGuid property that indicates the Target Object of the Request. So, why not just search a Pending Approval Request, where the Initiator is the Scheduled Task and the Target Object is the Subordinate? It will be many times faster than iterating through all of the approval requests for each subordinate. Here's a sample script on how to deal with subordinates:
$scheduledTaskName = "Account Review - Users" # TODO: modify me
try
{
$subordinateDNs = $Context.TargetObject.GetEx("directReports") | sort-object
}
catch
{
$subordinateDNs = $NULL
}
if ($subordinateDNs -ne $NULL)
{
# Get Scheduled Task 'Account Review - Users' GUID
$scheduledTasksContainerPath = $Context.GetWellKnownContainerPath("ScheduledTasks")
$scheduledTaskSearcher = New-Object "Softerra.Adaxes.Adsi.Search.DirectorySearcher" $NULL, $False
$scheduledTaskSearcher.SearchParameters.BaseObjectPath = $scheduledTasksContainerPath
$scheduledTaskSearcher.SearchParameters.PageSize = 500
$scheduledTaskSearcher.SearchParameters.SearchScope = "ADS_SCOPE_SUBTREE"
$scheduledTaskSearcher.SearchParameters.Filter = "(&(objectCategory=adm-ScheduledTask)(name=$scheduledTaskName))"
$scheduledTaskSearcher.SearchParameters.ReferralChasing = "ADS_CHASE_REFERRALS_NEVER"
$scheduledTaskSearcher.SetPropertiesToLoad(@("objectGuid"))
$searcherResult = $scheduledTaskSearcher.ExecuteSearch()
$result = $searcherResult.FetchAll()
$searcherResult.Dispose()
if ($result.Count -eq 1)
{
$scheduledTaskGuidInByte = $result[0].Properties["objectGUID"].Value
$scheduledTaskGuid = [Softerra.Adaxes.Ldap.FilterBuilder]::Create("adm-ApprovalRequestorGuid", $scheduledTaskGuidInByte)
}
else
{
$Context.LogMessage("The Scheduled Task $scheduledTaskName was not found or the name of the Scheduled Task is not unique", "Warning") # TODO: Modify me
return
}
foreach ($subordinateDN in $subordinateDNs)
{
if ($subordinateDN -like "*CN=Users,DC=root,DC=net") # This is a fudge fix to stop references to the AD root domain causing errors
{
$subordinateDN = "'root' DN trapped and excluded"
$context.LogMessage($subordinateDN, "Warning")
continue
}
$subordinate = $Context.BindToObjectByDN($subordinateDN)
$reviewDate = $subordinate.Get("adm-CustomAttributeDate1").ToString("dd/MM/yyy")
$reviewStatus = $subordinate.Get("adm-CustomAttributeText4")
if ($reviewStatus.startsWith("Review In Progress"))
{
$searchFilter = "(&(objectCategory=adm-ApprovalRequest)(adm-ApprovalState=0)$scheduledTaskGuid"
$objectGuidInByte = $subordinate.Get("objectGUID")
$objectGuid = [Softerra.Adaxes.Ldap.FilterBuilder]::Create("adm-TargetObjectGuid", $objectGuidInByte)
$searchFilter += "$objectGuid)"
$containerPath = $Context.GetWellKnownContainerPath("ApprovalRequests")
$searcher = New-Object "Softerra.Adaxes.Adsi.Search.DirectorySearcher" $NULL, $False
$searcher.SearchParameters.BaseObjectPath = $containerPath
$searcher.SearchParameters.PageSize = 500
$searcher.SearchParameters.SearchScope = "ADS_SCOPE_SUBTREE"
$searcher.SearchParameters.Filter = $searchFilter
$searcher.SearchParameters.ReferralChasing = "ADS_CHASE_REFERRALS_NEVER"
$searcherResult = $searcher.ExecuteSearch()
$requests = $searcherResult.FetchAll()
$searcherResult.Dispose()
foreach ($requestID in $requests)
{
# Bind to the approval request
$request = $Context.BindToObject($requestID.AdsPath)
$requestorName = $request.Requestor.Get("name")
# TODO: Some code to include the Approval Request info in the repoort
}
}
}
}
In the script, $scheduledTaskName indicates the name of the 'Account Review' Scheduled Task. Change it, if necessary.