Provisioning BPOS powershell commands as CS Objects

Here’s something else I’ve been developing, which I think has interesting potential. Essentially it’s an XMA which is used to provision powershell “command” objects. These objects contain the name of the cmdlet and the list of arguments, and the Export step is actually running the command.

The background to this is a BPOS (Microsoft cloud services) project I’ve been working on. BPOS doesn’t actually give you any way to disable an account while keeping the mailbox, so I wanted to be able to reset a BPOS password as part of the deprovisioning cycle. I also wanted to do this quickly and efficiently. BPOS gives you two methods to change a password: through the admin console and through powershell. Obviously powershell is the way to go for automation. 

At the same time I was thinking of other times where I may want to fire off a powershell cmdlet – such as when initailly activating the account, when adding BPOS services, changing a mailbox quota or when adding mailbox trustees. With this method I think I have found a nicely generalised solution. My XMA will run any powershell cmdlet and all I need to do is provision these “command” objects, based on changes in the metaverse. 

Some Warnings…

You can run into problems with this approach where FIM Sync unprovisions the unexported command object because it thinks you don’t need it any more. I found this when I tied the provisioning of the BPOS command object to the moving of an AD account, but then exported the AD MA first. The Sync service thought the command was then not needed and removed. An extra status flag should sort this out.

FIM Portal Workflow may be better

The other comment is that this is a Sync Service based way of approaching this problem. A better bet, if you’re using the FIM Portal, is probably to fire the powershell cmdlets directly from the Portal as part of a workflow. Unfortunately, at the moment, this means a custom workflow, which I haven’t really gotten my head around yet.

About the Export Files

At the moment the XMA is writing a new delta CSV file, and adding to the existing full CSV file, during each export. At the moment I only add successful runs to the full.csv file – this means I don’t have to mess about with updating existing entries – however it can lead to weird behaviour if you had some failures and follow it by a Full Import. For safety I have the “Delta Import” step as part of my Export run profile, so it always runs straight after the export. The better way would be to use SQL tables for this exported info.

Steps

Preparing the XMA

The server prerequisites are covered in this post. You should also review this other post about creating a remote powershell XMA. The big difference with this MA is that it is Call-based, giving me the ExportEntry Sub which runs against each individual export.

I still need a CSV file to feed to the MA creation wizard for its schema. My CSV, very simply, contains the following:

     cmdlet,arguments,date,time,identity,status

The “identity” here is an identifier for the BPOS object that I’m going to run the command against, and is not actually a unique identifier for the command itself. Because I don’t have a single unique attribute I use the following combination as the anchor: cmdlet + identity + date.

This has the useful side-effect of ensuring only one command object for a particular activity will be configured per day – though if the export of the command were to repeatedly fail you would find a new command object being provisioned the next day. Just something to watch out for.

Retry Mechanism

I also configure one direct Export Attribute Flow:      Constant “success” –> status

This is my feedback mechanism. I force the export of “success” to my command object – but if the running of the command actually failed I write “failed” into the delta import file – so this actually makes a re-export happen. Until the Sync service can export and then re-import a status of “success” it will keep trying.

CS Extension

Here’s the CSExtension. Because this is for BPOS I’ve added the appropriate PSSnapin in the BeginExport sub. You may not need to add any snapins. Or you may just need the standard Exchange ones in which case it looks like you don’t need to add a snapin at all, because you have a nice Exchange-specific shell URI to use. There are plenty of other posts about that so I won’t say more.  
  

Imports Microsoft.MetadirectoryServices
Imports System.Management.Automation
Imports System.Management.Automation.Host
Imports System.Management.Automation.Runspaces
Imports System.IO

Public Class MACallExport
    Implements IMAExtensibleFileImport
    Implements IMAExtensibleCallExport

    Const SHELL_URI As String = "http://schemas.microsoft.com/powershell/Microsoft.Powershell"
    Const MA_FOLDER As String = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\MaData\BPOS commands\"
    Const DELTA_FILE As String = "delta.csv"
    Const FULL_FILE As String = "full.csv"
    Const HEADER As String = "cmdlet,arguments,date,time,identity,status"

    Dim myRunSpace As Runspace
    Dim bposCred As PSCredential

    Dim fileDelta As StreamWriter
    Dim fileFull As StreamWriter

    Public Sub GenerateImportFile(ByVal filename As String, ByVal connectTo As String, ByVal user As String, ByVal password As String, ByVal configParameters As ConfigParameterCollection, ByVal fullImport As Boolean, ByVal types As TypeDescriptionCollection, ByRef customData As String) Implements IMAExtensibleFileImport.GenerateImportFile
        ' TODO: Remove this throw statement if you implement this method
        Throw New EntryPointNotImplementedException
    End Sub

    Public Sub BeginExport(ByVal connectTo As String, ByVal user As String, ByVal password As String, ByVal configParameters As ConfigParameterCollection, ByVal types As TypeDescriptionCollection) Implements IMAExtensibleCallExport.BeginExport
        ' Start new DELTA file.
        fileDelta = New StreamWriter(MA_FOLDER & DELTA_FILE, False, System.Text.Encoding.Default)
        fileDelta.WriteLine(HEADER)
        fileDelta.Close()

        ' Open existing FULL file
        fileFull = New StreamWriter(MA_FOLDER & FULL_FILE, True, System.Text.Encoding.Default)

        ' Create credential for attaching to remote server
        Dim remotepass As New Security.SecureString
        Dim c As Char
        For Each c In password
            remotepass.AppendChar(c)
        Next
        Dim remoteCred As New PSCredential(user, remotepass)

        ' Create credential for attaching to BPOS
        ' The configParameters are configured on the Additional Parameters page of the MA.
        Dim bpospass As New Security.SecureString
        For Each c In configParameters("bposPassword").Value
            bpospass.AppendChar(c)
        Next
        bposCred = New PSCredential(configParameters("bposUser").Value, bpospass)

        ' Open remote powershell session
        Dim serverUri As New Uri("http://" & connectTo & ":5985/wsman")
        Dim connectionInfo As New WSManConnectionInfo(serverUri, SHELL_URI, remotecred)
        myRunSpace = RunspaceFactory.CreateRunspace(connectionInfo)
        Dim psException As PSSnapInException = Nothing
        myRunSpace.Open()

        Dim psh As PowerShell = PowerShell.Create()
        psh.Runspace = myRunSpace
        psh.AddCommand("Add-PSSnapin")
        psh.AddParameter("Name", "Microsoft.Exchange.Transporter")
        psh.Invoke()

    End Sub

    Public Sub ExportEntry(ByVal modificationType As ModificationType, ByVal changedAttributes As String(), ByVal csentry As CSEntry) Implements IMAExtensibleCallExport.ExportEntry
        Dim psh As PowerShell = PowerShell.Create()
        Dim psresult As New System.Collections.ObjectModel.Collection(Of PSObject)
        psh.Runspace = myRunSpace

        psh.AddCommand(csentry("cmdlet").StringValue)

        Dim arguments As String = csentry("arguments").StringValue
        If Not arguments.StartsWith(" ") Then arguments = " " & arguments

        Dim parameter As String
        Dim seperator As String() = {" -"}
        For Each parameter In arguments.Split(seperator, StringSplitOptions.None)
            If parameter.Contains(" ") Then
                If parameter.Contains("$true") Then
                    psh.AddParameter(parameter.Split(" ".ToCharArray, 2)(0), True)
                ElseIf parameter.Contains("$false") Then
                    psh.AddParameter(parameter.Split(" ".ToCharArray, 2)(0), False)
                Else
                    psh.AddParameter(parameter.Split(" ".ToCharArray, 2)(0), parameter.Split(" ".ToCharArray, 2)(1))
                End If
            End If
        Next
        psh.AddParameter("Credential", bposCred)

        Dim strLine As String = csentry("cmdlet").StringValue & "," & csentry("arguments").StringValue & "," _
                                & csentry("date").StringValue & "," & csentry("time").StringValue & "," & csentry("identity").StringValue
        fileDelta = New StreamWriter(MA_FOLDER & DELTA_FILE, True, System.Text.Encoding.Default)

        Try
            psresult = psh.Invoke()
        Catch ex As Exception
            fileDelta.WriteLine(strLine & ",error: " & ex.Message)
        End Try

        If psh.Streams.Warning.Count > 0 Then
            fileDelta.WriteLine(strLine & ",warning: " & psh.Streams.Warning.Item(0).Message)
       ElseIf psh.Streams.Error.Count > 0 Then
            fileDelta.WriteLine(strLine & ",error: " & psh.Streams.Error.Item(0).ErrorDetails.Message)
       Else
            fileDelta.WriteLine(strLine & ",success")
            fileFull.WriteLine(strLine & ",success")
        End If
        fileDelta.Close()

        psh.Dispose()
    End Sub

    Public Sub EndExport() Implements IMAExtensibleCallExport.EndExport

        fileFull.Close()
        myRunSpace.Close()

    End Sub

End Class

 

Provisioning Code

Here’s an example of provisioning a command object from the MVExtension code.


' To change BPOS password, provision a BPOS command object
Dim bposCmd As CSEntry
bposCmd = mventry.ConnectedMAs(MAName_BPOScmd).Connectors.StartNewConnector("command")
bposCmd("cmdlet").StringValue = "Set-MSOnlineUserPassword"
bposCmd("identity").StringValue = mventry("bposIdentity").StringValue
bposCmd("date").StringValue = Now.Date.ToString("d")
bposCmd("time").StringValue = Now.TimeOfDay.ToString
Dim randompassword As String = < your random password generator here >
bposCmd("arguments").StringValue = "-Identity " & mventry("bposIdentity").StringValue & " -Password " & randompassword & " -ChangePasswordOnNextLogon $false"
Try
     bposCmd.CommitNewConnector()
Catch ex As ObjectAlreadyExistsException
End Try