Public/Get-GkUserAccessReport.ps1
|
function Get-GkUserAccessReport { <# .SYNOPSIS Report a single user's full access footprint: group memberships, directory roles, licenses, and application role assignments. .DESCRIPTION For each user, gathers: * Identity GET /users/{id} * Groups & roles GET /users/{id}/transitiveMemberOf (classified by type) * App assignments GET /users/{id}/appRoleAssignments * Licenses GET /users/{id}/licenseDetails and returns one object per user with the collections plus counts. Because licenseDetails has no application permission, this cmdlet requires a DELEGATED session; Test-GkConnection blocks app-only sessions. Individual facets that fail (e.g. a denied sub-resource) warn and continue so the rest of the report still returns. A user id that cannot be resolved is skipped with a warning. .PARAMETER UserId One or more user object IDs or userPrincipalNames. Accepts pipeline input (including by the UserPrincipalName/Id property, so output of other cmdlets can be piped in). .PARAMETER AsReport Flatten the Groups/DirectoryRoles/Licenses/AppRoleAssignments collections to '; '-joined strings and add ReportGeneratedUtc for clean export. .EXAMPLE Get-GkUserAccessReport -UserId ada@contoso.com Full access footprint for one user. .EXAMPLE 'ada@contoso.com','bob@contoso.com' | Get-GkUserAccessReport -AsReport | Export-Csv .\access.csv -NoTypeInformation Footprints for several users, flattened for export. .EXAMPLE Get-GkAdminRoleAssignment -AssignmentKind Active | Select-Object -ExpandProperty PrincipalId -Unique | Get-GkUserAccessReport Pipe the principals holding active roles into a full access report. .OUTPUTS PSGraphKit.UserAccessReport #> [CmdletBinding()] [OutputType('PSGraphKit.UserAccessReport')] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('UserPrincipalName', 'Id', 'PrincipalId')] [string[]] $UserId, [switch] $AsReport ) begin { Test-GkConnection -FunctionName 'Get-GkUserAccessReport' | Out-Null $now = [datetime]::UtcNow } process { foreach ($uid in $UserId) { if ([string]::IsNullOrWhiteSpace($uid)) { continue } # Percent-encode the id for the URL path segment. Object IDs (GUIDs) are unaffected, # but guest UPNs contain '#' (e.g. bob_x.com#EXT#@contoso.onmicrosoft.com) which a URL # otherwise treats as a fragment delimiter and truncates the request. $enc = [uri]::EscapeDataString($uid) try { $user = Invoke-GkGraphRequest -Raw ` -Uri "/users/$enc`?`$select=id,displayName,userPrincipalName,accountEnabled,userType" ` -CallerFunction 'Get-GkUserAccessReport' } catch { Write-Warning "Skipping '$uid': could not resolve user. $($_.Exception.Message)" continue } # Facet helper: return the collection, or warn+empty on failure. $facet = { param($relativeUri, $label) try { return @(Invoke-GkGraphRequest -Uri $relativeUri -CallerFunction 'Get-GkUserAccessReport') } catch { Write-Warning "Could not read $label for '$uid': $($_.Exception.Message)" return @() } } # Note: @odata.type must NOT be in $select (Graph rejects it) — it is auto-included for # the derived types in this heterogeneous directoryObject collection, so classification still works. $memberOf = & $facet "/users/$enc/transitiveMemberOf?`$select=id,displayName" 'group/role memberships' $appRoles = & $facet "/users/$enc/appRoleAssignments" 'app role assignments' $licenses = & $facet "/users/$enc/licenseDetails" 'license details' $groups = @(); $roles = @() foreach ($m in $memberOf) { $type = [string](Get-GkDictValue $m '@odata.type') $name = [string](Get-GkDictValue $m 'displayName') if (-not $name) { continue } switch ($type) { '#microsoft.graph.group' { $groups += $name } '#microsoft.graph.directoryRole' { $roles += $name } default { } # administrativeUnit and others are intentionally not listed here } } $apps = @($appRoles | ForEach-Object { [string](Get-GkDictValue $_ 'resourceDisplayName') } | Where-Object { $_ }) $skus = @($licenses | ForEach-Object { [string](Get-GkDictValue $_ 'skuPartNumber') } | Where-Object { $_ }) $obj = [ordered]@{ PSTypeName = 'PSGraphKit.UserAccessReport' UserPrincipalName = [string](Get-GkDictValue $user 'userPrincipalName') DisplayName = [string](Get-GkDictValue $user 'displayName') AccountEnabled = [bool](Get-GkDictValue $user 'accountEnabled') UserType = [string](Get-GkDictValue $user 'userType') Groups = if ($AsReport) { $groups -join '; ' } else { $groups } GroupCount = $groups.Count DirectoryRoles = if ($AsReport) { $roles -join '; ' } else { $roles } RoleCount = $roles.Count Licenses = if ($AsReport) { $skus -join '; ' } else { $skus } LicenseCount = $skus.Count AppRoleAssignments = if ($AsReport) { $apps -join '; ' } else { $apps } AppCount = $apps.Count Id = [string](Get-GkDictValue $user 'id') } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } } } |