Public/Get-PSUAzAccountAccessInSubscriptions.ps1
function Get-PSUAzAccountAccessInSubscriptions { <# .SYNOPSIS Retrieves Azure role assignments for a specified user across filtered subscriptions. .DESCRIPTION This function analyzes Azure role assignments for a user by checking their direct assignments and transitive group memberships across filtered subscriptions. .PARAMETER UserPrincipalName (Mandatory) The User Principal Name (UPN) of the user to analyze. .PARAMETER OutputCsv (Optional) Path where the CSV output file will be saved. Default value is "C:\Temp\account-access-by-subscription.csv". .PARAMETER OutputJson (Optional) Switch parameter to enable JSON output format. .PARAMETER JsonDepth (Optional) Depth level for JSON serialization. Default value is 6. .PARAMETER SubscriptionFilter (Optional) Array of subscription name patterns to filter by. Default value is @('*Non-Prod*'). .EXAMPLE Get-PSUAzAccountAccessInSubscriptions -UserPrincipalName "user@domain.com" .NOTES Author: Lakshmanachari Panuganti Requires: Az.Accounts, Az.Resources modules .LINK https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.AzureCore https://www.linkedin.com/in/lakshmanachari-panuganti/ https://www.powershellgallery.com/packages/OMG.PSUtilities.AzureCore https://learn.microsoft.com/en-us/powershell/module/az.resources/get-azroleassignment #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $UserPrincipalName, [Parameter(Mandatory = $false)] [string] $OutputCsv = "C:\Temp\account-access-by-subscription.csv", [Parameter(Mandatory = $false)] [switch] $OutputJson, [Parameter(Mandatory = $false)] [int] $JsonDepth = 6, [Parameter(Mandatory = $false)] [string[]] $SubscriptionFilter = @('*Non-Prod*') ) # 1) Graph lookups $me = Get-PSUGraphUser -Upn $UserPrincipalName $groups = Get-PSUTransitiveGroups -UserId $me.Id $principalIdsToCheck = @($me.Id) + ($groups | Select-Object -ExpandProperty Id -ErrorAction SilentlyContinue | Where-Object { $_ }) # 2) Subscriptions filter $subs = Get-AzSubscription $patterns = if ($SubscriptionFilter -and $SubscriptionFilter.Count -gt 0) { $SubscriptionFilter } else { @('*') } $filteredSubs = $subs | Where-Object { foreach ($p in $patterns) { if ($null -ne $p -and $p -ne '' -and ($_.Name -like $p -or $_.Id -like $p)) { return $true } } return $false } if (-not $filteredSubs -or $filteredSubs.Count -eq 0) { Write-Warning "No subscriptions matched filter" return @() } # 3) Collect assignments & normalize principal IDs $assignmentsBySub = @{} $unknownPrincipalIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($s in $filteredSubs) { $subScope = "/subscriptions/$($s.Id)" try { $assigns = Get-AzRoleAssignment -Scope $subScope -ErrorAction Stop } catch { Write-Host "Get-AzRoleAssignment failed for $($s.Id): $_" $assigns = @() } foreach ($a in $assigns) { # Try helper first $norm = Get-PSUAssignmentPrincipalId -Assignment $a # Fallback if PrincipalId missing but ObjectId exists if (-not $norm.PrincipalId -and $a.ObjectId) { $norm = [PSCustomObject]@{ PrincipalId = $a.ObjectId PrincipalType = $a.ObjectType AssignmentId = if ($a.Id) { $a.Id } else { $a.RoleAssignmentName } } } # Add normalized props to $a $a | Add-Member -NotePropertyName PSU_PrincipalId -NotePropertyValue $norm.PrincipalId -Force $a | Add-Member -NotePropertyName PSU_PrincipalType -NotePropertyValue $norm.PrincipalType -Force $a | Add-Member -NotePropertyName PSU_AssignmentId -NotePropertyValue $norm.AssignmentId -Force } $matched = $assigns | Where-Object { $_.PSU_PrincipalId -and $principalIdsToCheck -contains $_.PSU_PrincipalId } $assignmentsBySub[$s.Id] = $matched foreach ($a in $matched) { $PSU_PrincipalId = $a.PSU_PrincipalId if ($PSU_PrincipalId -and $PSU_PrincipalId -ne $me.Id -and -not ($groups | Where-Object { $_.Id -eq $PSU_PrincipalId })) { $null = $unknownPrincipalIds.Add($PSU_PrincipalId) } } } # 4) Resolve principals $resolvedCache = @{} $resolvedCache[$me.Id] = @{ DisplayName = $me.DisplayName; Type = 'User' } foreach ($g in $groups) { if ($g.Id) { $resolvedCache[$g.Id] = @{ DisplayName = $g.DisplayName; Type = 'Group' } } } if ($unknownPrincipalIds.Count -gt 0) { $objs = Get-PSUBatchDirectoryObjects -Ids $unknownPrincipalIds foreach ($o in $objs) { $odata = $o.'@odata.type' -or ($o.AdditionalProperties.'@odata.type' -as [string]) $display = $o.displayName -or $o.AdditionalProperties.displayName -or $o.userPrincipalName -or $o.mail $type = if ($odata -like '*group*') { 'Group' } elseif ($odata -like '*user*') { 'User' } else { 'DirectoryObject' } $resolvedCache[$o.id] = @{ DisplayName = $display; Type = $type } } } # 5) Build output $rows = foreach ($s in $filteredSubs) { $matchedAssignments = $assignmentsBySub[$s.Id] | ForEach-Object { $roleDef = Get-AzRoleDefinition -Id $_.RoleDefinitionId -ErrorAction SilentlyContinue [PSCustomObject]@{ PrincipalId = $_.PSU_PrincipalId PrincipalDisplayName = $resolvedCache[$_.PSU_PrincipalId].DisplayName PrincipalType = $resolvedCache[$_.PSU_PrincipalId].Type RoleDefinitionName = $_.RoleDefinitionName RoleDefinitionId = $_.RoleDefinitionId RoleDefinition = $roleDef Scope = $_.Scope ScopeLevel = if ($_.Scope -match '^/subscriptions/[^/]+$') { 'Subscription' } elseif ($_.Scope -match '^/subscriptions/[^/]+/resourceGroups/[^/]+$') { 'ResourceGroup' } else { 'Resource' } ResourceGroup = if ($_.Scope -match '/resourceGroups/([^/]+)') { $matches[1] } else { '' } ResourceType = if ($_.Scope -match 'providers/([^/]+/[^/]+)$') { $matches[1] } else { '' } ResourceName = if ($_.Scope -match 'providers/[^/]+/[^/]+/(.+)$') { $matches[1] } else { '' } AssignmentObjectId = if ($_.PSU_AssignmentId) { $_.PSU_AssignmentId } elseif ($_.RoleAssignmentId) { $_.RoleAssignmentId } elseif ($_.Id) { $_.Id } else { $null } Condition = $_.Condition Description = $_.Description } } [PSCustomObject]@{ SubscriptionId = $s.Id SubscriptionName = $s.Name MatchedAssignmentsCount = $matchedAssignments.Count MatchedAssignmentsJson = ($matchedAssignments | ConvertTo-Json -Depth $JsonDepth -Compress) } } # 6) Export try { $rows | Select-Object SubscriptionId, SubscriptionName, MatchedAssignmentsCount, MatchedAssignmentsJson | Export-Csv -Path $OutputCsv -NoTypeInformation -Force -Encoding UTF8 } catch { Write-Warning "Export CSV failed: $_" } if ($OutputJson) { $full = @{ GeneratedAt = (Get-Date).ToString("o"); User = $UserPrincipalName; Results = $rows } $jsonPath = [System.IO.Path]::ChangeExtension($OutputCsv, '.full.json') try { $full | ConvertTo-Json -Depth ($JsonDepth + 2) | Out-File -FilePath $jsonPath -Encoding UTF8 Write-Output "JSON: $(Resolve-Path $jsonPath)" } catch { Write-Warning "Failed to write JSON: $_" } } return $rows } |