.\Matthew Long

{An unsorted collection of thoughts}

Posts Tagged ‘SCSM 2012’

SCSM 2012 R2: Dynamically replacing AD groups in Review Activities with users

Posted by Matthew on May 14, 2014

Problem

One of the challenges people seem to run into with using Review activities in System Center Service Manager is using Active Directory groups as reviewers.  When you add an AD group as a reviewer the functionality you get is:

  1. Everyone who is a member of that group may vote, via the SCSM Console (if they have access) or via the Self-Service Portal.
  2. The group only has a single vote – if there are 5 members of the group and you need a vote from each person, the moment 1 person votes yes/no the entire group has voted.
  3. Unless the group also has a mail address, group members won’t get a notification as the notification engine doesn’t know who is a member of the group.
  4. Even if you do send out a notification via the group address, if the members reply back to the mail with a vote it won’t be processed by SCSM as only the Group is recognised as a reviewer, not them.

Number 4 is essentially a bug, but a long-standing one.  However for many people this mechanism isn’t suitable for their needs because:

  1. You may want to require an approve vote from everyone in the group, or a % proportion.
  2. If the group is the only reviewer, as soon as one person votes yes, the activity is approved, even if 30 seconds later someone wants to vote declined.
  3. Each group only gets a single vote – there is no room for weighting.  If one group contains 7 people and one contains 3, they both have equal voting share.
  4. When your Service desk is trying to chase or determine who can vote on an activity, it can be difficult to ascertain exactly who can vote.

The alternative is therefore to populate the review activity with the users that make up the group… which can be extremely painful if you’ve got many templates all with individual users that need updating whenever a person joins/leaves a team, nested groups, and must all this must be cross-referenced when building new templates or creating review activities manually.

Solution

In order to address the problem, I’ve put together a powershell script that will replace any Active Directory groups from an instance of a review activity and replace them with SCSM AD Users (assuming they have been imported into the CMDB).  This means you can add groups to your templates (or manually created activities) and once they go in progress the groups are expanded.  Features include:

  1. Recursively parses groups, so nested groups are supported.
  2. Ensures that a user is not added twice if a member of multiple groups (or manually added to the review activity).
  3. Calculates and maintains the highest approval rights a user may be entitled to (Must vote and Veto).
  4. Group is removed once users have been added.
  5. No external PS modules required – uses the native SCSM 2012 R2 module.

This script can be hooked up to service manager in a variety of ways – the primary method being a System Center Orchestrator runbook that is monitoring for created review activities.  You could also utilise this script in a Service Management Automation (SMA) runbook or even using the native SCSM Workflow engine if you use the community PS workflow activities.

As the script has been written to support System Center Orchestrator, it makes use of Powershell remoting to get around the x86 PSv2 only restrictions, but you can remove these elements if you are running it using SMA or natively on the SCSM Management servers.  There are a few variables at the start of the script that contain connection details – these can either be hard coded (not recommended) or populated via SCO Variables.  The Review activity GUID needs to be set using a subscription to a “Monitor Object” activity.


#Create Session that we will re-use for multiple invokes.
$username = "scsmusername"
$password = "scsmpassword" | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username,$password)
$session = New-PSSession -computername 'scsmmanagementservername' -Credential $credential

#Invoke into session to setup variables & structures, parse initial RA membership.
Invoke-Command -Session $session -Scriptblock {
   Import-Module Microsoft.EnterpriseManagement.Core.Cmdlets
   Import-Module Microsoft.EnterpriseManagement.ServiceManager.Cmdlets
   New-SCManagementGroupConnection localhost

   $reviewActivity = Get-SCClassInstance -Id 'Review Activity SC Object Guid'

   $reviewers = $reviewActivity.GetRelatedObjectsWhereSource("System.ReviewActivityHasReviewer")
   $groupQueue = New-Object "System.Collections.Queue"
   $userDictionary = @{}
   $SCADGroupClass = Get-SCClass -name "Microsoft.AD.Group"
   $SCADUserClass = Get-SCClass -name "Microsoft.AD.User"
   $SCReviewerClass = Get-SCClass -Name "System.Reviewer"
   $SCHasReviewerRelationship = Get-SCRelationship -Name "System.ReviewActivityHasReviewer"
   $SCReviewerIsUser = Get-SCRelationship -Name "System.ReviewerIsUser"

   ForEach ($reviewer in $reviewers)
   {
      $psReviewer = $reviewer.ToPSObject()
      $user = @($reviewer.GetRelatedObjectsWhereSource("System.ReviewerIsUser"))[0]
      If ($user -ne $null)
      {
         $dn = $user.EnterpriseManagementObject.Item([guid]'97362a11-e6b1-a352-820e-b1e75d09da66').Value
         If ($dn -ne $null -and $dn.length -gt 0)
         {
            $adReviewer = New-Object PSObject -Property @{MustVote=[Boolean]$psReviewer.MustVote;Veto=[boolean]$psReviewer.Veto;DN=$dn;Id=$reviewer.Enterprisemanagementobject.id}
            If ($user.EnterpriseManagementObject.IsInstanceOf($SCADGroupClass) -eq $true)
            {
               $groupQueue.Enqueue($adReviewer)
            }
            ElseIf ($user.EnterpriseManagementObject.IsInstanceOf($SCADUserClass) -eq $true)
            {
               If ($userDictionary.ContainsKey($dn) -eq $false)
               {
                  $userDictionary[$dn] = $adReviewer
               }
               else
               {
                  $userDictionary[$dn].MustVote = $psReviewer.MustVote -or $userDictionary[$dn].MustVote
                  $userDictionary[$dn].Veto = $psReviewer.Veto -or $userDictionary[$dn].Veto
               }
            }
         }
      }
   }
}

#Return groups to process and existing user reviewers to local machine
$groupQueue = Invoke-Command -Session $session -scriptblock {(,$groupqueue)}
$userDictionary = Invoke-Command -Session $session -scriptblock {$userDictionary}
$processedGroups = New-Object 'System.Collections.Generic.List[System.String]'

#Declare recursive function to process and expand AD groups into SCSM user-based Reviewers
Function ExpandGroup ($ADSIGroup, $hasVeto, $mustVote)
{
   If ($processedGroups.Contains($ADSIGroup.Path) -eq $false)
   {
      $processedGroups.Add( $ADSIGroup.Path)
      $members = $ADSIGroup.Invoke("Members",$null) | % { [ADSI]$_}
      ForEach ($member in $members)
      {
         If ($member.objectClass -match 'group')
         {
            ExpandGroup $member $hasVeto $mustVote
         }
         ElseIf ($member.objectClass -match 'user')
         {
            $userDn = $member.Path.replace("LDAP://",'')
            If ($userDictionary.ContainsKey($userDn) -eq $false)
            {
               $userDictionary[$userDn] += $null
               $newReviewerTargetId = Invoke-Command -Session $session -ArgumentList $userDn,$mustVote,$hasVeto -ScriptBlock {
                  Param($userDn,$mustVote,$hasVeto)
                  $reviewerUser = Get-SCClassInstance -Class $SCADUserClass -filter ("DistinguishedName -eq " + $userDn)
                  If ($reviewerUser -ne $null)
                  {
                     $newReviewRelationship = New-SCRelationshipInstance -RelationshipClass $SCHasReviewerRelationship -Source $reviewActivity.EnterpriseManagementObject -TargetClass $SCReviewerClass -TargetProperty @{ReviewerId="{0}";MustVote=$mustVote;Veto=$hasVeto} -PassThru
                     New-SCRelationshipInstance -RelationshipClass $SCReviewerIsUser -Source $newReviewRelationship.TargetObject -Target $reviewerUser
                     $newReviewRelationship.TargetObject.Id
                  }
               }
               If ($newReviewerTargetId -ne $null)
               {
                  $userDictionary[$userDn] = New-Object PSObject -Property @{MustVote=$mustVote;Veto=$hasVeto;DN=$userDn;Id=$newReviewerTargetId}
               }
            }
            ElseIf($userDictionary[$userDn] -ne $null)
            {
               $changedRights = $false
               If ($userDictionary[$userDn].MustVote -ne $mustVote)
               {
                  $changedRights = $true
                  $userDictionary[$userDn].MustVote = $userDictionary[$userDn].MustVote -or $mustVote
               }
               If ($userDictionary[$userDn].Veto -ne $hasVeto)
               {
                  $changedRights = $true
                  $userDictionary[$userDn].Veto = $userDictionary[$userDn].Veto -or $hasVeto
               }
               If ($changedRights -eq $true)
               {
                  Invoke-Command -Session $session -ArgumentList $userDictionary[$userDn].Id,$userDictionary[$userDn].MustVote,$userDictionary[$userDn].Veto -ScriptBlock {
                     Param($Id,$MustVote,$Veto)
                     $existingUser = Get-SCClassInstance -Id $Id
                     $existingUser.MustVote = $MustVote
                     $existingUser.Veto = $Veto
                     $existingUser | Update-SCClassInstance
                  }
               }
            }
         }
      }
   }
}

#Process Groups using above function.
While ($groupQueue.Count -gt 0)
{
   $group = $groupQueue.Dequeue()
   $adsiGroup = [adsi]("LDAP://" + $group.dn)
   ExpandGroup $adsiGroup $group.Veto $group.MustVote
   Invoke-Command -session $session -scriptblock {Param($Id) Get-SCClassInstance -Id $Id | Remove-SCClassInstance } -ArgumentList $group.Id
}
#Cleanup session
Remove-PSSession $session

With this script we can now

  1. Place groups in our templates at design time, and know that the review activity will contain the correct members of the group.
  2. The service desk can add groups to manually created activities, without having to look up who is a member, find their CIs and deal with nested groups
  3. We can use notification channels to allow the reviewers to vote back via email etc.
  4. Finally, if we only wanted to give the group a single vote share in specific cases, we can still do that by putting a parallel activity in our workflow and putting two review activities in – one with the single vote group and one with the remaining voters.  The group review activity can then have it’s criteria set appropriately (unanimous, %), and if the group rejects the review activity then the Parallel activity will fail and the parent work item will stop as normal.

 Example

In the screenshots below, you can see that I’ve used this script in an Orchestrator Runbook.  The “Monitor for New Review Activities” trigger is just set to monitor for new “Review Activity” work items, with no filters.

Runbook

The script is inserted in the (badly named, i’ll admit) “Delete Group Reviewer” activity, which is actually a Run .Net Script activity.  Hook up all your variables and published data (remember that the ID the script is looking for is the SC Object Guid from “Monitor for..”, NOT the work item ID.

Start the runbook, and then go create a review activity.  Below I’ve got one user with the “Must Vote” right enabled, and one group with the “Has Veto” right.  The user is also a member of the group, so when the runbook executes it should expand the group into users but also give the existing user the “Has Veto” right and maintain their must vote status.

Before

Wait for the runbook to execute, then check back on your review activity – as you can see we now have all users and the group has been removed!

after

Hope you find this script helpful!

Advertisements

Posted in Computing | Tagged: , , , , , | 1 Comment »