ResolveEntraID.psm1

function Clear-MeidIdentityCache {
    <#
    .SYNOPSIS
    Clears the Entra ID identiy cache.
     
    .DESCRIPTION
    Clears the Microsoft Entra ID identiy cache. The cache is used to cache the mapping between the ID and resolved name.
     
    .EXAMPLE
    PS C:\> Clear-MeidIdentityCache
     
    Clears the Entra ID identiy cache.
    #>

    [CmdletBinding()]
    param ()
    $script:IdNameMappingTable = @{}
}

function Get-MeidIdentityProvider {
    <#
    .SYNOPSIS
    Get registered Entra ID identity provider.
     
    .DESCRIPTION
    Get by the user registered Microsoft Entra ID identity provider.
     
    .PARAMETER Name
    Can be used to filter for Name of the registered Entra ID identity provider.
     
    .EXAMPLE
    PS C:\> Get-MeidIdentityProvider
 
    Get all registered Microsoft Entra ID identity provider.
     
    .EXAMPLE
    PS C:\> Get-MeidIdentityProvider -Name "*User*"
 
    Get registered Microsoft Entra ID identity provider where name like "*User*".
    #>

    [CmdletBinding()]
    param (
        [PSFArgumentCompleter("ResolveEntraID.Provider")]
        [string]
        $Name = '*'
    )
    process {
        $script:IdentityProvider.Values | Where-Object Name -like $Name
    }
}

function Register-MeidIdentityProvider {
    <#
    .SYNOPSIS
    Register Entra ID identity provider.
     
    .DESCRIPTION
    Register Microsoft Entra ID identity provider.
     
    .PARAMETER Name
    Name(s) of the Entra ID identity provider.
     
    .PARAMETER NameProperty
    Property(s) to search for. E.g.: userPrincipalName
     
    .PARAMETER Query
    Graph endpoint url. E.g.: users
     
    .EXAMPLE
    PS C:\> Register-MeidIdentityProvider -Name "UserUPN" -NameProperty "userPrincipalName" -Query "users"
 
    Will register a provider with name "UserUPN", the property to search for "userPrincipalName" with the query "users/{0}".
     
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string[]]
        $Name,

        [Parameter(Mandatory)]
        [string[]]
        $NameProperty,

        [Parameter(Mandatory)]
        [string[]]
        $Query
    )
    process {
        $queries = foreach ($item in $Query){
            if($item -match "\{0\}"){ $item; continue}
            $item.TrimEnd("/").Replace("{","{{").Replace("}","}}"), "{0}" -join "/"
        }
        foreach ($entry in $Name) {
            $script:IdentityProvider[$entry] = [PSCustomObject]@{
                Name         = $entry
                NameProperty = $NameProperty
                Query        = $queries
            }
        }
    }
}

function Resolve-MeidIdentity {
    <#
    .SYNOPSIS
    Resolve Entra ID identity.
     
    .DESCRIPTION
    Resolve Microsoft Entra ID identity.
    This function will resolve an ID to a user defined property.
    Requires an active connection to Azure with MiniGraph.
     
    .PARAMETER Id
    ID(s) that should be resolved by the function.
     
    .PARAMETER Provider
    Provider(s) that should be used to resolve the ID(s).
     
    .PARAMETER NoCache
    Option that no Cache will be created. ID(s) with the mapping property will not be cached.
     
    .PARAMETER NameOnly
    Option that only show the resolved name.
     
    .EXAMPLE
    PS C:\> Resolve-MeidIdentity -ID "xyz" -Provider UserUPN
 
    Will resolve the ID "xyz" with defined property in the provider "UserUPN".
    The written output is ID, Name (Property), Provider and the result will be written in the cache.
     
    .EXAMPLE
    PS C:\> Resolve-MeidIdentity -ID "xyz","abc" -Provider UserUPN,Group
 
    Will resolve the IDs "xyz" and "abc" with defined property in the providers "UserUPN" and "Group".
    The written output is ID, Name (Property), Provider and the result will be written in the cache.
 
    .EXAMPLE
    PS C:\> Resolve-MeidIdentity -ID "xyz","abc" -Provider UserUPN,Group -NoCache
 
    Will resolve the IDs "xyz" and "abc" with defined property in the providers "UserUPN" and "Group".
    The written output is ID, Name (Property), Provider and the result will NOT be written in the cache.
 
    .EXAMPLE
    PS C:\> Resolve-MeidIdentity -ID "xyz","abc" -Provider UserUPN,Group -NoCache
 
    Will resolve the IDs "xyz" and "abc" with defined property in the providers "UserUPN" and "Group".
    The written output is only Name (Property) and the result will NOT be written in the cache.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]
        $Id,

        [Parameter(Mandatory)]
        [PSFArgumentCompleter("ResolveEntraID.Provider")]
        [PSFValidateSet(TabCompletion = "ResolveEntraID.Provider")]
        [string[]]
        $Provider,

        [switch]
        $NoCache,

        [switch]
        $NameOnly
    )
    begin {
        function Write-Result {
            <#
            .SYNOPSIS
            Write the result
     
            .DESCRIPTION
            Write the result in several variants:
            ID, Name (resolved Property), Provider
 
            or with NameOnly
            Name
 
            The function will also write the result in the cache (IdNameMappingTable), if NoCache isn't set.
            #>

            [OutputType([string])]
            [CmdletBinding()]
            param (
                [string]
                $Id,

                [string]
                $Provider,

                [string]
                $Value,

                [switch]
                $NameOnly,

                [switch]
                $NoCache
            )
            $result = [PSCustomObject]@{
                ID       = $Id
                Name     = $Value
                Provider = $Provider
            }
            if ($NameOnly) { $Value }
            else { $result }

            if ($NoCache) { return }

            if (-not $script:IdNameMappingTable[$Provider]) {
                $script:IdNameMappingTable[$Provider] = @{}
            }
            $script:IdNameMappingTable[$Provider][$Id] = $result
        }
    }
    process {
        :main foreach ($entry in $Id) {
            if ($entry -notmatch '^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$') {
                $entry
                continue
            }
            foreach ($providerName in $Provider) {
                if ($NoCache) { break }
                if ($script:IdNameMappingTable[$providerName].$entry) {
                    Write-Result -Id $entry -Value $script:IdNameMappingTable[$providerName].$entry.Name -NoCache -Provider $providerName -NameOnly:$NameOnly
                    continue main
                }
            }
            foreach ($providerName in $Provider) {
                $providerObject = $script:IdentityProvider[$providerName]
                if (-not $providerObject) {
                    Write-PSFMessage -Level Error -Message "Could not find identity provider {0}. Please register or check the spelling of the provider. Known providers: {1}" -StringValues $providerName, ((Get-MeidIdentityProvider).Name -join ", ") -Target $providerName
                    continue
                }
                try {
                    $graphResponse = MiniGraph\Invoke-GraphRequest -Query ($providerObject.Query -f $entry) -ErrorAction Stop
                    if (-not $graphResponse) { continue }
                    foreach ($propertyName in $providerObject.NameProperty) {
                        $resolvedName = $graphResponse.$propertyName
                        if (-not $resolvedName) { continue }
                        break
                    }
                    if (-not $resolvedName) {
                        $resolvedName = $entry
                        Write-PSFMessage -Level SomewhatVerbose -Message "{0} of type {1} could be found but failed to resolve the {2}." -StringValues $entry, $providerName, ($providerObject.NameProperty -join ", ") -Target $entry -Tag $providerName
                    }
                    Write-Result -Id $entry -Value $resolvedName -Provider $providerName -NameOnly:$NameOnly -NoCache:$NoCache
                    continue main
                }
                catch {
                    if ($_.ErrorDetails.Message -match '"code":"Request_ResourceNotFound"') {
                        Write-PSFMessage -Level InternalComment -Message "ID {0} could not found as {1}." -StringValues $entry, $providerName -Target $entry -Tag $providerName -ErrorRecord $_ -OverrideExceptionMessage
                        continue
                    }
                    Write-PSFMessage -Level Error -Message "Error resolving {0}." -StringValues $entry -ErrorRecord $_ -Target $entry -Tag $providerName, "fail" -EnableException $true -PSCmdlet $PSCmdlet
                }
            }
            Write-Result -Id $entry -Value $entry -Provider "Unknown" -NameOnly:$NameOnly -NoCache
        }
    }
    end {
    
    }
}

# ID for Name mapping
$script:IdNameMappingTable = @{}

# Identity Provider
$script:IdentityProvider = @{}

Register-PSFTeppScriptblock -Name ResolveEntraID.Provider -ScriptBlock {
    foreach ($provider in Get-MeidIdentityProvider){
        @{
            Text = $provider.Name
            ToolTip = "{0} --> {1}" -f $provider.Name, ($provider.NameProperty -join ", ")
        }
    }
}

$param = @{
    Name = "Application"
    NameProperty = "displayName"
    Query = "applications/{0}"
}
Register-MeidIdentityProvider @param

$param = @{
    Name = "Group"
    NameProperty = "displayName"
    Query = "groups/{0}"
}
Register-MeidIdentityProvider @param

$param = @{
    Name = "User"
    NameProperty = "userPrincipalName"
    Query = "users/{0}"
}
Register-MeidIdentityProvider @param