Private/Show-AACGroupMembershipView.ps1
|
function Show-AACGroupMembershipView { <# .SYNOPSIS Renders Get-AACEntraGroupMembership's result as a Spectre.Console view: the scope, tiles, a table of the groups, then each group in turn - its details and owners, its direct members, and each nested group's members under its path. .DESCRIPTION [ 4 groups ] [ 57 users ] [ 6 guests ] [ 2 disabled ] [ 3 nested groups ] Groups: name, type, source, membership type, created, owners, direct members, nested groups, users, guests, disabled -- grp-finance --------------------------------------------- Type Security Created 2024-01-15 09:30 UTC Source Cloud Owners Ada Lovelace (ada@contoso.com); app-payroll (Service principal) Membership Assigned Members 3 direct · 1 nested group · 4 in all (1 guest, 1 disabled) DIRECT MEMBERS (3) Ada Lovelace User ada@contoso.com Gus Guest User gus@fabrikam.com GUEST grp-finance-emea Nested group its members are listed below NESTED GROUP grp-finance-emea (3 members) Grace Hopper User grace@contoso.com DISABLED ... Everything is listed - wrap the call in Invoke-AACPagedOutput to page it. Guests are amber, disabled accounts red. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable] $Membership, [System.Collections.IDictionary] $Scope ) $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $glyph = Get-AACGlyph $stats = $Membership.Stats $facts = [System.Collections.Generic.List[string]]::new() if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } } $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm')) Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]" [Spectre.Console.AnsiConsole]::WriteLine() Show-AACTileRow -Tile @( @{ Value = '{0:N0}' -f $stats.Groups; Caption = 'groups'; Color = 'mediumpurple2' } @{ Value = '{0:N0}' -f $stats.Users; Caption = 'unique users'; Color = 'deepskyblue1' } @{ Value = '{0:N0}' -f $stats.Guests; Caption = 'guests'; Color = $(if ($stats.Guests) { 'orange1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.Disabled; Caption = 'disabled accounts'; Color = $(if ($stats.Disabled) { 'red1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.NestedGroups; Caption = 'nested groups'; Color = 'gold1' } @{ Value = '{0:N0}' -f $stats.EmptyGroups; Caption = 'empty groups'; Color = $(if ($stats.EmptyGroups) { 'orange1' } else { 'grey50' }) } ) [Spectre.Console.AnsiConsole]::WriteLine() # --- The groups ---------------------------------------------------------------------------------- $table = [Spectre.Console.Table]::new() $table.Border = [Spectre.Console.TableBorder]::Rounded $table.BorderStyle = [Spectre.Console.Style]::Parse('mediumpurple2') $table.Expand = $true $table.Title = [Spectre.Console.TableTitle]::new("[bold mediumpurple2]$($glyph.Bullet) Groups[/]") foreach ($header in 'Group', 'Type', 'Source', 'Membership', 'Created', 'Owners', 'Direct', 'Nested groups', 'Users', 'Guests', 'Disabled') { $column = [Spectre.Console.TableColumn]::new("[grey62]$header[/]") if ($header -in 'Owners', 'Direct', 'Nested groups', 'Users', 'Guests', 'Disabled') { $column.Alignment = [Spectre.Console.Justify]::Right; $column.NoWrap = $true } $table.AddColumn($column) | Out-Null } $number = { param([int] $Value, [string] $Color) if ($Value -eq 0) { '[grey42]0[/]' } elseif ($Color) { "[$Color]$('{0:N0}' -f $Value)[/]" } else { '{0:N0}' -f $Value } } foreach ($group in $Membership.Groups) { $name = "[bold]$(& $escape $group.GroupName)[/]" if ($group.Error) { $name += "`n[orange1]$(& $escape $group.Error)[/]" } elseif ($group.MembershipRule) { $name += "`n[grey50]rule: $(& $escape $group.MembershipRule)[/]" } $cells = @( $name "[grey70]$(& $escape $group.GroupType)[/]" "[grey70]$(& $escape $group.GroupSource)[/]" $(if ($group.MembershipType -like 'Dynamic*') { "[gold1]$(& $escape $group.MembershipType)[/]" } else { "[grey70]$(& $escape $group.MembershipType)[/]" }) "[grey70]$(if ($group.Created) { $group.Created.ToString('yyyy-MM-dd') })[/]" $(if ($group.OwnerCount) { & $number $group.OwnerCount } else { '[orange1]0[/]' }) (& $number $group.DirectMembers) (& $number $group.NestedGroups 'gold1') (& $number $group.Users 'deepskyblue1') (& $number $group.Guests 'orange1') (& $number $group.DisabledUsers 'red1') ) [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]@($cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() # --- Each group: its details, its direct members, then each nested group's members ----------------- $memberTable = { param($Section) $list = [Spectre.Console.Table]::new() $list.Border = [Spectre.Console.TableBorder]::Simple $list.BorderStyle = [Spectre.Console.Style]::Parse('grey35') foreach ($heading in 'Member', 'Type', 'Account', '') { $column = [Spectre.Console.TableColumn]::new("[grey62]$heading[/]") $column.Padding = [Spectre.Console.Padding]::new(0, 0, 3, 0) $null = $list.AddColumn($column) } # People first, by name; the nested groups after them. foreach ($r in @($Section.Members | Sort-Object -Property @{ Expression = { $_.MemberType -eq 'Group' } }, MemberName)) { $name = if ($r.MemberName) { $r.MemberName } elseif ($r.UserPrincipalName) { $r.UserPrincipalName } else { $r.MemberId } $cells = if ($r.MemberType -eq 'Group') { @( "[bold gold1]$(& $escape $name)[/]" '[gold1]Nested group[/]' $(if ($Section.Loops.Contains($r.MemberId)) { '[grey50]already listed above (a loop)[/]' } else { '[grey50]its members are listed below[/]' }) '' ) } else { $flags = @( if ($r.UserType -eq 'Guest') { '[black on orange1] GUEST [/]' } if ($r.AccountEnabled -eq $false) { '[white on red3] DISABLED [/]' } ) @( "[white]$(& $escape $name)[/]" "[grey70]$(& $escape $r.MemberType)[/]" "[grey50]$(& $escape $(if ($r.UserPrincipalName) { $r.UserPrincipalName } else { $r.Mail }))[/]" ($flags -join ' ') ) } [Spectre.Console.TableExtensions]::AddRow($list, [Spectre.Console.Rendering.IRenderable[]]@($cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null } [Spectre.Console.AnsiConsole]::Write([Spectre.Console.Padder]::new($list, [Spectre.Console.Padding]::new(2, 0, 0, 0))) } $label = { param($Text) [Spectre.Console.Markup]::new("[grey62]$Text[/]") } $cell = { param($Text) [Spectre.Console.Markup]::new([string]$Text) } foreach ($group in $Membership.Groups) { $people = @($Membership.People | Where-Object GroupId -EQ $group.GroupId) $rule = [Spectre.Console.Rule]::new("[bold mediumpurple2]$(& $escape $group.GroupName)[/]") $rule.Justification = [Spectre.Console.Justify]::Left $rule.Style = [Spectre.Console.Style]::Parse('mediumpurple2') [Spectre.Console.AnsiConsole]::Write($rule) # The details: two columns of label and value. $guests = @($people | Where-Object UserType -EQ 'Guest').Count $disabled = @($people | Where-Object { $_.AccountEnabled -eq $false }).Count $flagged = @( if ($guests) { "[orange1]$guests guest$(if ($guests -ne 1) { 's' })[/]" } if ($disabled) { "[red1]$disabled disabled[/]" } ) $inAll = "$($group.DirectMembers) direct $($glyph.Dot) $($group.NestedGroups) nested group$(if ($group.NestedGroups -ne 1) { 's' }) $($glyph.Dot) [bold]$($people.Count)[/] in all$(if ($flagged.Count) { " ($($flagged -join ', '))" })" $created = if ($group.Created) { $group.Created.ToString('yyyy-MM-dd HH:mm') + ' UTC' } else { '[grey50]unknown[/]' } $owners = if ($group.OwnerCount) { & $escape $group.Owners } else { "[orange1]$(& $escape $group.Owners)[/]" } $kind = if ($group.MembershipType -like 'Dynamic*') { "[gold1]$(& $escape $group.MembershipType)[/]" } else { & $escape $group.MembershipType } $grid = [Spectre.Console.Grid]::new() foreach ($i in 1..4) { $null = $grid.AddColumn() } $null = $grid.AddRow([Spectre.Console.Rendering.IRenderable[]]@((& $label 'Type'), (& $cell (& $escape $group.GroupType)), (& $label 'Created'), (& $cell $created))) $null = $grid.AddRow([Spectre.Console.Rendering.IRenderable[]]@((& $label 'Source'), (& $cell (& $escape $group.GroupSource)), (& $label 'Owners'), (& $cell $owners))) $null = $grid.AddRow([Spectre.Console.Rendering.IRenderable[]]@((& $label 'Membership'), (& $cell $kind), (& $label 'Members'), (& $cell $inAll))) if ($group.MembershipRule) { $null = $grid.AddRow([Spectre.Console.Rendering.IRenderable[]]@((& $label 'Rule'), (& $cell "[grey70]$(& $escape $group.MembershipRule)[/]"), (& $cell ''), (& $cell ''))) } [Spectre.Console.AnsiConsole]::Write([Spectre.Console.Padder]::new($grid, [Spectre.Console.Padding]::new(2, 0, 0, 0))) [Spectre.Console.AnsiConsole]::WriteLine() foreach ($section in @($Membership.Sections[$group.GroupId])) { $count = $section.Members.Count $title = if ($section.Depth -eq 0) { "[bold deepskyblue1]DIRECT MEMBERS[/] [grey58]($count)[/]" } else { "[bold gold1]NESTED GROUP[/] [bold]$(& $escape $section.Path)[/] [grey58]($count member$(if ($count -ne 1) { 's' }))[/]" } Write-AACMarkup " $title" if ($section.Error) { Write-AACMarkup " [orange1]members could not be read: $(& $escape $section.Error)[/]" } elseif (-not $count) { Write-AACMarkup ' [orange1]no members[/]' } else { & $memberTable $section } [Spectre.Console.AnsiConsole]::WriteLine() } } if ($stats.Unreadable) { Write-AACMarkup "[orange1]![/] [grey58]The members of $($stats.Unreadable) group(s) couldn't be read - the reason is under each group.[/]" } Write-AACMarkup "[grey42]Add -PassThru (or pipe the command) for the rows - one per group and member; -CsvPath, -PdfPath or -HtmlPath for a report.[/]" } |