Public/Metaverse/Get-JIMMetaverseObjectConnection.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Get-JIMMetaverseObjectConnection {
    <#
    .SYNOPSIS
        Explains why a Metaverse Object is connected where it is, and why it is not connected elsewhere.
 
    .DESCRIPTION
        Returns one object per Connected System Object joined to the Metaverse Object: how it was
        joined (method, Synchronisation Rule, date and Activity) and the scoping of every relevant
        enabled Synchronisation Rule, evaluated now against current values.
 
        With -IncludeNotConnected, also returns one object per enabled export Synchronisation Rule
        whose Connected System holds no object joined to this one, with the reason (NotInScope,
        ProvisioningDisabled, RuleMisconfigured or NotYetProvisioned), a one-line hint, what would
        change it, and a plain-text summary to paste into a ticket. The words are generated by JIM
        and are the ones the portal's Connections tab shows.
 
        Every object has the same top-level properties, so connections and not-connected entries can
        be listed, filtered and sorted together. Values of credential attributes are withheld.
 
    .PARAMETER Id
        The ID of the Metaverse Object.
 
    .PARAMETER ConnectedSystemName
        Returns only the entries for the Connected System with this name, matched exactly but ignoring case. Nothing
        matching is an error.
 
    .PARAMETER IncludeNotConnected
        Also explains each enabled export Synchronisation Rule whose Connected System holds no object joined to this
        one.
 
    .OUTPUTS
        One object per entry: each joined Connected System Object and, with -IncludeNotConnected, each export
        Synchronisation Rule that has not connected. Every entry has the same properties, so the two can be listed,
        filtered and sorted together. Values of credential attributes are withheld.
 
        JIM.MetaverseObjectConnectionEntry
        MetaverseObjectId (guid): The unique identifier of the Metaverse Object.
        MetaverseObjectName (string): The Metaverse Object's display name.
        ConnectedSystem (string): The Connected System's name.
        ConnectedSystemId (int): The unique identifier of the Connected System.
        Connected (bool): $true for a joined Connected System Object, $false for an export Synchronisation Rule that
            has not connected.
        Object (string): The joined Connected System Object's identifier as the portal's Connections tab shows it: its
            Secondary External ID (for an LDAP Connected System, its distinguished name) where it has one, otherwise its
            External ID, falling back to the value a queued Pending Export will assign; empty when not connected.
        ConnectedSystemObjectId (guid): The unique identifier of the joined Connected System Object; empty when not
            connected.
        ExternalId (string): The joined object's External ID; empty when it has none yet or when not connected.
        SecondaryExternalId (string): The joined object's Secondary External ID; empty when its Connected System has
            none, it has no value yet, or when not connected.
        PendingExternalId (string): The External ID a queued Pending Export will give the joined object, where it has
            none yet; empty otherwise.
        PendingSecondaryExternalId (string): The Secondary External ID a queued Pending Export will give the joined
            object, where it has none yet; empty otherwise.
        ObjectType (string): The Connected System Object Type's name.
        Role (string): Source, Target, Source and Target or None, from the enabled Synchronisation Rules for that
            Connected System and Object Type; always Target when not connected.
        State (string): For a connection, its state, such as InSync or UpdatePending; when not connected, why:
            NotInScope, ProvisioningDisabled, RuleMisconfigured or NotYetProvisioned.
        SyncRule (string): The export Synchronisation Rule that has not connected; empty for a connection.
        Join (object): For a connection, how it was joined: JoinType, Method, DateJoined, SyncRuleId, SyncRuleName,
            Source, Description, ActivityId and RunProfileExecutionItemId.
        Hint (string): When not connected, a one-line hint at what would change it.
        BulletsTitle (string): When not connected, the heading for Bullets.
        Bullets (string[]): When not connected, what would change it, a line each.
        Summary (string): When not connected, a plain-text summary to paste into a ticket.
        Scoping (object[]): One explanation per relevant Synchronisation Rule, evaluated now against current values:
            SyncRuleName, Direction, Outcome, Hint, Groups and Criteria, each criterion with Path, Met, Outcome,
            AttributeName, ComparisonType, Expected, Actual, ValueCount, Description and ActualDescription; an
            expression criterion adds Expression, ExpressionInputs (each attribute it read, with Accessor, Values,
            ValueCount and Masked) and, when it could not be evaluated, FailureReason.
        Conflicts (object[]): For a connection, the export Synchronisation Rules that cannot connect because it
            holds the slot.
        EvaluatedAt (datetime): When JIM evaluated the explanations (UTC).
 
    .EXAMPLE
        Get-JIMMetaverseObjectConnection -Id "12345678-1234-1234-1234-123456789abc" -IncludeNotConnected |
            Format-Table ConnectedSystem, Connected, State, Hint
 
        Lists where the Metaverse Object is connected, and why it is not connected elsewhere.
 
    .EXAMPLE
        (Get-JIMMetaverseObjectConnection -Id $mvoId -IncludeNotConnected -ConnectedSystemName "Finance App").Summary
 
        Prints the plain-text explanation of why the Metaverse Object is not in Finance App, ready to
        paste into a ticket.
 
    .EXAMPLE
        Get-JIMMetaverseObjectConnection -Id $mvoId -IncludeNotConnected |
            ForEach-Object { $_.Scoping } |
            ForEach-Object { $_.Criteria } |
            Where-Object { -not $_.Met } |
            Format-Table Path, Description, ActualDescription
 
        Lists every scoping criterion the Metaverse Object currently fails, across all its rules.
 
    .EXAMPLE
        Get-JIMMetaverseObject -AttributeName "Account Name" -AttributeValue jsmith |
            Get-JIMMetaverseObjectConnection |
            Format-Table ConnectedSystem, Object, @{ Name = 'Joined'; Expression = { $_.Join.Description } }
 
        Pipes a Metaverse Object into the cmdlet to see how each of its connections was joined.
 
    .LINK
        Get-JIMMetaverseObject
 
    .LINK
        Get-JIMMetaverseObjectProvenance
 
    .LINK
        Get-JIMSyncRule
    #>

    [CmdletBinding()]
    [OutputType('JIM.MetaverseObjectConnectionEntry')]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Guid]$Id,

        [ValidateNotNullOrEmpty()]
        [string]$ConnectedSystemName,

        [switch]$IncludeNotConnected
    )

    process {
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        $endpoint = "/api/v1/metaverse/objects/$Id/connections"
        if ($IncludeNotConnected) {
            $endpoint += '?includeNotConnected=true'
        }

        Write-Verbose "Getting connection explanations for Metaverse Object $Id"
        $response = Invoke-JIMApi -Endpoint $endpoint
        if (-not $response) {
            # Invoke-JIMApi already wrote the error (e.g. Metaverse Object not found).
            return
        }

        $entries = [System.Collections.Generic.List[object]]::new()

        foreach ($connection in $response.Connections) {
            $role = if ($connection.IsSource -and $connection.IsTarget) { 'Source and Target' }
                    elseif ($connection.IsSource) { 'Source' }
                    elseif ($connection.IsTarget) { 'Target' }
                    else { 'None' }

            $entries.Add([PSCustomObject]@{
                MetaverseObjectId       = $response.MetaverseObjectId
                MetaverseObjectName     = $response.DisplayName
                ConnectedSystem         = $connection.ConnectedSystemName
                ConnectedSystemId       = $connection.ConnectedSystemId
                Connected               = $true
                Object                  = @($connection.SecondaryExternalId, $connection.PendingSecondaryExternalId,
                                              $connection.ExternalId, $connection.PendingExternalId) |
                                              Where-Object { $_ } | Select-Object -First 1
                ConnectedSystemObjectId = $connection.ConnectedSystemObjectId
                ExternalId              = $connection.ExternalId
                SecondaryExternalId     = $connection.SecondaryExternalId
                PendingExternalId       = $connection.PendingExternalId
                PendingSecondaryExternalId = $connection.PendingSecondaryExternalId
                ObjectType              = $connection.ObjectTypeName
                Role                    = $role
                State                   = $connection.State
                SyncRule                = $null
                Join                    = $connection.Join
                Hint                    = $null
                BulletsTitle            = $null
                Bullets                 = @()
                Summary                 = $null
                Scoping                 = @($connection.Scoping)
                Conflicts               = @($connection.Conflicts)
                EvaluatedAt             = $response.EvaluatedAt
            })
        }

        foreach ($entry in $response.NotConnected) {
            $entries.Add([PSCustomObject]@{
                MetaverseObjectId       = $response.MetaverseObjectId
                MetaverseObjectName     = $response.DisplayName
                ConnectedSystem         = $entry.ConnectedSystemName
                ConnectedSystemId       = $entry.ConnectedSystemId
                Connected               = $false
                Object                  = $null
                ConnectedSystemObjectId = $null
                ExternalId              = $null
                SecondaryExternalId     = $null
                PendingExternalId       = $null
                PendingSecondaryExternalId = $null
                ObjectType              = $entry.ObjectTypeName
                Role                    = 'Target'
                State                   = $entry.Reason
                SyncRule                = $entry.SyncRuleName
                Join                    = $null
                Hint                    = $entry.Hint
                BulletsTitle            = $entry.BulletsTitle
                Bullets                 = @($entry.Bullets | ForEach-Object { $_.Text })
                Summary                 = $entry.Summary
                Scoping                 = @($entry.Scoping)
                Conflicts               = @()
                EvaluatedAt             = $response.EvaluatedAt
            })
        }

        if ($PSBoundParameters.ContainsKey('ConnectedSystemName')) {
            $matched = @($entries | Where-Object { $_.ConnectedSystem -eq $ConnectedSystemName })
            if ($matched.Count -eq 0) {
                $hint = if ($IncludeNotConnected) { '' } else { ' Add -IncludeNotConnected to include Connected Systems it is not connected to.' }
                Write-Error "No connection to a Connected System named '$ConnectedSystemName' was found for Metaverse Object $Id.$hint"
                return
            }
            return $matched
        }

        $entries
    }
}