diff --git a/src/PolicyIO.ps1 b/src/PolicyIO.ps1 index f7d1c2c..6b9df4b 100644 --- a/src/PolicyIO.ps1 +++ b/src/PolicyIO.ps1 @@ -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" } }