Tuesday, April 4, 2017

CodePlex says "Bye Felicia"

If you've visited CodePlex recently, you may have noticed a message at the top that reads:

CodePlex is shutting down. Read about the shutdown plan, including archive and migration information, on Brian Harry's blog.

Here's a summary of the timeline, according to the link above:

March 31, 2017
Announcement of shutdown and disabled ability to create new CodePlex projects.
October 2017
CodePlex goes into read-only mode.
December 15, 2017
CodePlex shuts down completely.

I know that a lot of projects have already moved over to GitHub, and we at Identity Managed have been quietly transitioning over to Bitbucket, but there are still plenty of projects on CodePlex that should be migrated to a new host (ahem, FIM Query Tool!).

Wednesday, January 4, 2017

Function evaluator custom expression escape double quote

Curiously, there's no need to escape double quotes inside custom expressions!

ReplaceString(
    [//WorkflowData/XMLEncoded],
    """,
    """
)

Tuesday, August 30, 2016

Unable To Create New WorkflowInstance For WorkflowDefinition

I discovered another variant that causes the titular error message.  I was copying the FIM policy config from one data source to another, and that meant making copies of a couple activities within the same workflow.  After some troubleshooting and head-scratching, I realized that FIM was choking on the duplicate activity names (see highlights below).  As you can see, when you build a workflow from the portal, FIM names all activities "authenticationGateActivityX."  And interestingly, if you made the same mistake as I did, and you try to fix it by opening all of the activities within the portal and click "Save," FIM won't fix your mistakes!

<ns0:SequentialWorkflow
  x:Name="SequentialWorkflow"
  ActorId="00000000-0000-0000-0000-000000000000"
  WorkflowDefinitionId="00000000-0000-0000-0000-000000000000"
  RequestId="00000000-0000-0000-0000-000000000000"
  TargetId="00000000-0000-0000-0000-000000000000"
  xmlns:ns1="clr-namespace:IDMware.FIM.Workflow.Activities;Assembly=IDMware.FIM.Workflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b6cdb71f2a5e8e65"
  xmlns:ns2="clr-namespace:FimExtensions.FimActivityLibrary;Assembly=FimExtensions.FimActivityLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=68dc29282b77eca9"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:ns0="clr-namespace:Microsoft.ResourceManagement.Workflow.Activities;Assembly=Microsoft.ResourceManagement, Version=4.1.3559.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  <ns1:GenerateRandomPasswordActivity
    x:Name="authenticationGateActivity1"
    DestinationValue="{x:Null}"
    MinPasswordLength="8"
    DestinationString="RandomPassword"
    TheActivityName="Generate random password"
    MaxPasswordLength="10" />
  <ns0:FunctionActivity
    x:Name="authenticationGateActivity6"
    FunctionExpression="<fn id="SingleValueAssignment"  isCustomExpression="false"><arg>[//Target/AccountName]</arg></fn>"
    Description="Grab Target account name"
    Destination="[//WorkflowData/TargetAccountName]" />
  <ns2:PowerShellActivity
    x:Name="authenticationGateActivity5"
    TheActivityName="Powershell: Set AD password"
    PowerShellScript="C:\Scryptography\SetADPwd.ps1" />
  <ns0:FunctionActivity
    x:Name="authenticationGateActivity7"
    FunctionExpression="<fn id="SingleValueAssignment"  isCustomExpression="false"><arg>[//Target/LMSID]</arg></fn>"
    Description="Grab LMS ID"
    Destination="[//WorkflowData/LMSID]" />
  <ns1:ExecuteSQLActivity
    SQLStatement="EXECUTE [dbo].[SetLMSNewUserPassword]    @id   ,@password" x:Name="authenticationGateActivity8"
    SQLConnectionStringName="LMS"
    ParamData="<DocumentElement>   <Parameters>     <ParamName>@id</ParamName>     <ParamValue>[//WorkflowData/LMSID]</ParamValue>     <DataType>Integer</DataType>   </Parameters>   <Parameters>     <ParamName>@password</ParamName>     <ParamValue>[//WorkflowData/RandomPassword]</ParamValue>     <DataType>String</DataType>   </Parameters> </DocumentElement>"
    WorkflowKey="Results"
    TheActivityName="SQL: Set LMS password" />
  <ns0:FunctionActivity
    x:Name="authenticationGateActivity9"
    FunctionExpression="<fn id="SingleValueAssignment"  isCustomExpression="false"><arg>[//Target/VLMID]</arg></fn>"
    Description="Grab VLM ID"
    Destination="[//WorkflowData/VLMID]" />
  <ns1:ExecuteSQLActivity
    SQLStatement="EXECUTE [dbo].[SetVLMNewUserPassword]    @id   ,@password"
    x:Name="authenticationGateActivity10"
    SQLConnectionStringName="VLM"
    ParamData="<DocumentElement>   <Parameters>     <ParamName>@id</ParamName>     <ParamValue>[//WorkflowData/VLMID]</ParamValue>     <DataType>String</DataType>   </Parameters>   <Parameters>     <ParamName>@password</ParamName>     <ParamValue>[//WorkflowData/RandomPassword]</ParamValue>     <DataType>String</DataType>   </Parameters> </DocumentElement>"
    WorkflowKey="VLMResults"
    TheActivityName="SQL: Set VLM password" />
  <ns0:FunctionActivity
    x:Name="authenticationGateActivity9"
    FunctionExpression="<fn id="SingleValueAssignment"  isCustomExpression="false"><arg>[//Target/PDSID]</arg></fn>"
    Description="Grab PDS ID"
    Destination="[//WorkflowData/PDSID]" />
  <ns1:ExecuteSQLActivity
    SQLStatement="EXECUTE [dbo].[SetPDSNewUserPassword]    @id   ,@password"
    x:Name="authenticationGateActivity10"
    SQLConnectionStringName="PDS"
    ParamData="<DocumentElement>   <Parameters>     <ParamName>@id</ParamName>     <ParamValue>[//WorkflowData/PDSID]</ParamValue>     <DataType>String</DataType>   </Parameters>   <Parameters>     <ParamName>@password</ParamName>     <ParamValue>[//WorkflowData/RandomPassword]</ParamValue>     <DataType>String</DataType>   </Parameters> </DocumentElement>"
    WorkflowKey="PDSResults"
    TheActivityName="SQL: Set PDS password" />
  <ns1:ExpressionEvaluator
    x:Name="authenticationGateActivity4"
    DestinationValue="False"
    TheUpdateMode="0"
    Notes=""
    DestinationString="[//Target/SetPassword]"
    ActorID="31415926-5358-9793-2384-626433832795"
    ThisActivityName="Turn off SetPassword flag"
    ValueType="String" />
</ns0:SequentialWorkflow>


Monday, June 13, 2016

FIM Powershell Module: Remove/unset/clear a single-valued reference attribute

In the latest version of the FIM Powershell Module (2016-05-18), in order to remove/unset/clear a single-valued reference attribute, you're supposed to do this:

New-FimImportChange -Operation 'Replace' -AttributeName "Manager"

Note that you just don't supply the -AttributeValue paramter.  However, in my script, I don't want to perform the extra step of checking whether my value is present; so I'd like to do this:

New-FimImportChange -Operation 'Replace' -AttributeName "Manager" -AttributeValue "$newManager"

In order to do that, I had to make a small change to New-FimImportChange in FimPowerShellModule.psm1:

###
### Process the AttributeValue Parameter
###
if (!$AttributeValue)
{
    # Allow the caller to pass an empty AttributeValue to unset it, but DO NOT set the AttributeValue on the ImportChange object.
}
elseif ($AttributeValue -is [String])
{
    $importChange.AttributeValue = $AttributeValue
}
elseif ($AttributeValue -is [DateTime])



Wednesday, March 23, 2016

MIM metaverse SQL query - manager contributing MA

This is a sequel (no pun intended) to my old post, FIM metaverse SQL query - employeeID contributing MA.  Since 'manager' is a reference attribute, you need a slightly different query than for scalar attributes.

set transaction isolation level read uncommitted

SELECT TOP 1000
       mv.object_type
       ,mv.accountName
       ,mv.domain
       ,l.attribute_name
       ,ma_mgr.ma_name as [manager MA]
FROM [FIMSynchronizationService].[dbo].[mms_mv_link] l
join [FIMSynchronizationService].dbo.mms_metaverse mv
on l.object_id = mv.object_id

left join [FIMSynchronizationService].[dbo].[mms_lineage_cross_reference] cr_mgr
on cr_mgr.lineage_id = l.lineage_id
left join [FIMSynchronizationService].[dbo].[mms_management_agent] ma_mgr
on ma_mgr.ma_id = cr_mgr.ma_id

where object_type = 'person'
and l.attribute_name = 'manager'

Wednesday, March 2, 2016

ReplaceGuids.ps1

I was looking for a way to replace all GUIDs in a Visual Studio solution, so I took the answer to this StackOverflow question (Replacing all GUIDs in a file with new GUIDs from the command line) and extended it so that the script keeps track of GUIDs that are referenced across multiple files. An example is shown in the header below.

<#
    .Synopsis
    Replace all GUIDs in specified files under a root folder, carefully keeping track
    of how GUIDs are referenced in different files (e.g. Visual Studio solution).
   
    Loosely based on GuidSwap.ps1:
    http://stackoverflow.com/questions/2201740/replacing-all-guids-in-a-file-with-new-guids-from-the-command-line
   
    .NOTES
    Version:        1.0
    Author:         Joe Zamora (blog.idmware.com)
    Creation Date:  2016-03-01
    Purpose/Change: Initial script development
 
    .EXAMPLE
    .\ReplaceGuids.ps1 "C:\Code\IDMware" -FileNamePatterns @("*.sln","*.csproj","*.cs") -Verbose -WhatIf
#>

# Add common parameters to the script.
[CmdletBinding()]
param(
    $RootFolder
    ,$LogFolder='.'
    ,[String[]]$FileNamePatterns
    ,[switch]$WhatIf
)
$global:WhatIf = $WhatIf.IsPresent

# Change directory to the location of this script.
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
cd $dir
$ScriptName = $MyInvocation.MyCommand.Name

If(!($RootFolder))
{
    Write-Host @"
Usage: $ScriptName  -RootFolder <rootfolder> [Options]

Options:
    -LogFolder <logfolder>                      Defaults to location of script.
   
    -FileNamePatterns @(*.ext1, *.ext2, ...)    Defaults to all files (*).
   
    -WhatIf                                     Test run without replacements.
   
    -Verbose                                    Standard Powershell flags.
    -Debug
"@
    Exit
}

if ($LogFolder -and !(Test-Path "$LogFolder" -PathType Container))
{
      Write-Host "No such folder: `"$LogFolder`""
      Exit
}

<#
    .Synopsis
    This code snippet gets all the files in $Path that contain the specified pattern.
    Based on this sample:
    http://www.adminarsenal.com/admin-arsenal-blog/powershell-searching-through-files-for-matching-strings/
#>
function Enumerate-FilesContainingPattern {
[CmdletBinding()]
param(
    $Path=(throw 'Path cannot be empty.')
    ,$Pattern=(throw 'Pattern cannot be empty.')
    ,[String[]]$FileNamePatterns=$null
)
    $PathArray = @()
    if (!$FileNamePatterns) {
        $FileNamePatterns = @("*")
    }

    ForEach ($FileNamePattern in $FileNamePatterns) {
        Get-ChildItem $Path -Recurse -Filter $FileNamePattern |
        Where-Object { $_.Attributes -ne "Directory"} |
        ForEach-Object {
            If (Get-Content $_.FullName | Select-String -Pattern $Pattern) {
                $PathArray += $_.FullName
            }
        }
    }
    $PathArray
} <# function Enumerate-FilesContainingPattern #>

# Timestamps and performance.
$stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
$startTime = Get-Date
Write-Verbose @"

--- SCRIPT BEGIN $ScriptName $startTime ---

"@

# Begin by finding all files under the root folder that contain a GUID pattern.
$GuidRegexPattern = "[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}"
$FileList = Enumerate-FilesContainingPattern $RootFolder $GuidRegexPattern $FileNamePatterns
   
$LogFilePrefix = "{0}-{1}" -f $ScriptName, $startTime.ToString("yyyy-MM-dd_HH-mm-ss")
$FileListLogFile = Join-Path $LogFolder "$LogFilePrefix-FileList.txt"
$FileList | ForEach-Object {$_ | Out-File $FileListLogFile -Append}
Write-Host "File list log file:`r`n$FileListLogFile"
cat $FileListLogFile | %{Write-Verbose $_}

# Next, do a read-only loop over the files and build a mapping table of old to new GUIDs.
$guidMap = @{}
foreach ($filePath in $FileList)
{
    $text = [string]::join([environment]::newline, (get-content -path $filePath))
    Foreach ($match in [regex]::matches($text, $GuidRegexPattern)) {
        $oldGuid = $match.Value.ToUpper()
        if (!$guidMap.ContainsKey($oldGuid)) {
            $newGuid = [System.Guid]::newguid().ToString().ToUpper()
            $guidMap[$oldGuid] = $newGuid
        }
    }
}

$GuidMapLogFile = Join-Path $LogFolder "$LogFilePrefix-GuidMap.csv"
"OldGuid,NewGuid" | Out-File $GuidMapLogFile
$guidMap.Keys | % { "$_,$($guidMap[$_])" | Out-File $GuidMapLogFile -Append }
Write-Host "GUID map log file:`r`n$GuidMapLogFile"
cat $GuidMapLogFile | %{Write-Verbose $_}

# Finally, do the search-and-replace.
foreach ($filePath in $FileList) {
    Write-Verbose "Processing $filePath"
    $newText = New-Object System.Text.StringBuilder
    cat $filePath | % {
        $original = $_
        $new = $_
        $isMatch = $false
        $matches = [regex]::Matches($new, $GuidRegexPattern)
        foreach ($match in $matches) {
            $isMatch = $true
            $new = $new -ireplace $match.Value, $guidMap[$match.Value.ToString().ToUpper()]
        }       
        $newText.AppendLine($new) | Out-Null
        if ($isMatch) {
            $msg = "Old: $original`r`nNew: $new"
            if ($global:WhatIf) {
                Write-Host "What if:`r`n$msg"
            } else {
                Write-Verbose "`r`n$msg"
            }
        }
    }
    if (!$global:WhatIf) {
        $newText.ToString() | Set-Content $filePath
    }
}

# Timestamps and performance.
$endTime = Get-Date
Write-Verbose @"

--- SCRIPT END $ScriptName $endTime ---

Total elapsed: $($stopWatch.Elapsed)
"@