Policies: Import ingesteter (custom) ADMX-Vorlagen tenantübergreifend

Custom-ADMX-Definitionen (policyType 'admxIngested') haben tenant-spezifische
IDs -> Binding per Export-Id scheiterte im Ziel-Tenant mit 404. Import löst
solche Definitionen jetzt im Ziel über displayName/classType/categoryPath neu
auf und bindet an die dortige Id; Presentations werden per label/@odata.type
(Fallback: Reihenfolge je Typ) zugeordnet. Eingebaute Vorlagen (admxBacked)
unverändert per stabiler Id. categoryPath wird dafür mitexportiert.

Voraussetzung: dieselbe ADMX ist im Ziel-Tenant importiert; fehlt sie, wird die
betroffene Einstellung mit klarer Meldung übersprungen (Rest wird angelegt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 09:21:53 +02:00
co-authored by Claude Opus 4.8
parent ead650d326
commit 300c72eb4f
+88 -13
View File
@@ -243,7 +243,7 @@ function Get-GraphPolicyDetail {
if ($cfg.Key -eq 'administrativetemplate') {
$cfgRoot = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations/$Id"
$base = Invoke-MgGraphRequestRetry -Uri $cfgRoot -Method GET -AsRawJson
$dvUri = "$cfgRoot/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,version)"
$dvUri = "$cfgRoot/definitionValues?`$expand=definition(`$select=id,classType,displayName,categoryPath,policyType,version)"
$dvs = @(Get-GraphPaged -Uri $dvUri -AsRawJson)
foreach ($dv in $dvs) {
if (-not $dv) { continue }
@@ -484,6 +484,62 @@ function Import-GraphSettingsCatalogPolicy {
return Invoke-MgGraphRequestRetry -Uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -Method POST -Body $json -ContentType 'application/json'
}
# Loest eine ingestete (custom) ADMX-Definition im AKTUELLEN Ziel-Tenant auf.
# Custom-ADMX-IDs sind tenant-spezifisch -> Binding per Export-Id scheitert (404).
# Match ueber stabile Merkmale: displayName + classType (+ categoryPath).
# Voraussetzung: die ADMX ist im Ziel-Tenant importiert. Sonst $null.
function Resolve-TargetGpDefinition {
param($SrcDefinition)
$dn = [string](Get-PolicyProp $SrcDefinition 'displayName')
$ct = [string](Get-PolicyProp $SrcDefinition 'classType')
$cat = [string](Get-PolicyProp $SrcDefinition 'categoryPath')
if (-not $dn) { return $null }
$dnEsc = $dn -replace "'", "''"
$base = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions'
$cands = @()
try {
$uri = "$base`?`$filter=" + [uri]::EscapeDataString("displayName eq '$dnEsc'")
$cands = @(Get-GraphPaged -Uri $uri -AsRawJson)
} catch { $cands = @() }
# Fallback, falls $filter auf displayName nicht unterstuetzt wird: ueber categoryPath.
if ($cands.Count -eq 0 -and $cat) {
try {
$catEsc = $cat -replace "'", "''"
$uri2 = "$base`?`$filter=" + [uri]::EscapeDataString("categoryPath eq '$catEsc'")
$cands = @(Get-GraphPaged -Uri $uri2 -AsRawJson) | Where-Object { [string](Get-PolicyProp $_ 'displayName') -eq $dn }
} catch { $cands = @() }
}
if (@($cands).Count -eq 0) { return $null }
$narrow = @($cands) | Where-Object {
((-not $ct) -or ([string](Get-PolicyProp $_ 'classType') -eq $ct)) -and
((-not $cat) -or ([string](Get-PolicyProp $_ 'categoryPath') -eq $cat))
}
$match = @($narrow) | Select-Object -First 1
if (-not $match) { $match = @($cands) | Select-Object -First 1 }
return $match
}
# Findet zu einer Quell-Presentation die passende Ziel-Presentation-Id:
# 1) per label + @odata.type, 2) Fallback: i-te Ziel-Presentation gleichen Typs
# (in Definitions-Reihenfolge; $Counter zaehlt je Typ mit).
function Resolve-TargetPresentationId {
param($TargetPres, $SrcPresentation, [hashtable]$Counter)
$srcLabel = [string](Get-PolicyProp $SrcPresentation 'label')
$srcType = [string](Get-PolicyProp $SrcPresentation '@odata.type')
if ($srcLabel) {
$m = @($TargetPres) | Where-Object {
([string](Get-PolicyProp $_ 'label') -eq $srcLabel) -and
([string](Get-PolicyProp $_ '@odata.type') -eq $srcType)
} | Select-Object -First 1
if ($m) { return [string](Get-PolicyProp $m 'id') }
}
$idx = 0; if ($Counter.ContainsKey($srcType)) { $idx = [int]$Counter[$srcType] }
$sameType = @($TargetPres) | Where-Object { [string](Get-PolicyProp $_ '@odata.type') -eq $srcType }
$Counter[$srcType] = $idx + 1
if ($idx -lt @($sameType).Count) { return [string](Get-PolicyProp $sameType[$idx] 'id') }
return $null
}
function Import-GraphAdministrativeTemplate {
param([Parameter(Mandatory=$true)]$Policy)
@@ -504,23 +560,43 @@ function Import-GraphAdministrativeTemplate {
if (-not $newId) { throw 'Anlegen der Administrative-Vorlage-Huelle lieferte keine Id.' }
# 2) Jeden definitionValue einzeln anhaengen. Definition + Presentations werden
# per @odata.bind referenziert — die IDs eingebauter ADMX-Vorlagen sind
# tenantuebergreifend identisch, daher tenantunabhaengig einsetzbar.
# per @odata.bind referenziert. EINGEBAUTE ADMX-Vorlagen (policyType
# 'admxBacked') haben tenantuebergreifend identische IDs -> Export-Id direkt
# verwendbar. INGESTETE/CUSTOM ADMX ('admxIngested') hat tenant-spezifische
# IDs -> im Ziel-Tenant ueber displayName/classType/categoryPath neu aufloesen
# (setzt voraus, dass dieselbe ADMX im Ziel importiert ist; sonst 404).
$errors = @()
foreach ($dv in @(Get-PolicyProp $Policy 'definitionValues')) {
if (-not $dv) { continue }
$def = Get-PolicyProp $dv 'definition'
$defId = [string](Get-PolicyProp $def 'id')
if (-not $defId) {
$errors += 'definitionValue ohne Definition-Id uebersprungen'
continue
$def = Get-PolicyProp $dv 'definition'
$srcDefId = [string](Get-PolicyProp $def 'id')
$defName = [string](Get-PolicyProp $def 'displayName'); if (-not $defName) { $defName = $srcDefId }
if (-not $srcDefId) { $errors += 'definitionValue ohne Definition-Id uebersprungen'; continue }
$ingested = ([string](Get-PolicyProp $def 'policyType') -eq 'admxIngested')
$defId = $srcDefId
$tgtPres = $null
if ($ingested) {
$tgtDef = Resolve-TargetGpDefinition $def
if (-not $tgtDef) {
$errors += "${defName}: ingestete ADMX-Definition im Ziel-Tenant nicht gefunden - die passende ADMX muss dort importiert sein"
continue
}
$defId = [string](Get-PolicyProp $tgtDef 'id')
try { $tgtPres = @(Get-GraphPaged -Uri "$defRoot/$defId/presentations" -AsRawJson) } catch { $tgtPres = @() }
}
$presVals = @()
$presVals = @()
$typeCounter = @{}
foreach ($pv in @(Get-PolicyProp $dv 'presentationValues')) {
if (-not $pv) { continue }
$pres = Get-PolicyProp $pv 'presentation'
$presId = [string](Get-PolicyProp $pres 'id')
$pres = Get-PolicyProp $pv 'presentation'
if ($ingested) {
$presId = Resolve-TargetPresentationId -TargetPres $tgtPres -SrcPresentation $pres -Counter $typeCounter
if (-not $presId) { $errors += "${defName}: Presentation im Ziel nicht zuordenbar - Wert uebersprungen"; continue }
} else {
$presId = [string](Get-PolicyProp $pres 'id')
}
$entry = [ordered]@{
'@odata.type' = [string](Get-PolicyProp $pv '@odata.type')
'presentation@odata.bind' = "$defRoot('$defId')/presentations('$presId')"
@@ -543,8 +619,7 @@ function Import-GraphAdministrativeTemplate {
} catch {
$m = $_.Exception.Message
try { if ($_.ErrorDetails.Message) { $m = $_.ErrorDetails.Message } } catch {}
$dn = [string](Get-PolicyProp $def 'displayName'); if (-not $dn) { $dn = $defId }
$errors += "${dn}: $m"
$errors += "${defName}: $m"
}
}