Providers/Entra/Private/Resolve-EntraSeededId.ps1

function Resolve-EntraSeededId {
    <#
    .SYNOPSIS
        Maps a seed data key to the object id it was created as

    .DESCRIPTION
        The CSV files reference each other by key - a group lists members as 'jnino', a
        Conditional Access policy scopes itself to 'dept-engineering' - and Graph needs object
        ids. This resolves one to the other by querying the tenant rather than by remembering
        what a previous step created.

        That is the whole point. Every seeding function has to work standalone, so somebody
        can rebuild just the groups over an existing set of users without re-running anything
        else. A resolver that depended on in-memory state from the same run would make each
        function only usable as part of the orchestrator.

        Lookups are cached for the lifetime of the call chain, because a full seed resolves
        the same handful of users dozens of times across groups, devices, applications and
        policies, and each miss is a network round trip.

    .PARAMETER Key
        The seed data key, for example 'jnino' or 'dept-engineering'

    .PARAMETER Kind
        Whether the key names a user or a group

    .PARAMETER Cache
        A hashtable reused across calls to avoid repeating lookups

    .PARAMETER Connection
        Connection to use instead of the module's active one

    .OUTPUTS
        System.String, the object id, or $null when the object does not exist

    .EXAMPLE
        PS> Resolve-EntraSeededId -Key jnino -Kind User -Cache $cache

        DESCRIPTION: Finds the object id of the seeded user whose key is jnino
        OUTPUT: 5ba0f0b4-6299-4ff5-893e-92d22a597ca6
        USE CASE: Turning a CSV member list into the ids Graph needs

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Key,

        [Parameter(Mandatory = $true)]
        [ValidateSet('User', 'Group')]
        [string]$Kind,

        [Parameter()]
        [hashtable]$Cache,

        [Parameter()]
        [hashtable]$Connection
    )

    if (-not $Connection) { $Connection = Get-EntraConnection }
    $marker = Get-EntraSeedMarker -Connection $Connection

    $cacheKey = "$Kind::$Key"
    if ($Cache -and $Cache.ContainsKey($cacheKey)) { return $Cache[$cacheKey] }

    $id = $null

    if ($Kind -eq 'User') {
        $upn = '{0}{1}@{2}' -f $marker.Prefix, $Key, $marker.UpnSuffix
        # Addressed by UPN directly rather than filtered. It is one call either way, and a
        # direct address cannot return two results the way a filter can.
        try {
            $user = Invoke-EntraRequest -Method GET -Path "/users/$([uri]::EscapeDataString($upn))" `
                -Query @{ '$select' = 'id' } -Connection $Connection
            $id = $user.id
        }
        catch {
            Write-Verbose "No seeded user for key '$Key' ($upn): $($_.Exception.Message)"
        }
    }
    else {
        $definition = @(Get-EntraSeedData -Name 'EntraGroups') | Where-Object { $_.Key -eq $Key } | Select-Object -First 1
        if (-not $definition) {
            Write-Verbose "No group seed definition with key '$Key'"
        }
        else {
            $displayName = '{0}{1}' -f $marker.Prefix, $definition.DisplayName
            # Escaped for OData, where a single quote inside a string literal is doubled. No
            # seeded name contains one today, and a resolver that breaks the first time
            # somebody adds one is a resolver that breaks silently.
            $literal = $displayName.Replace("'", "''")

            # Deliberately not called $matches. That is an automatic variable PowerShell
            # overwrites on every -match operation, so any regex evaluated between here and
            # the read below would silently replace this result with capture groups.
            $found = @(Invoke-EntraRequest -Method GET -Path '/groups' -Connection $Connection -Paginate -ConsistencyLevel `
                    -Query @{ '$filter' = "displayName eq '$literal'"; '$select' = 'id,displayName' })

            if ($found.Count -gt 1) {
                Write-Warning ("Found $($found.Count) groups called '$displayName'. Using the first; the " +
                    "duplicates were not created by this module or were created by an interrupted run.")
            }
            if ($found.Count -ge 1) { $id = $found[0].id }
            else { Write-Verbose "No seeded group named '$displayName'" }
        }
    }

    if ($Cache) { $Cache[$cacheKey] = $id }
    return $id
}