IntuneManagement 4.0.0-beta1

This commit is contained in:
Mikael Karlsson
2026-09-23 19:13:09 +10:00
commit 7869619510
892 changed files with 577109 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
# Shared plumbing behind the policy-picker dialogs in both UI backends.
#
# The picker used to work off a full-tenant sweep: every Add Pair click ran
# Get-GraphPolicies once per policy group, which took minutes on a real tenant.
# Instead the user now picks a scope (one policy group or one policy type) and
# types a name; the search goes to Graph as a server-side prefix filter and
# comes back with just the matches.
#
# Lives in Internal/ rather than UI/ so WPF and Avalonia agree on the scope
# list and the search call, and so the UI stays a thin caller (R9/R10).
# Minimum characters before a search is sent. Below this the prefix matches
# most of the tenant, which is the slow full sweep this exists to avoid. Users
# who really want everything in the scope click "Load all in scope" instead.
$script:PolicySearchMinLength = 2
function Get-PolicySearchMinLength
{
return $script:PolicySearchMinLength
}
# The scope list for the picker's "Search in" dropdown: every policy group,
# each followed by its own policy types. Groups are listed first so picking
# "everything in Configuration" is one click; the indented type rows narrow it
# to a single API.
#
# Key is what the UI binds SelectedValue to - Id alone is not unique because a
# group and a type can carry the same Id (e.g. Applications).
function Get-PolicySearchScopes
{
param(
# Marks the scope for this policy type as the default selection. The
# compare flows pass the source policy's type, which is the scope the
# user wants almost every time.
[string]$DefaultPolicyTypeId
)
$scopes = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach($group in @($script:IntuneGroups | Where-Object { $_.Title } | Sort-Object Title))
{
[void]$scopes.Add([PSCustomObject]@{
Key = "Group|$($group.Id)"
Id = $group.Id
Kind = "Group"
Title = "$($group.Title) (all types)"
GroupId = $group.Id
IsDefault = $false
})
foreach($type in @($group.PolicyTypes | Where-Object { $_.Title -and $_.API } | Sort-Object Title))
{
[void]$scopes.Add([PSCustomObject]@{
Key = "Type|$($type.Id)"
Id = $type.Id
Kind = "Type"
# Leading spaces indent the type under its group row. ASCII
# only - see the string-literal gate in Static.Tests.ps1.
Title = " $($type.Title)"
GroupId = $group.Id
IsDefault = ($DefaultPolicyTypeId -and $type.Id -eq $DefaultPolicyTypeId)
})
}
}
return $scopes.ToArray()
}
# The tenants the picker can search, one entry per live token - same source and
# labelling as the Copy dialog's destination-tenant combo (Get-TokenInfo, which
# is provider-agnostic, so MSAL / OAuth / MgGraph tokens all appear). Returns an
# empty array when only one tenant is signed in; callers hide the combo then.
#
# Searching another tenant works because Get-GraphPolicies stamps each policy
# with the TokenId it came from, and GetFullObject / Invoke-PolicyHydrate resolve
# against that stamp - so a policy picked from tenant B still hydrates and
# compares against tenant B.
function Get-PolicySearchTenants
{
$tokens = @(Get-TokenInfo)
if($tokens.Count -le 1) { return @() }
$tenants = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach($token in $tokens)
{
if(-not $token) { continue }
$title = Get-StringOrDefault $token.TenantName $token.TenantID
if([String]::IsNullOrWhiteSpace($title)) { $title = "Tenant $($token.Id)" }
if($token.IsDefault -eq $true) { $title = "$title (Current)" }
[void]$tenants.Add([PSCustomObject]@{
Key = "Token|$($token.Id)"
TokenId = [int]$token.Id
TenantId = $token.TenantID
Title = $title
IsDefault = ($token.IsDefault -eq $true)
})
}
return $tenants.ToArray()
}
# Key of the tenant to preselect: the current one, else the first.
function Get-PolicySearchDefaultTenantKey
{
param($Tenants)
$all = @($Tenants)
if($all.Count -eq 0) { return $null }
$current = $all | Where-Object { $_.IsDefault } | Select-Object -First 1
if($current) { return $current.Key }
return $all[0].Key
}
# Default scope key for a policy type id: the type's own row when it is in the
# list, otherwise its group, otherwise the first scope. Returns $null when
# there are no scopes at all.
function Get-PolicySearchDefaultScopeKey
{
param(
$Scopes,
[string]$PolicyTypeId
)
$all = @($Scopes)
if($all.Count -eq 0) { return $null }
if($PolicyTypeId)
{
$typeScope = $all | Where-Object { $_.Kind -eq "Type" -and $_.Id -eq $PolicyTypeId } | Select-Object -First 1
if($typeScope) { return $typeScope.Key }
}
return $all[0].Key
}
# Run one scoped search. $Scope is an entry from Get-PolicySearchScopes; an
# empty $SearchText returns everything in the scope (the "Load all in scope"
# path). Types whose endpoint rejects a server-side filter are handled inside
# Get-GraphPolicies, which always re-checks names client-side.
function Search-IntunePolicies
{
param(
[Parameter(Mandatory = $true)]
$Scope,
[string]$SearchText,
# Ids to drop from the result - used to keep a policy from being
# compared against itself.
[string[]]$ExcludeIds,
[Int]$TokenId = (Get-DefaultTokenId)
)
if(-not $Scope) { return @() }
$params = @{ TokenId = $TokenId }
if($Scope.Kind -eq "Group") { $params.Add("PolicyGroup", $Scope.Id) }
else { $params.Add("PolicyType", $Scope.Id) }
if($SearchText) { $params.Add("NameFilter", $SearchText) }
$found = @()
try {
$found = @(Get-GraphPolicies @params)
}
catch {
Write-LogError "Search-IntunePolicies failed for scope $($Scope.Key)" $_.Exception
return @()
}
if($ExcludeIds -and $ExcludeIds.Count -gt 0) {
$found = @($found | Where-Object { $ExcludeIds -notcontains $_.Id })
}
return $found
}