EnterpriseInfo.ps1
|
function Script:Get-EnterpriseNodeAndDescendantIds { param([object]$enterpriseData, [long]$rootId) if ($rootId -le 0) { return $null } $subnodes = @{} foreach ($n in $enterpriseData.Nodes) { $id = $n.Id if ($n.ParentNodeId -gt 0) { if (-not $subnodes[$n.ParentNodeId]) { $subnodes[$n.ParentNodeId] = [System.Collections.Generic.List[long]]::new() } $subnodes[$n.ParentNodeId].Add($id) | Out-Null } } $set = [System.Collections.Generic.HashSet[long]]::new() $queue = [System.Collections.Generic.Queue[long]]::new() $queue.Enqueue($rootId) | Out-Null while ($queue.Count -gt 0) { $nid = $queue.Dequeue() [void]$set.Add($nid) if ($subnodes[$nid]) { foreach ($c in $subnodes[$nid]) { $queue.Enqueue($c) | Out-Null } } } return $set } function Get-KeeperEnterpriseInfoTree { <# .SYNOPSIS Display a tree structure of the enterprise (nodes with users, roles, teams). .DESCRIPTION Outputs a tree view of the enterprise hierarchy (nodes with users, roles, and teams). Output format is always tree. .PARAMETER Node Limit output to this node and its descendants (node name or ID). .PARAMETER Detailed Include node IDs and list individual users/roles/teams by name. .PARAMETER Output If supplied, write output to this file path. .EXAMPLE Get-KeeperEnterpriseInfoTree Get-KeeperEnterpriseInfoTree -Node "Sales" -Detailed -Output tree.txt #> [CmdletBinding()] Param ( [Parameter()][string] $Node, [Parameter()][switch] $Detailed, [Parameter()][string] $Output ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $subnodes = @{} foreach ($n in $ed.Nodes) { $id = $n.Id if (-not $subnodes.ContainsKey($id)) { $subnodes[$id] = [System.Collections.Generic.List[long]]::new() } if ($n.ParentNodeId -gt 0) { if (-not $subnodes.ContainsKey($n.ParentNodeId)) { $subnodes[$n.ParentNodeId] = [System.Collections.Generic.List[long]]::new() } $subnodes[$n.ParentNodeId].Add($id) | Out-Null } } $rootId = $ed.RootNode.Id if ($Node) { $resolved = resolveSingleNode $Node if (-not $resolved) { Write-Error "Node '$Node' not found"; return } $rootId = $resolved.Id } $usersByNode = @{} foreach ($u in $ed.Users) { $nid = $u.ParentNodeId if (-not $usersByNode.ContainsKey($nid)) { $usersByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $usersByNode[$nid].Add($u) | Out-Null } $rolesByNode = @{} foreach ($r in $rd.Roles) { $nid = $r.ParentNodeId if (-not $rolesByNode.ContainsKey($nid)) { $rolesByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $rolesByNode[$nid].Add($r) | Out-Null } $teamsByNode = @{} foreach ($t in $ed.Teams) { $nid = $t.ParentNodeId if (-not $teamsByNode.ContainsKey($nid)) { $teamsByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $teamsByNode[$nid].Add($t) | Out-Null } $lines = [System.Collections.Generic.List[string]]::new() function writeTreeNode { param([long]$nodeId, [string]$prefix, [bool]$isLastSibling = $true) $n = $null if (-not $ed.TryGetNode($nodeId, [ref]$n)) { return } $name = $n.DisplayName if ([string]::IsNullOrEmpty($name)) { $name = $enterprise.loader.EnterpriseName } if ($Detailed) { $name += " ($nodeId)" } if ($n.RestrictVisibility) { $name += " |Isolated|" } if ($prefix -eq '') { $lines.Add($name) | Out-Null } else { $lines.Add("$prefix+-- $name") | Out-Null } $us = $usersByNode[$nodeId]; $ro = $rolesByNode[$nodeId]; $te = $teamsByNode[$nodeId] $childIds = if ($subnodes[$nodeId]) { @($subnodes[$nodeId]) } else { @() } $sortedChildIds = if ($childIds.Count -gt 0) { @($childIds | Sort-Object { $nn = $null; if ($ed.TryGetNode($_, [ref]$nn)) { $nn.DisplayName } else { '' } }) } else { @() } $contentItems = [System.Collections.Generic.List[object]]::new() foreach ($cid in $sortedChildIds) { $contentItems.Add([PSCustomObject]@{ NodeId = $cid }) | Out-Null } if ($us -and $us.Count -gt 0) { if ($Detailed) { foreach ($u in ($us | Sort-Object { $_.Email })) { $contentItems.Add($($u.Email) + " ($($u.Id))") | Out-Null } } else { $contentItems.Add("$($us.Count) user(s)") | Out-Null } } if ($ro -and $ro.Count -gt 0) { if ($Detailed) { $i = 0; foreach ($r in ($ro | Sort-Object { $_.DisplayName })) { if ($i -ge 50) { $contentItems.Add("$($ro.Count - 50) more role(s)"); break } $contentItems.Add("$($r.DisplayName) ($($r.Id))") | Out-Null; $i++ } } else { $contentItems.Add("$($ro.Count) role(s)") | Out-Null } } if ($te -and $te.Count -gt 0) { if ($Detailed) { $i = 0; foreach ($t in ($te | Sort-Object { $_.Name })) { if ($i -ge 50) { $contentItems.Add("$($te.Count - 50) more team(s)"); break } $contentItems.Add("$($t.Name) ($($t.Uid))") | Out-Null; $i++ } } else { $contentItems.Add("$($te.Count) team(s)") | Out-Null } } $total = $contentItems.Count for ($i = 0; $i -lt $total; $i++) { $isLast = ($i -eq $total - 1) $branch = if ($isLastSibling -and $isLast) { ' ' } else { ' | ' } $connector = if ($prefix -eq '') { ' ' } else { $prefix + $branch } $item = $contentItems[$i] if ($item -is [string]) { $lines.Add("$connector+-- $item") | Out-Null } else { writeTreeNode -nodeId $item.NodeId -prefix $connector -isLastSibling $isLast } } } writeTreeNode -nodeId $rootId -prefix "" -isLastSibling $true $out = $lines -join "`n" if ($Output) { Set-Content -Path $Output -Value $out -Encoding utf8 } else { $out } } function Get-KeeperEnterpriseInfoNode { <# .SYNOPSIS Display node information as a table. .DESCRIPTION Outputs nodes with parent path, user/team/role counts, and optionally user/team/role lists and provisioning. .PARAMETER Pattern Optional search pattern to filter nodes. .PARAMETER Columns Comma-separated columns: parent_node, user_count, users, team_count, teams, role_count, roles, provisioning. Default: parent_node, user_count, team_count, role_count. .PARAMETER Node Filter by node name or ID: only nodes that are this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoNode Get-KeeperEnterpriseInfoNode -Columns "parent_node,user_count,users" -Pattern "Sales" -Node "Sales" -Format json -Output nodes.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $teamCount = @{}; $roleCount = @{} $userList = @{}; $teamList = @{}; $roleList = @{} foreach ($u in $ed.Users) { $userCount[$u.ParentNodeId] = ((if ($null -ne $userCount[$u.ParentNodeId]) { $userCount[$u.ParentNodeId] } else { 0 }) + 1) if (-not $userList[$u.ParentNodeId]) { $userList[$u.ParentNodeId] = [System.Collections.Generic.List[string]]::new() } $userList[$u.ParentNodeId].Add($u.Email) | Out-Null } foreach ($t in $ed.Teams) { $nid = if ($t.ParentNodeId -eq 0) { $ed.RootNode.Id } else { $t.ParentNodeId } $teamCount[$nid] = ((if ($null -ne $teamCount[$nid]) { $teamCount[$nid] } else { 0 }) + 1) if (-not $teamList[$nid]) { $teamList[$nid] = [System.Collections.Generic.List[string]]::new() } $teamList[$nid].Add($t.Name) | Out-Null } foreach ($r in $rd.Roles) { $roleCount[$r.ParentNodeId] = ((if ($null -ne $roleCount[$r.ParentNodeId]) { $roleCount[$r.ParentNodeId] } else { 0 }) + 1) if (-not $roleList[$r.ParentNodeId]) { $roleList[$r.ParentNodeId] = [System.Collections.Generic.List[string]]::new() } $roleList[$r.ParentNodeId].Add($r.DisplayName) | Out-Null } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if (-not $resolved) { Write-Error "Node '$Node' not found"; return } $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } $colSet = @('parent_node', 'user_count', 'team_count', 'role_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(parent_node|user_count|users|team_count|teams|role_count|roles|provisioning)$' }) if ($colSet.Count -eq 0) { $colSet = @('parent_node', 'user_count', 'team_count', 'role_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($n in ($ed.Nodes | Sort-Object { $_.DisplayName })) { if ($nodeFilterIds -and -not $nodeFilterIds.Contains($n.Id)) { continue } $row = [ordered]@{ NodeId = $n.Id; Name = $n.DisplayName } foreach ($c in $colSet) { switch ($c) { 'parent_node' { $row['ParentNode'] = if ($n.ParentNodeId -le 0) { '' } else { Get-KeeperNodePath -NodeId $n.ParentNodeId } } 'user_count' { $row['UserCount'] = (if ($null -ne $userCount[$n.Id]) { $userCount[$n.Id] } else { 0 }) } 'users' { $row['Users'] = ($userList[$n.Id] | Sort-Object) -join ', ' } 'team_count' { $row['TeamCount'] = (if ($null -ne $teamCount[$n.Id]) { $teamCount[$n.Id] } else { 0 }) } 'teams' { $row['Teams'] = ($teamList[$n.Id] | Sort-Object) -join ', ' } 'role_count' { $row['RoleCount'] = (if ($null -ne $roleCount[$n.Id]) { $roleCount[$n.Id] } else { 0 }) } 'roles' { $row['Roles'] = ($roleList[$n.Id] | Sort-Object) -join ', ' } 'provisioning' { $parts = @(); if ($n.BridgeId -gt 0) { $parts += 'Bridge' }; if ($n.ScimId -gt 0) { $parts += 'SCIM' }; if ($n.SsoServiceProviderIds -and $n.SsoServiceProviderIds.Length -gt 0) { $parts += 'SSO' }; $row['Provisioning'] = ($parts -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Format -eq 'table') { $disp = $result | Format-Table -AutoSize } else { $disp = $result } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $disp } else { $disp } } } function Get-KeeperEnterpriseInfoUser { <# .SYNOPSIS Display user information as a table. .DESCRIPTION Outputs users with status, node, roles, teams, and optional columns. .PARAMETER Pattern Optional search pattern to filter users. .PARAMETER Columns Comma-separated columns: name, status, transfer_status, node, role_count, roles, team_count, teams, queued_team_count, queued_teams, alias, 2fa_enabled. Default: name, status, transfer_status, node. .PARAMETER Node Filter by node name or ID: only users in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoUser Get-KeeperEnterpriseInfoUser -Columns "name,status,node,roles" -Pattern "admin" -Node "Sales" -Format json -Output users.json -Offset 0 -Limit 100 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $roleUsers = @{} foreach ($r in $rd.Roles) { foreach ($uid in @($rd.GetUsersForRole($r.Id))) { if (-not $roleUsers[$uid]) { $roleUsers[$uid] = [System.Collections.Generic.List[long]]::new() } $roleUsers[$uid].Add($r.Id) | Out-Null } } $teamUsers = @{} foreach ($t in $ed.Teams) { foreach ($uid in @($ed.GetUsersForTeam($t.Uid))) { if (-not $teamUsers[$uid]) { $teamUsers[$uid] = [System.Collections.Generic.List[string]]::new() } $teamUsers[$uid].Add($t.Name) | Out-Null } } $colSet = @('name', 'status', 'transfer_status', 'node') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(name|status|transfer_status|node|role_count|roles|team_count|teams|queued_team_count|queued_teams|alias|2fa_enabled)$' }) if ($colSet.Count -eq 0) { $colSet = @('name', 'status', 'transfer_status', 'node') } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } $statusText = { param($s) switch ($s) { 'Active' { 'Active' } 'Inactive' { 'Invited' } 'Locked' { 'Locked' } 'Blocked' { 'Blocked' } 'Disabled' { 'Disabled' } default { $s } } } $transferText = { param($s) switch ([int]$s) { 0 { 'Undefined' } 1 { 'Not required' } 2 { 'Pending transfer' } 3 { 'Partially accepted' } 4 { 'Transfer accepted' } default { $s } } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($u in ($ed.Users | Sort-Object { $_.Email })) { $nid = if ($u.ParentNodeId -le 0) { $ed.RootNode.Id } else { $u.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $row = [ordered]@{ UserId = $u.Id; Email = $u.Email } foreach ($c in $colSet) { switch ($c) { 'name' { $row['Name'] = $u.DisplayName } 'status' { $row['Status'] = & $statusText $u.UserStatus } 'transfer_status' { $row['TransferStatus'] = & $transferText $u.TransferAcceptanceStatus } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $u.ParentNodeId -OmitRoot } 'role_count' { $row['RoleCount'] = (if ($null -ne $roleUsers[$u.Id]) { $roleUsers[$u.Id].Count } else { 0 }) } 'roles' { $rnames = @($roleUsers[$u.Id] | ForEach-Object { $rr = $null; if ($rd.TryGetRole($_, [ref]$rr)) { $rr.DisplayName } } | Sort-Object); $row['Roles'] = ($rnames -join ', ') } 'team_count' { $row['TeamCount'] = (if ($null -ne $teamUsers[$u.Id]) { $teamUsers[$u.Id].Count } else { 0 }) } 'teams' { $row['Teams'] = (($teamUsers[$u.Id] | Sort-Object) -join ', ') } 'queued_team_count' { $row['QueuedTeamCount'] = 0 } 'queued_teams' { $row['QueuedTeams'] = '' } 'alias' { $row['Alias'] = '' } '2fa_enabled' { $row['2FAEnabled'] = $u.TwoFactorEnabled } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Email }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoTeam { <# .SYNOPSIS Display team information as a table. .DESCRIPTION Outputs teams with restricts (Read/Write/Share), node, user/role counts, and optional user/role lists. .PARAMETER Pattern Optional search pattern to filter teams. .PARAMETER Columns Comma-separated columns: restricts, node, user_count, users, queued_user_count, queued_users, role_count, roles. Default: restricts, node, user_count. .PARAMETER Node Filter by node name or ID: only teams in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoTeam Get-KeeperEnterpriseInfoTeam -Columns "restricts,node,user_count,users" -Pattern "Eng" -Node "Engineering" -Format json -Output teams.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $roleCount = @{} $userList = @{}; $roleList = @{} foreach ($t in $ed.Teams) { $uids = @($ed.GetUsersForTeam($t.Uid)) $userCount[$t.Uid] = $uids.Count $userList[$t.Uid] = @($uids | ForEach-Object { $uu = $null; if ($ed.TryGetUserById($_, [ref]$uu)) { $uu.Email } } | Sort-Object) } foreach ($t in $ed.Teams) { foreach ($rid in @($rd.GetRolesForTeam($t.Uid))) { if (-not $roleList[$t.Uid]) { $roleList[$t.Uid] = [System.Collections.Generic.List[string]]::new() } $rr = $null; if ($rd.TryGetRole($rid, [ref]$rr)) { $roleList[$t.Uid].Add($rr.DisplayName) | Out-Null } } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $colSet = @('restricts', 'node', 'user_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(restricts|node|user_count|users|queued_user_count|queued_users|role_count|roles)$' }) if ($colSet.Count -eq 0) { $colSet = @('restricts', 'node', 'user_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($t in ($ed.Teams | Sort-Object { $_.Name })) { $nid = if ($t.ParentNodeId -eq 0) { $ed.RootNode.Id } else { $t.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $restrictParts = @() if ($t.RestrictView) { $restrictParts += 'Read' } if ($t.RestrictEdit) { $restrictParts += 'Write' } if ($t.RestrictSharing) { $restrictParts += 'Share' } $restricts = $restrictParts -join ', ' $row = [ordered]@{ TeamUid = $t.Uid; Name = $t.Name } foreach ($c in $colSet) { switch ($c) { 'restricts' { $row['Restricts'] = $restricts } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $t.ParentNodeId -OmitRoot } 'user_count' { $row['UserCount'] = (if ($null -ne $userCount[$t.Uid]) { $userCount[$t.Uid] } else { 0 }) } 'users' { $row['Users'] = ($userList[$t.Uid] -join ', ') } 'queued_user_count'{ $row['QueuedUserCount'] = 0 } 'queued_users' { $row['QueuedUsers'] = '' } 'role_count' { $row['RoleCount'] = (if ($null -ne $roleList[$t.Uid]) { $roleList[$t.Uid].Count } else { 0 }) } 'roles' { $row['Roles'] = (($roleList[$t.Uid] | Sort-Object) -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoRole { <# .SYNOPSIS Display role information as a table. .DESCRIPTION Outputs roles with node, user/team counts, admin flag, and optional user/team lists. .PARAMETER Pattern Optional search pattern to filter roles. .PARAMETER Columns Comma-separated columns: visible_below, default_role, admin, node, user_count, users, team_count, teams. Default: default_role, admin, node, user_count. .PARAMETER Node Filter by node name or ID: only roles in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoRole Get-KeeperEnterpriseInfoRole -Columns "visible_below,node,user_count,users" -Pattern "Admin" -Node "Sales" -Format json -Output roles.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $teamCount = @{} $userList = @{}; $teamList = @{} foreach ($r in $rd.Roles) { $uids = @($rd.GetUsersForRole($r.Id)) $userCount[$r.Id] = $uids.Count $userList[$r.Id] = @($uids | ForEach-Object { $uu = $null; if ($ed.TryGetUserById($_, [ref]$uu)) { $uu.Email } } | Sort-Object) } foreach ($r in $rd.Roles) { foreach ($tuid in @($rd.GetTeamsForRole($r.Id))) { if (-not $teamList[$r.Id]) { $teamList[$r.Id] = [System.Collections.Generic.List[string]]::new() } $tt = $null; if ($ed.TryGetTeam($tuid, [ref]$tt)) { $teamList[$r.Id].Add($tt.Name) | Out-Null } } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $managedNodes = @($rd.GetManagedNodes()) $adminRoleIds = [System.Collections.Generic.HashSet[long]]::new() foreach ($mn in $managedNodes) { [void]$adminRoleIds.Add($mn.RoleId) } $colSet = @('visible_below', 'default_role', 'admin', 'node', 'user_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(visible_below|default_role|admin|node|user_count|users|team_count|teams)$' }) if ($colSet.Count -eq 0) { $colSet = @('default_role', 'admin', 'node', 'user_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($r in ($rd.Roles | Sort-Object { $_.DisplayName })) { $nid = if ($r.ParentNodeId -le 0) { $ed.RootNode.Id } else { $r.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $row = [ordered]@{ RoleId = $r.Id; Name = $r.DisplayName } foreach ($c in $colSet) { switch ($c) { 'visible_below' { $row['VisibleBelow'] = $r.VisibleBelow } 'default_role' { $row['DefaultRole'] = $r.NewUserInherit } 'admin' { $row['Admin'] = $adminRoleIds.Contains($r.Id) } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $r.ParentNodeId -OmitRoot } 'user_count' { $row['UserCount'] = (if ($null -ne $userCount[$r.Id]) { $userCount[$r.Id] } else { 0 }) } 'users' { $row['Users'] = ($userList[$r.Id] -join ', ') } 'team_count' { $row['TeamCount'] = (if ($null -ne $teamList[$r.Id]) { $teamList[$r.Id].Count } else { 0 }) } 'teams' { $row['Teams'] = (($teamList[$r.Id] | Sort-Object) -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoManagedCompany { <# .SYNOPSIS Display managed company information (MSP only). .DESCRIPTION Outputs managed company information. Available when logged in as MSP. .PARAMETER Pattern Optional search pattern to filter companies. .PARAMETER Node Filter by node name or ID: only managed companies in this node or its descendants. .PARAMETER ExactNode If set, -Node filters to that node only (exclude descendants). .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoManagedCompany Get-KeeperEnterpriseInfoManagedCompany -Format json -Output mcs.json -Offset 0 -Limit 20 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getMspEnterprise $ed = $enterprise.enterpriseData $mcs = $enterprise.mspData.ManagedCompanies if (-not $mcs) { return @() } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $planName = { param($planId) switch ($planId) { 'enterprise' { 'Enterprise' } 'enterprise_plus' { 'Enterprise Plus' } 'business' { 'Business' } 'businessPlus' { 'Business Plus' } default { $planId } } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($mc in ($mcs | Sort-Object { $_.EnterpriseName })) { $nid = if ($mc.ParentNodeId -le 0) { $ed.RootNode.Id } else { $mc.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $storage = if ($mc.FilePlanType) { $mc.FilePlanType } else { '' } $addons = if ($mc.AddOns) { $mc.AddOns.Count } else { 0 } $allocated = $mc.NumberOfSeats; if ($allocated -eq 2147483647) { $allocated = $null } $nodePath = Get-KeeperNodePath -NodeId $mc.ParentNodeId -OmitRoot $row = [PSCustomObject]@{ CompanyId = $mc.EnterpriseId CompanyName = $mc.EnterpriseName Node = $nodePath Plan = & $planName $mc.ProductId Storage = $storage Addons = $addons Allocated = $allocated Active = $mc.NumberOfUsers } if ($patternLower) { $text = ($row.PSObject.Properties.Value | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add($row) | Out-Null } $result = @($out | Sort-Object { $_.CompanyName }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } New-Alias -Name keitree -Value Get-KeeperEnterpriseInfoTree -ErrorAction SilentlyContinue New-Alias -Name kein -Value Get-KeeperEnterpriseInfoNode -ErrorAction SilentlyContinue New-Alias -Name keiu -Value Get-KeeperEnterpriseInfoUser -ErrorAction SilentlyContinue New-Alias -Name keit -Value Get-KeeperEnterpriseInfoTeam -ErrorAction SilentlyContinue New-Alias -Name keir -Value Get-KeeperEnterpriseInfoRole -ErrorAction SilentlyContinue New-Alias -Name keimc -Value Get-KeeperEnterpriseInfoManagedCompany -ErrorAction SilentlyContinue # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAiTzaDFjf0M6uS # IB/zoydx214l/sv6xNep6LMdKEAkr6CCITswggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg # MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit # eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS # 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM # swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC # Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3 # /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j # q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5 # OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo # 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU # tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm # KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP # TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq # hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK # r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda # qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+ # lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a # brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS # y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK # iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb # KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q # xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm # zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn # HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w # gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1 # c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo # dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi # 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg # xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF # cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ # m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS # GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1 # ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9 # MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7 # Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG # RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6 # X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd # BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx # XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF # BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln # aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy # bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL # BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj # aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0 # hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0 # F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT # mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf # ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE # wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh # OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX # gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO # LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG # WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg # AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy # NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI # hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3 # zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch # TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj # FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo # yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP # KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS # uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w # JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW # doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg # rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K # 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf # gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy # Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL # TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG # AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy # dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j # cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB # CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ # D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ # ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu # +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o # bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h # ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn # M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol # /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY # xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc # CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB # ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx # oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a # vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ # aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc # vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC # h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt # 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk # Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s # FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm # 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG # 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6 # z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2 # IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL # iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu # FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc # H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA # 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB # +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b # WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A # g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH # 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCDDeFMO5PPq4xzy2zZihdL/1aJ3lGMeViEhdyG4EqHwvzANBgkqhkiG9w0B # AQEFAASCAYBsQk2l1Tym7p7SmhoShST9qb2FnSJr5DTlGyclPXaP137ecVI0eHY9 # o0+vg4VF7VECIZdX+hYy1+hS4RIUPTXWLutjnHw2+ucQj/X0UxgFYEDR839Tghgl # bBLd8p+0h7yWcGCWlfZl2W5Pn9pXgMyCoi5a0F/vylkaXJr/1tUhbKyv/S4pu7nd # Sl3fagrW7remoIjQPiYUScdYPpCX5TaVJ1ep10ff/U4wLnTgc9c1rpXVWu202gVQ # WKjsHnNk0Z9CQYXcCOm1g8fsiHPDQuYKgkghykiN59OAMgeFkwACw/HdH/y1LugS # xyhlhAkutfh01r8lCQd8eMqOF+rNU+OegMR0U+ZQ2lOoilNbRu+NZVldHCz6s15k # 9V5noRViHxXTsX+j0CHh1tpbEfhXzDcD87fngEq5PY3B191FoE/ntOrCY4LDmHVZ # aVa5E5Jta3HL7csliHoTiVM8ebCp5KBDkhG+GqEDmXNvDdCXL/eH1PvAMLmocxmT # W5lcCaPkUC6hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwMjI1MjIzNzUyWjAv # BgkqhkiG9w0BCQQxIgQgRWMf0wYSJzQlKe7TX9Wjbtbu1KwG1xBaO2TH5G0JH0Ew # DQYJKoZIhvcNAQEBBQAEggIAxe+wG6ofF64fXP4PrJoM+WgYhihn8FFrHQfcvmy7 # pcpZ4kfKjm7A4T5d11WQJWYEW7IZAPQAKHAztL6Btaw4YTgYlba4BEawiqEp6fWZ # prfH1s7uF3FfJ90kRcm7uFgt85+CJezcEZ7tLdicmWWwuuU4J8Q+8xYZwRAjvUT/ # 7ENCgzPpQlHPJXKe9ZPJFvOFMq5mdBR9y5UDBrPKsLSbXoLRoURbuWOWdBTT1ENY # OBjiSZPBzj326tp07v+bEqpcvtycwlafsLORQGj+S6gYjElq8y843zfFKwqWaPEm # kp5GQIbxlUWdNmdGlcstw/O4W2+AVG89VTtQEPZ3OAy6pMwjIjaar2m26u0HFp9X # jP5zfZRbEfis6X8MnFqa3cNRUizgN1CmBtb7ZviXI0hm9jcL1DIYXnyqmJh0N+Ay # apMVQjFcLSRduKorjT9zlAYeM7DoTHH1c8C4eDD5WfpYzd0cPuLdnDxRWtaSwXJe # CpwDF4f3nfS/CH0pG6Zw3KTmT64pDxw04E/2PlYa4ocTM7zPV5RzvXSyK6ZNQFun # OLrarFLNIW4EU37Sta6abCnqm2bOPznbGptgGufK+Uo+ev6t90w+5xxLvXcXe0v4 # omY+4KBr9Atvm1vNlBJQeC2Tb0NUz0y0LDjzBWKORk/jCK6Lk2gsCcFm/KN6NKlg # WKI= # SIG # End signature block |