functions/roleManagement/roleAssignments/Export-TmfRoleAssignment.ps1


<#
.SYNOPSIS
Exports role assignments (active and eligible) into TMF configuration objects or JSON.
.DESCRIPTION
Retrieves directory role assignments and eligibility schedules (optionally AzureResources/AADGroup scoping placeholder) and converts them to TMF shape including principal, role and scope metadata. Returns objects unless -OutPath is supplied.
.PARAMETER SpecificResources
Optional list of IDs, principal display names, or role definition display names (comma separated accepted) to filter.
.PARAMETER Scope
AzureResources | AzureAD | AADGroup (default AzureAD).
.PARAMETER OutPath
Root folder to write export; when omitted objects are returned.
.PARAMETER Append
Add content to an existing file
.PARAMETER ForceBeta
Use beta Graph endpoint for retrieval (experimental; current retrieval uses directory scope endpoints).
.PARAMETER Cmdlet
Internal pipeline parameter; do not supply manually.
.EXAMPLE
Export-TmfRoleAssignment -Scope AzureAD -OutPath C:\temp\tmf
.EXAMPLE
Export-TmfRoleAssignment -Scope AzureResources -SpecificResources Owner
NOTE: Parameter `-OutPutPath` is deprecated; retained as alias.
#>

function Export-TmfRoleAssignment {
    

    [CmdletBinding()] param(
        [string[]] $SpecificResources,
        [ValidateSet('AzureResources', 'AzureAD', 'AADGroup')] [string] $Scope,
        [Alias('OutPutPath')] [string] $OutPath,
        [switch] $Append,
        [switch] $ForceBeta,
        [System.Management.Automation.PSCmdlet] $Cmdlet = $PSCmdlet
    )

    begin {
        Test-GraphConnection -Cmdlet $Cmdlet
        $resourceName = 'roleAssignments'
        $tenant = (Invoke-MgGraphRequest -Method GET -Uri ("$($script:graphBaseUrl)/organization?`$select=displayname,id")).value
        $roleAssignmentsExport = @()
        Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfRoleAssignment' -Message "Scope=$Scope ForceBeta=$ForceBeta"
        function Convert-RoleAssignment {
            param([object]$assignment, [string]$assignmentScope)
            $obj = [ordered]@{ present = $true }
            # Type
            if ($assignment -and $assignment.PSObject.Members.Match('assignmentState')) {
                $obj.type = $assignment.assignmentState.ToLower()
            } else {
                $obj.type = 'active'
            }
            # Principal reference/type
            if ($assignment.principal -and $assignment.principal.PSObject.Members.Match('displayName')) {
                $obj.principalReference = $assignment.principal.displayName
            }
            if ($assignment.principal) {
                switch ($assignment.principal.'@odata.type') {
                    '#microsoft.graph.user' {
                        $obj.principalType = 'user'
                    }
                    '#microsoft.graph.group' {
                        $obj.principalType = 'group'
                    }
                    '#microsoft.graph.servicePrincipal' {
                        $obj.principalType = 'servicePrincipal'
                    }
                    default {
                        $obj.principalType = 'user'
                    }
                }
            }
            # Role reference
            if ($assignment.roleDefinition -and $assignment.roleDefinition.PSObject.Members.Match('displayName')) {
                $obj.roleReference = $assignment.roleDefinition.displayName
            }
            # Scope mapping
            if ($assignmentScope -eq 'AzureResources') {
                if ($assignment.PSObject.Members.Match('directoryScopeId') -and $assignment.directoryScopeId) {
                    $scopeId = $assignment.directoryScopeId
                    if ($scopeId -match '/subscriptions/([^/]+)') {
                        $subId = $matches[1]; $obj.subscriptionReference = $subId
                        if ($scopeId -match '/subscriptions/[^/]+/resourceGroups/([^/]+)') {
                            $obj.scopeReference = $matches[1]; $obj.scopeType = 'resourceGroup'
                        } else {
                            $obj.scopeReference = $subId; $obj.scopeType = 'subscription'
                        }
                    }
                }
            } else {
                if ($assignment.PSObject.Members.Match('directoryScopeId') -and $assignment.directoryScopeId) {
                    $directoryScopeId = $assignment.directoryScopeId
                    if ($directoryScopeId -eq '/' -or $directoryScopeId -eq '') {
                        $obj.directoryScopeReference = '/'; $obj.directoryScopeType = 'directory'
                    } else {
                        if ($directoryScopeId -match 'administrativeUnits') {
                            $obj.directoryScopeReference = Resolve-AdministrativeUnit $directoryScopeId.split("/")[2] -DisplayName; $obj.directoryScopeType = 'administrativeUnit'
                        }
                        else {
                            $obj.directoryScopeReference = Resolve-Application $directoryScopeId.replace("/","") -DisplayName; $obj.directoryScopeType = 'application'
                        }                        
                    }
                } else {
                    $obj.directoryScopeReference = '/'; $obj.directoryScopeType = 'directory'
                }
            }
            # Schedule / expiration (null-safe)
            $obj.expirationType = 'noExpiration'
            if ($assignment.PSObject.Members.Match('scheduleInfo') -and $assignment.scheduleInfo) {
                $schedule = $assignment.scheduleInfo
                if ($schedule -and $schedule.PSObject.Members.Match('startDateTime') -and $schedule.startDateTime) {
                    $obj.startDateTime = $schedule.startDateTime
                }
                if ($schedule -and $schedule.PSObject.Members.Match('expiration') -and $schedule.expiration) {
                    $expiration = $schedule.expiration
                    if ($expiration -and $expiration.PSObject.Members.Match('type') -and $expiration.type) {
                        switch ($expiration.type) {
                            'noExpiration' {
                                $obj.expirationType = 'noExpiration'
                            }
                            'afterDateTime' {
                                $obj.expirationType = 'AfterDateTime'
                            }
                            'afterDuration' {
                                $obj.expirationType = 'AfterDuration'
                            }
                            default {
                                $obj.expirationType = $expiration.type
                            }
                        }
                    }
                    if ($expiration -and $expiration.PSObject.Members.Match('endDateTime') -and $expiration.endDateTime) {
                        $obj.endDateTime = $expiration.endDateTime
                    }
                    if ($expiration -and $expiration.PSObject.Members.Match('duration') -and $expiration.duration) {
                        $obj.duration = $expiration.duration
                    }
                }
            }
            return $obj
        }
        function Get-AllRoleAssignments {
            param([string]$assignmentScope)
            $apiBase = if ($ForceBeta) {
                $script:graphBaseUrlbeta
            } else {
                $script:graphBaseUrl1
            }
            $list = @()
            try {
                $activeResp = Invoke-MgGraphRequest -Method GET -Uri "$apiBase/roleManagement/directory/roleAssignmentSchedules?`$expand=principal" -ErrorAction Stop
            } catch {
                Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -Message "Active role assignments error: $($_.Exception.Message)"; $activeResp = @{value = @() }
            }
            try {
                $eligibleResp = Invoke-MgGraphRequest -Method GET -Uri "$apiBase/roleManagement/directory/roleEligibilitySchedules?`$expand=principal" -ErrorAction Stop
            } catch {
                Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -Message "Eligible role assignments error: $($_.Exception.Message)"; $eligibleResp = @{value = @() }
            }
            $activeAssignments = @()
            foreach ($item in $activeResp.value) {
                try {
                    $roleDefResp = Invoke-MgGraphRequest -Method GET -Uri "$apiBase/roleManagement/directory/roleDefinitions/$($item.roleDefinitionId)"; $item | Add-Member -MemberType NoteProperty -Name roleDefinition -Value $roleDefResp -Force
                } catch {
                    Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -Message "Role retrieval error: $($_.Exception.Message)"
                }
                $item | Add-Member -MemberType NoteProperty -Name assignmentState -Value Active -Force; $activeAssignments += $item
            }
            $eligibleAssignments = @()
            foreach ($item in $eligibleResp.value) {
                try {
                    $roleDefResp = Invoke-MgGraphRequest -Method GET -Uri "$apiBase/roleManagement/directory/roleDefinitions/$($item.roleDefinitionId)"; $item | Add-Member -MemberType NoteProperty -Name roleDefinition -Value $roleDefResp -Force
                } catch {
                    Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -Message "Role retrieval error: $($_.Exception.Message)"
                }
                $item | Add-Member -MemberType NoteProperty -Name assignmentState -Value Eligible -Force; $eligibleAssignments += $item
            }
            foreach ($i in $activeAssignments) {
                $list += $i
            }
            foreach ($i in $eligibleAssignments) {
                $list += $i
            }
            Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfRoleAssignment' -Message "Retrieved $($list.Count) role assignment records"
            return $list
        }
    }
    process {
        if ($Scope -and ($Scope -ne "AzureAD")) {
            $assignmentScope = 'AzureAD'
            Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -String 'TMF.Export.ScopeNotSupported' -StringValues $Scope,$resourceName,$assignmentScope
        } else {
            $assignmentScope = 'AzureAD'
        }
        if ($SpecificResources) {
            $identifiers = @()
            foreach ($entry in $SpecificResources) {
                $identifiers += $entry -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
            }
            $identifiers = $identifiers | Select-Object -Unique
            $allRoleAssignments = Get-AllRoleAssignments -assignmentScope $assignmentScope
            foreach ($idOrName in $identifiers) {
                $match = $allRoleAssignments | Where-Object {
                    $_.id -eq $idOrName -or
                    ($_.principal -and $_.principal.displayName -eq $idOrName) -or
                    ($_.roleDefinition -and $_.roleDefinition.displayName -eq $idOrName)
                }
                if ($match) {
                    foreach ($m in $match) {
                        $convertedAssignment = Convert-RoleAssignment $m $assignmentScope
                        $roleAssignmentsExport += $convertedAssignment
                    }
                } else {
                    Write-PSFMessage -Level Warning -FunctionName 'Export-TmfRoleAssignment' -String 'TMF.Export.NotFound' -StringValues $idOrName, $resourceName, $tenant.displayName
                }
            }
        } else {
            $allRoleAssignments = Get-AllRoleAssignments -assignmentScope $assignmentScope
            Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfRoleAssignment' -String 'TMF.Export.RetrievedResources' -StringValues ($allRoleAssignments.Count), $resourceName, $tenant.displayName

            foreach ($assignment in $allRoleAssignments) {
                $convertedAssignment = Convert-RoleAssignment $assignment $assignmentScope
                $roleAssignmentsExport += $convertedAssignment
            }
        }
    }
    end {
        Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfRoleAssignment' -Message "Exporting $($roleAssignmentsExport.Count) role assignment(s)"
        if ($OutPath) {
            if ($roleAssignmentsExport) {
                if ($Append) {
                    Write-TmfExportFile -OutPath $OutPath -ParentPath 'roleManagement' -ResourceName $resourceName -Data $roleAssignmentsExport -Append
                }
                else {
                    Write-TmfExportFile -OutPath $OutPath -ParentPath 'roleManagement' -ResourceName $resourceName -Data $roleAssignmentsExport
                }
            }
        } else {
            return $roleAssignmentsExport
        }
    }
}