diff --git a/src/PolicyIO.ps1 b/src/PolicyIO.ps1 index eda598c..618cc12 100644 --- a/src/PolicyIO.ps1 +++ b/src/PolicyIO.ps1 @@ -706,27 +706,82 @@ function Invoke-PolicyAssignEndpoint { $name = [string](Get-PolicyProp $it 'policyName') $include = @(Get-PolicyProp $it 'include') | Where-Object { $_ } $exclude = @(Get-PolicyProp $it 'exclude') | Where-Object { $_ } + # Integrierte (virtuelle) Include-Ziele: Alle Geraete / Alle Benutzer. + $allDevices = [bool](Get-PolicyProp $it 'allDevices') + $allUsers = [bool](Get-PolicyProp $it 'allUsers') + # Modus: 'replace' (Default, Full-Replace wie bisher) oder 'add' (bestehende + # Zuweisungen erhalten und die neuen Gruppen dazu mergen). Die Graph-/assign- + # Action ersetzt IMMER die komplette Liste -> fuer 'add' muessen die + # bestehenden Zuweisungen vorher gelesen und mitgeschickt werden. + $mode = ([string](Get-PolicyProp $it 'mode')).ToLower() + if ($mode -ne 'add') { $mode = 'replace' } $cfg = Get-PolicyTypeConfigByExportType $exportType if (-not $cfg) { $results += @{ id = $id; policyName = $name; success = $false; error = "Unbekannter exportType: $exportType" }; continue } if (-not $id) { $results += @{ policyName = $name; success = $false; error = 'Policy-Id fehlt' }; continue } - if (@($include).Count -eq 0 -and @($exclude).Count -eq 0) { + if (@($include).Count -eq 0 -and @($exclude).Count -eq 0 -and -not $allDevices -and -not $allUsers) { $results += @{ id = $id; policyName = $name; success = $true; skipped = $true } continue } $assignments = @() + $seen = @{} # Dedup-Key "odataType|groupId" -> $true + + if ($mode -eq 'add') { + # Bestehende Zuweisungen lesen und 1:1 uebernehmen (inkl. evtl. Filter/ + # allDevices/allLicensedUsers). Schlaegt das Lesen fehl, brechen wir fuer + # diese Policy ab, statt versehentlich bestehende Zuweisungen zu loeschen. + try { + $existing = Invoke-MgGraphRequestRetry -Uri "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)/$id/assignments" -Method GET + foreach ($a in @($existing.value)) { + $t = $a.target + if (-not $t) { continue } + $ot = [string]$t.'@odata.type' + $gid = [string]$t.groupId + $key = "$ot|$gid" + if ($seen[$key]) { continue } + $seen[$key] = $true + $tgt = @{ '@odata.type' = $ot } + if ($gid) { $tgt['groupId'] = $gid } + foreach ($fld in @('deviceAndAppManagementAssignmentFilterId','deviceAndAppManagementAssignmentFilterType')) { + $v = $t.$fld + if ($null -ne $v -and [string]$v -ne '') { $tgt[$fld] = $v } + } + $assignments += @{ target = $tgt } + } + } catch { + $em = $_.Exception.Message + try { if ($_.ErrorDetails.Message) { $em = $_.ErrorDetails.Message } } catch {} + $results += @{ id = $id; policyName = $name; success = $false; error = "Bestehende Zuweisungen nicht lesbar (Hinzufuegen-Modus): $em" } + continue + } + } + foreach ($g in $include) { + $key = "#microsoft.graph.groupAssignmentTarget|$g" + if ($seen[$key]) { continue } + $seen[$key] = $true $assignments += @{ target = @{ '@odata.type' = '#microsoft.graph.groupAssignmentTarget'; groupId = [string]$g } } } foreach ($g in $exclude) { + $key = "#microsoft.graph.exclusionGroupAssignmentTarget|$g" + if ($seen[$key]) { continue } + $seen[$key] = $true $assignments += @{ target = @{ '@odata.type' = '#microsoft.graph.exclusionGroupAssignmentTarget'; groupId = [string]$g } } } + if ($allDevices) { + $key = '#microsoft.graph.allDevicesAssignmentTarget|' + if (-not $seen[$key]) { $seen[$key] = $true; $assignments += @{ target = @{ '@odata.type' = '#microsoft.graph.allDevicesAssignmentTarget' } } } + } + if ($allUsers) { + $key = '#microsoft.graph.allLicensedUsersAssignmentTarget|' + if (-not $seen[$key]) { $seen[$key] = $true; $assignments += @{ target = @{ '@odata.type' = '#microsoft.graph.allLicensedUsersAssignmentTarget' } } } + } $json = @{ assignments = @($assignments) } | ConvertTo-Json -Depth 10 $uri = "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)/$id/assign" try { Invoke-MgGraphRequestRetry -Uri $uri -Method POST -Body $json -ContentType 'application/json' | Out-Null - $results += @{ id = $id; policyName = $name; success = $true; includeCount = @($include).Count; excludeCount = @($exclude).Count } + $results += @{ id = $id; policyName = $name; success = $true; mode = $mode; includeCount = @($include).Count; excludeCount = @($exclude).Count } } catch { $m = $_.Exception.Message try { if ($_.ErrorDetails.Message) { $m = $_.ErrorDetails.Message } } catch {} diff --git a/www/app.js b/www/app.js index 69f7c28..df80017 100644 --- a/www/app.js +++ b/www/app.js @@ -6409,6 +6409,12 @@ function polUpdateSelCount() { const n = keys.length; document.getElementById('polSelCount').textContent = n; document.getElementById('btnPolExport').disabled = n === 0; + const btnA = document.getElementById('btnPolAssignGroups'); + if (btnA) { + btnA.disabled = n === 0; + const c = document.getElementById('polAssignSelCount'); + if (c) c.textContent = n; + } // Konsolidieren nur bei >=2 Policies UND alle vom Typ Settings Catalog. const btnC = document.getElementById('btnPolConsolidate'); if (btnC) { @@ -6843,6 +6849,168 @@ document.getElementById('polAssignApply')?.addEventListener('click', async () => } }); +// ============================================================= +// Bulk: mehreren ausgewählten Policies dieselben Gruppen zuweisen +// ============================================================= +const POL_TYPE_TO_EXPORT = { + settingscatalog: 'SettingsCatalog', + compliance: 'CompliancePolicy', + configuration: 'ConfigurationProfile', + administrativetemplate: 'AdministrativeTemplate', +}; +const PolBulkAssign = { policies: [], include: [], exclude: [] }; +let _polBulkSearchTimer = null; + +function openPolBulkAssign() { + const sel = PolState.items.filter(p => PolState.selected.has(polKey(p))); + if (!sel.length) return; + PolBulkAssign.policies = sel.map(p => ({ + id: p.id, + name: p.name || '(ohne Name)', + exportType: POL_TYPE_TO_EXPORT[p.type] || '', + })); + PolBulkAssign.include = []; + PolBulkAssign.exclude = []; + document.getElementById('polBulkCount').textContent = sel.length; + document.getElementById('polBulkNames').innerHTML = PolBulkAssign.policies.map(p => + `${escapeHtml(p.name)} ${escapeHtml(POL_TYPE_LABELS[p.exportType] || 'Policy')}` + ).join(''); + const addRadio = document.querySelector('input[name="polBulkMode"][value="add"]'); + if (addRadio) addRadio.checked = true; + const ad = document.getElementById('polBulkAllDevices'); if (ad) ad.checked = false; + const au = document.getElementById('polBulkAllUsers'); if (au) au.checked = false; + renderPolBulkChips('include'); + renderPolBulkChips('exclude'); + updatePolBulkApply(); + openModal('modalPolBulkAssign'); +} + +function renderPolBulkChips(kind) { + const box = document.querySelector(`.pol-assign-chips[data-scope="bulk"][data-kind="${kind}"]`); + if (!box) return; + box.innerHTML = PolBulkAssign[kind].map((g, j) => ` + + ${escapeHtml(g.name)} + + `).join(''); + updatePolBulkApply(); +} + +function updatePolBulkApply() { + const inc = PolBulkAssign.include.length, exc = PolBulkAssign.exclude.length; + const allDev = document.getElementById('polBulkAllDevices')?.checked || false; + const allUsr = document.getElementById('polBulkAllUsers')?.checked || false; + const has = inc + exc > 0 || allDev || allUsr; + const btn = document.getElementById('polBulkApply'); + if (btn) btn.disabled = !has; + const info = document.getElementById('polBulkInfo'); + if (info) { + if (!has) { info.textContent = 'Mindestens eine Gruppe oder ein integriertes Ziel wählen.'; } + else { + const parts = []; + if (allDev) parts.push('Alle Geräte'); + if (allUsr) parts.push('Alle Benutzer'); + if (inc) parts.push(`${inc} Include`); + if (exc) parts.push(`${exc} Exclude`); + info.textContent = parts.join(' · '); + } + } +} + +function polBulkHideDropdowns() { + document.querySelectorAll('.pol-bulk-dropdown').forEach(d => { d.classList.add('hidden'); d.innerHTML = ''; }); +} + +// Gruppensuche (debounced) im Bulk-Modal +document.getElementById('modalPolBulkAssign')?.addEventListener('input', e => { + const input = e.target.closest('.pol-bulk-search'); + if (!input) return; + const kind = input.dataset.kind; + const dd = document.querySelector(`.pol-bulk-dropdown[data-kind="${kind}"]`); + const q = input.value.trim(); + clearTimeout(_polBulkSearchTimer); + if (q.length < 2) { if (dd) { dd.classList.add('hidden'); dd.innerHTML = ''; } return; } + if (dd) { dd.classList.remove('hidden'); dd.innerHTML = '