mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
344 lines
15 KiB
PowerShell
344 lines
15 KiB
PowerShell
# Internal helpers for Set-GraphBulkAssignments (Public/Set-GraphBulkAssignments.ps1).
|
|
# Extracted here per architecture rule R11 (keep public driver files thin). These
|
|
# are module-internal (not exported) but several are also called by the bulk-
|
|
# assignment UI (Test-BulkAssignmentSupported, Get-BulkAssignmentObjectType, ...)
|
|
# and the online tests - all run in module scope, so name resolution is unchanged.
|
|
|
|
# App @odata.types Graph refuses an assignment filter for. Set-GraphBulkAssignments
|
|
# assigns these without the filter rather than failing the policy. Extend as
|
|
# further types are verified live; do not guess.
|
|
$script:BulkAssignmentAppTypesWithoutFilters = @(
|
|
'#microsoft.graph.webApp'
|
|
)
|
|
|
|
# Stable string key for an assignment — used for set-membership (dedupe,
|
|
# no-op detection, removal matching). Encodes the target discriminator +
|
|
# the fields we expose in the UI; Intent is appended for app-shape
|
|
# assignments so "Required to Group X" and "Available to Group X" are
|
|
# distinct entries.
|
|
function Get-AssignmentSignature
|
|
{
|
|
param($Target, [string]$Intent)
|
|
|
|
if(-not $Target) { return "" }
|
|
|
|
$type = [string]$Target.'@odata.type'
|
|
# Strip the leading '#microsoft.graph.' if present so signatures compare
|
|
# equal regardless of whether Graph returned the prefix.
|
|
$type = $type -replace '^#?microsoft\.graph\.', ''
|
|
|
|
$groupId = [string]$Target.groupId
|
|
$filterId = [string]$Target.deviceAndAppManagementAssignmentFilterId
|
|
$filterType = [string]$Target.deviceAndAppManagementAssignmentFilterType
|
|
# "none" and "" are equivalent — Intune sometimes writes one, sometimes
|
|
# the other. Normalise so signatures don't drift between Add runs. The all-zeros
|
|
# sentinel is the third spelling of the same thing (Test-AssignmentFilterDefined):
|
|
# without it, one assignment carrying the sentinel and an identical one carrying no
|
|
# filter property hash differently, so an already-assigned target looks new.
|
|
if($filterType -eq "none" -or -not (Test-AssignmentFilterDefined $filterId)) {
|
|
$filterType = ""
|
|
$filterId = ""
|
|
}
|
|
|
|
$intentPart = if([string]::IsNullOrEmpty($Intent)) { "" } else { [string]$Intent }
|
|
return "$type|$groupId|$filterId|$filterType|$intentPart".ToLowerInvariant()
|
|
}
|
|
|
|
function Get-AssignmentFullSignature
|
|
{
|
|
param($Tuple)
|
|
|
|
if(-not $Tuple) { return "" }
|
|
|
|
$runRemediationPart = ""
|
|
if($null -ne $Tuple.RunRemediation) { $runRemediationPart = [string][bool]$Tuple.RunRemediation }
|
|
|
|
$parts = @(
|
|
(Get-AssignmentSignature $Tuple.Target $Tuple.Intent),
|
|
(ConvertTo-StableAssignmentJson $Tuple.Settings),
|
|
(ConvertTo-StableAssignmentJson $Tuple.RunSchedule),
|
|
$runRemediationPart
|
|
)
|
|
return ($parts -join "|").ToLowerInvariant()
|
|
}
|
|
|
|
function ConvertTo-StableAssignmentJson
|
|
{
|
|
param($Value)
|
|
|
|
if($null -eq $Value) { return "" }
|
|
return (ConvertTo-StableAssignmentObject $Value | ConvertTo-Json -Depth 50 -Compress)
|
|
}
|
|
|
|
function ConvertTo-StableAssignmentObject
|
|
{
|
|
param($Value)
|
|
|
|
if($null -eq $Value) { return $null }
|
|
if($Value -is [string]) { return $Value }
|
|
if($Value -is [System.Collections.IDictionary]) {
|
|
$ordered = [ordered]@{}
|
|
foreach($key in @($Value.Keys | Sort-Object)) {
|
|
$ordered[[string]$key] = ConvertTo-StableAssignmentObject $Value[$key]
|
|
}
|
|
return [PSCustomObject]$ordered
|
|
}
|
|
if($Value -is [System.Collections.IEnumerable] -and $Value -isnot [PSCustomObject]) {
|
|
$items = [System.Collections.Generic.List[object]]::new()
|
|
foreach($item in $Value) {
|
|
[void]$items.Add((ConvertTo-StableAssignmentObject $item))
|
|
}
|
|
return $items.ToArray()
|
|
}
|
|
if($Value -is [PSCustomObject]) {
|
|
$ordered = [ordered]@{}
|
|
foreach($prop in @($Value.PSObject.Properties | Sort-Object Name)) {
|
|
$ordered[$prop.Name] = ConvertTo-StableAssignmentObject $prop.Value
|
|
}
|
|
return [PSCustomObject]$ordered
|
|
}
|
|
return $Value
|
|
}
|
|
|
|
# Project a raw assignment target (from Graph response) down to just the
|
|
# fields the bulk tool supports. Keeps the result POST-safe.
|
|
function ConvertTo-AssignmentTarget
|
|
{
|
|
param($Target)
|
|
|
|
$out = [ordered]@{ "@odata.type" = [string]$Target.'@odata.type' }
|
|
if($Target.groupId) { $out.groupId = [string]$Target.groupId }
|
|
if($Target.deviceAndAppManagementAssignmentFilterId) {
|
|
$out.deviceAndAppManagementAssignmentFilterId = [string]$Target.deviceAndAppManagementAssignmentFilterId
|
|
$out.deviceAndAppManagementAssignmentFilterType = [string]$Target.deviceAndAppManagementAssignmentFilterType
|
|
}
|
|
return [PSCustomObject]$out
|
|
}
|
|
|
|
# Build a fresh Graph target object from a UI-supplied assignment descriptor
|
|
# (the PSCustomObject the UI puts into AssignmentSettings.Assignments).
|
|
function Build-AssignmentTarget
|
|
{
|
|
param($Descriptor)
|
|
|
|
$obj = [ordered]@{
|
|
"@odata.type" = "#microsoft.graph.$($Descriptor.TargetType)"
|
|
}
|
|
if($Descriptor.GroupId) { $obj.groupId = [string]$Descriptor.GroupId }
|
|
if($Descriptor.FilterId) {
|
|
$obj.deviceAndAppManagementAssignmentFilterId = [string]$Descriptor.FilterId
|
|
$obj.deviceAndAppManagementAssignmentFilterType = if($Descriptor.FilterType) { [string]$Descriptor.FilterType } else { "include" }
|
|
}
|
|
return [PSCustomObject]$obj
|
|
}
|
|
|
|
# Bulk tool supports three assignment shapes:
|
|
# "simple" — `{target}` only. Most types.
|
|
# "app" — `{target, intent, settings?}`. mobileAppAssignments.
|
|
# "script" — `{target, runSchedule?, runRemediationScript?}`. Health
|
|
# scripts (deviceHealthScriptAssignments).
|
|
function Get-BulkAssignmentShape
|
|
{
|
|
param($PolicyType)
|
|
|
|
if(-not $PolicyType) { return $null }
|
|
switch ([string]$PolicyType.AssignmentsType) {
|
|
"mobileAppAssignments" { return "app" }
|
|
"deviceHealthScriptAssignments" { return "script" }
|
|
}
|
|
if(-not $PolicyType.AssignmentPropertiesToKeep) { return "simple" }
|
|
return $null
|
|
}
|
|
|
|
function Test-BulkAssignmentSupported
|
|
{
|
|
param($PolicyType)
|
|
|
|
if(-not $PolicyType) { return $false }
|
|
if(-not $PolicyType.SupportsAssignments) { return $false }
|
|
if(-not $PolicyType.AssignmentsType) { return $false }
|
|
|
|
# These types either do not expose a policy assignment action or use a
|
|
# custom assignment flow that is not the replace-all /assign contract.
|
|
# AppProtection uses the polymorphic /managedAppPolicies endpoint for
|
|
# list/read, but /assign is only bound to the concrete platform subtypes
|
|
# (iosManagedAppProtections, androidManagedAppProtections, windowsManagedAppProtections,
|
|
# mdmWindowsInformationProtectionPolicies). Calling /managedAppPolicies/{id}/assign
|
|
# is unsupported.
|
|
# AppleEnrollmentTypes has an `assignments` navigation property but no
|
|
# `/assign` action — assignments are managed by POST/DELETE on the nested
|
|
# /assignments collection, not the replace-all contract.
|
|
if($PolicyType.Id -in @(
|
|
"AppleEnrollmentTypes",
|
|
"AppProtection",
|
|
"Autopilot",
|
|
"IntuneBranding",
|
|
"Notifications",
|
|
"ScopeTags",
|
|
"TermsAndConditions"
|
|
)) { return $false }
|
|
|
|
return ($null -ne (Get-BulkAssignmentShape $PolicyType))
|
|
}
|
|
|
|
function Get-BulkAssignmentObjectType
|
|
{
|
|
param($PolicyType)
|
|
|
|
if(-not $PolicyType) { return $null }
|
|
|
|
# Per-type override on the PolicyType class takes precedence over the
|
|
# built-in heuristic below. Lets new PolicyTypes declare their assignment
|
|
# @odata.type next to the type definition instead of editing this central
|
|
# switch — see _AssignmentObjectType on IntunePolicyTypeBase.
|
|
$override = [string]$PolicyType.AssignmentObjectType
|
|
if($override) { return $override }
|
|
|
|
switch ([string]$PolicyType.AssignmentsType) {
|
|
"deviceHealthScriptAssignments" { return "#microsoft.graph.deviceHealthScriptAssignment" }
|
|
"deviceManagementScriptAssignments" { return "#microsoft.graph.deviceManagementScriptAssignment" }
|
|
"enrollmentConfigurationAssignments"{ return "#microsoft.graph.enrollmentConfigurationAssignment" }
|
|
"mobileAppAssignments" { return "#microsoft.graph.mobileAppAssignment" }
|
|
"hardwareConfigurationAssignments" { return "#microsoft.graph.hardwareConfigurationAssignment" }
|
|
}
|
|
|
|
switch ([string]$PolicyType.Id) {
|
|
"EndpointSecurity" { return "#microsoft.graph.deviceManagementIntentAssignment" }
|
|
"EnrollmentSettingsCatalog" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"DeviceConfiguration" { return "#microsoft.graph.deviceConfigurationAssignment" }
|
|
"CompliancePolicies" { return "#microsoft.graph.deviceCompliancePolicyAssignment" }
|
|
"CompliancePoliciesV2" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"SettingsCatalog" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"EndpointSecuritySettingsCatalog" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"DeviceConfigurationScripts" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"InventoryPolicies" { return "#microsoft.graph.deviceManagementConfigurationPolicyAssignment" }
|
|
"AppConfigurationManagedDevice" { return "#microsoft.graph.managedDeviceMobileAppConfigurationAssignment" }
|
|
"AppConfigurationManagedApp" { return "#microsoft.graph.targetedManagedAppPolicyAssignment" }
|
|
"AndroidOEMConfig" { return "#microsoft.graph.managedDeviceMobileAppConfigurationAssignment" }
|
|
"Applications" { return "#microsoft.graph.mobileAppAssignment" }
|
|
"IosLobAppProvisioningConfigurations" { return "#microsoft.graph.iosLobAppProvisioningConfigurationAssignment" }
|
|
"PolicySets" { return "#microsoft.graph.policySetAssignment" }
|
|
"UpdatePolicies" { return "#microsoft.graph.deviceConfigurationAssignment" }
|
|
"iOSiPadOSUpdatePolicies" { return "#microsoft.graph.deviceConfigurationAssignment" }
|
|
"macOSUpdatePolicies" { return "#microsoft.graph.deviceConfigurationAssignment" }
|
|
"FeatureUpdates" { return "#microsoft.graph.windowsFeatureUpdateProfileAssignment" }
|
|
"QualityUpdates" { return "#microsoft.graph.windowsQualityUpdateProfileAssignment" }
|
|
"QualityUpdatePolicies" { return "#microsoft.graph.windowsQualityUpdatePolicyAssignment" }
|
|
"DriverUpdateProfiles" { return "#microsoft.graph.windowsDriverUpdateProfileAssignment" }
|
|
"AdministrativeTemplates" { return "#microsoft.graph.groupPolicyConfigurationAssignment" }
|
|
"W365ProvisioningPolicies" { return "#microsoft.graph.cloudPcProvisioningPolicyAssignment" }
|
|
"W365UserSettings" { return "#microsoft.graph.cloudPcUserSettingAssignment" }
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
# Map a policy's @odata.type (e.g. "#microsoft.graph.win32LobApp") to the
|
|
# matching mobileAppAssignmentSettings derived type name (e.g.
|
|
# "win32LobAppAssignmentSettings"). The Graph naming convention is
|
|
# `<appType>AssignmentSettings`, so a strip-and-append is enough. Returns
|
|
# $null when the input doesn't look like a known app type.
|
|
function Get-AppSettingsTypeForPolicy
|
|
{
|
|
param([string]$PolicyOdataType)
|
|
|
|
if([string]::IsNullOrEmpty($PolicyOdataType)) { return $null }
|
|
$name = $PolicyOdataType -replace '^#?microsoft\.graph\.', ''
|
|
if([string]::IsNullOrEmpty($name)) { return $null }
|
|
return "${name}AssignmentSettings"
|
|
}
|
|
|
|
# Build a deviceHealthScriptRunSchedule PSCustomObject from a UI spec
|
|
# hashtable. Picks the right @odata.type based on scheduleType
|
|
# (Hourly/Daily/Once) and emits only the fields each schedule supports.
|
|
# Returns $null when the spec doesn't have enough information.
|
|
function Build-HealthScriptSchedule
|
|
{
|
|
param([Hashtable]$Spec)
|
|
|
|
if(-not $Spec -or -not $Spec.ContainsKey('scheduleType')) { return $null }
|
|
|
|
$type = [string]$Spec['scheduleType']
|
|
$interval = 0
|
|
if($Spec.ContainsKey('interval')) {
|
|
try { $interval = [int]$Spec['interval'] } catch { $interval = 0 }
|
|
}
|
|
|
|
switch ($type) {
|
|
"Hourly" {
|
|
return [PSCustomObject]@{
|
|
"@odata.type" = "#microsoft.graph.deviceHealthScriptHourlySchedule"
|
|
interval = $interval
|
|
}
|
|
}
|
|
"Daily" {
|
|
$obj = [ordered]@{
|
|
"@odata.type" = "#microsoft.graph.deviceHealthScriptDailySchedule"
|
|
interval = $interval
|
|
useUtc = [bool]$Spec['useUtc']
|
|
}
|
|
if($Spec.ContainsKey('time') -and $Spec['time']) {
|
|
$obj.time = [string]$Spec['time']
|
|
}
|
|
return [PSCustomObject]$obj
|
|
}
|
|
"Once" {
|
|
$obj = [ordered]@{
|
|
"@odata.type" = "#microsoft.graph.deviceHealthScriptRunOnceSchedule"
|
|
interval = $interval
|
|
useUtc = [bool]$Spec['useUtc']
|
|
}
|
|
if($Spec.ContainsKey('date') -and $Spec['date']) { $obj.date = [string]$Spec['date'] }
|
|
if($Spec.ContainsKey('time') -and $Spec['time']) { $obj.time = [string]$Spec['time'] }
|
|
return [PSCustomObject]$obj
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# Convert a user-built settings hashtable (from the App settings... dialog)
|
|
# into a Graph-shaped PSCustomObject with the requested @odata.type stamped
|
|
# at the top. Nested complex types (win32LobAppRestartSettings etc.) are
|
|
# expected to already carry their own @odata.type in the hashtable; this
|
|
# function only wraps the outer object.
|
|
function ConvertTo-AppSettingsObject
|
|
{
|
|
param([Hashtable]$Hash, [string]$GraphType)
|
|
|
|
if(-not $Hash) { return $null }
|
|
$obj = [ordered]@{ "@odata.type" = "#microsoft.graph.$GraphType" }
|
|
foreach($k in $Hash.Keys) {
|
|
$obj[$k] = ConvertTo-AppSettingsValue $Hash[$k]
|
|
}
|
|
return [PSCustomObject]$obj
|
|
}
|
|
|
|
# Recursive companion to ConvertTo-AppSettingsObject. Walks arbitrary
|
|
# nesting depth, converting any [Hashtable] (including nested ones inside
|
|
# arrays) into [PSCustomObject] so ConvertTo-Json emits JSON objects rather
|
|
# than the @{key=value} debug format. Arrays are preserved and their
|
|
# elements recursed individually. Strings (which are IEnumerable) and
|
|
# PSCustomObject (already correct shape) pass through unchanged.
|
|
function ConvertTo-AppSettingsValue
|
|
{
|
|
param($Value)
|
|
|
|
if($null -eq $Value) { return $null }
|
|
if($Value -is [Hashtable]) {
|
|
$nested = [ordered]@{}
|
|
foreach($k in $Value.Keys) {
|
|
$nested[$k] = ConvertTo-AppSettingsValue $Value[$k]
|
|
}
|
|
return [PSCustomObject]$nested
|
|
}
|
|
if($Value -is [string]) { return $Value }
|
|
if($Value -is [System.Collections.IList]) {
|
|
$items = [System.Collections.Generic.List[object]]::new()
|
|
foreach($item in $Value) {
|
|
[void]$items.Add((ConvertTo-AppSettingsValue $item))
|
|
}
|
|
return $items.ToArray()
|
|
}
|
|
return $Value
|
|
}
|