Fix Windows Delete edge cases with labels.#42632
Conversation
- Address code review: SCEP atomic normalization for delete, failed atomic detail includes DeleteCommands, only update rows when delete command was actually enqueued, batch UPDATE for mark-for-removal, if-else→switch lint fix, remove unused const. - Fix install query to re-install profiles currently marked for removal but back in desired state. - Fix test assertions to account for two-phase Windows profile removal.
Lint fixes:
- Remove unused simulateWindowsRemoveReconciliation function
- Replace interface{} with any (modernize lint)
Integration test fixes:
- TestDeleteMDMProfileCancelsInstalls: both Windows hosts have
non-NULL status (pending) after reconciler runs, so both get
remove+pending (not just host4)
- TestWindowsProfileResend: account for <Delete> commands when
profile content changes; clean up queued commands in subtest
cleanup to prevent cross-contamination
- TestHostMDMProfilesExcludeLabels: add profile_name to
windowsProfilesToRemoveQuery so remove rows have correct names
- Preserve NotFound error contract in DeleteMDMWindowsConfigProfile when profile doesn't exist (Copilot, CodeRabbit) - Restore IsNotFound assertion in TestMDMWindowsConfigProfiles - Only delete remove+verified rows in response handler cleanup, not verifying (Copilot: verifying is in-flight, not terminal) - Restrict install query for remove rows to exclude verifying/verified to prevent install churn while Delete is in-flight (Copilot) - Use 16-byte zero checksum instead of empty slice for BINARY(16) column compatibility (Copilot) - Add deprecation comment to UpdateOrDeleteHostMDMWindowsProfile explaining it is superseded by response handler cleanup - Remove SQL comment containing ? inside query template (was treated as bind variable by sqlx.In, causing runtime errors)
cleanupStaleWindowsRemoveRows now only queries and deletes remove rows for hosts present in the current want map, instead of scanning all remove rows in the table. This prevents implicitly hiding issues for hosts the current test phase doesn't check.
- WindowsResponseToDeliveryStatusForRemove now treats 500 (Command Failed) as success. Windows returns 500 (not 405) for CSP nodes that do not support <Delete>, such as DeviceLock/AccountLockoutPolicy and some SystemServices nodes. Since removal is best-effort, this prevents permanent remove+failed rows in host details. - TestDeleteMDMProfileCancelsInstalls: fix second Windows assertion at line 6927 to expect persistent remove+pending rows (no simulated device check-in processes the <Delete> command in this test). - TestHostMDMProfilesExcludeLabels: fix assertion to expect install (not remove) after label exclusion is removed and profile becomes desired again -- the install query correctly flips remove rows back to install when the profile re-enters the desired state. - TestWindowsProfileResend: content change updates profile in place (same name, different checksum) so only a re-install is sent, not a delete+install pair.
…iles. Windows CSPs support multiple profiles targeting the same LocURI via built-in conflict resolution (LastWrite, LowestValueMostSecure, etc.). However, sending <Delete> for a shared LocURI removes the CSP node entirely, bypassing conflict resolution and undoing the remaining profile's settings. BuildDeleteCommandFromProfileBytes now accepts an optional locURIsInUseByOtherProfiles set. LocURIs targeted by other active profiles in the same team are skipped when generating <Delete> commands. Both deletion paths (cancelWindowsHostInstallsFor- DeletedMDMProfiles and ReconcileWindowsProfiles) query for active LocURIs before generating commands. When all LocURIs are protected, no <Delete> is generated and host-profile rows are cleaned up directly. Includes unit tests for single/multi/atomic profiles with full, partial, and no LocURI protection.
…iles. After the install query fix (profiles in desired state with operation_type='remove' are flipped back to install), W2 becomes install+pending instead of remove+verifying. Update all downstream assertions in this test to expect the visible install+pending state instead of empty (filtered-out remove+verifying).
…gerReconcileProfiles. triggerReconcileProfiles() calls awaitTriggerProfileSchedule then UPDATEs all pending Windows profiles to verifying. After the first triggerReconcileProfiles call, W2 transitions from install+pending to install+verifying. All subsequent assertions must use verifying.
Team deletion previously bypassed cancelWindowsHostInstallsFor- DeletedMDMProfiles because it deleted config profiles via SQL cascade (teamRefs loop) without calling the cancel function. Now DeleteTeam reads the SyncML bytes and profile UUIDs before deleting the config rows, then calls the cancel function to generate <Delete> commands for profiles that were delivered to hosts. This ensures settings are removed from devices when a team is deleted. Also fixes Phase 0 in the cancel function to only delete terminal remove rows (failed/verified/verifying), not remove+NULL rows which may need <Delete> commands. Phase 2 now also picks up remove+NULL rows created by bulkSetPending during team moves.
…yHostMDMProfiles. After triggerReconcileProfiles sets W2 to verifying, the subsequent VerifyHostMDMProfiles call at line 5917 advances it to verified. All assertions after that point must use verified, not verifying.
1. <Delete> commands wrapped in <Atomic> caused rollback failures 2. $FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID not substituted in delete LocURIs
…tdm/fleet into victor/33418-windows-delete
…delete # Conflicts: # server/datastore/mysql/microsoft_mdm.go
…ared LocURI from one profile would delete it even though another profile still uses it.
…rite a subsequent Add.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Pull request overview
This PR addresses Windows MDM profile deletion/edit edge cases where LocURI <Delete> commands were being over-protected due to team-wide LocURI retention logic, especially in the presence of label-scoped profiles (issue #42591).
Changes:
- Adds a two-pass LocURI “protection” approach (team-wide first, then per-host refinement for label-scoped protectors).
- Introduces helper logic to enqueue supplemental
<Delete>commands for hosts where label-scoped protectors don’t apply. - Adds datastore tests for LocURI protection and for delete generation when LocURIs are removed during profile edits.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| server/datastore/mysql/microsoft_mdm.go | Implements two-pass LocURI protection and per-host label-scoped supplemental delete handling. |
| server/datastore/mysql/microsoft_mdm_test.go | Adds tests around shared-LocURI protection and edit-time LocURI deletion behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
WalkthroughThis change updates Windows MDM profile deletion and editing logic in the MySQL datastore to implement a two-pass LocURI protection strategy accounting for label scoping. The first pass collects protected LocURIs from other active profiles into a protection map with reverse indexing. The second pass identifies which protecting profiles are label-scoped, checks per-host install assignments in the database, and enqueues supplemental delete commands only for hosts where the label-scoped protector does not apply. Two new test cases validate LocURI lifecycle behavior across profile deletion and editing scenarios. Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
server/datastore/mysql/microsoft_mdm.go (2)
2762-2770:⚠️ Potential issue | 🟠 MajorReserved profiles are missing from the edit-time protection set.
Line 2764 only loads
existingProfilesforincomingNames, so the loop at Lines 2880-2885 never sees reserved Windows profiles that are implicitly kept but omitted from the request. Any LocURI owned only by one of those reserved profiles is absent fromallRetainedURIs, and an edit can emit a<Delete>for a setting Fleet still intends to enforce. Load kept reserved profiles explicitly before building the retained/protector maps.Also applies to: 2880-2885
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/datastore/mysql/microsoft_mdm.go` around lines 2762 - 2770, The code only loads existingProfiles for incomingNames, so reserved Windows profiles that are implicitly kept are never included and their LocURIs may be missing from allRetainedURIs; modify the logic in the block that builds existingProfiles (use the same loadExistingProfiles stmt or an additional query) to also explicitly load reserved/kept profiles for profTeamID (e.g., those marked reserved or with the known reserved names) and merge them into existingProfiles before building the retained/protector maps (used later to populate allRetainedURIs and in the loop around Lines ~2880-2885); ensure you update any variables used in that flow (incomingNames, existingProfiles, allRetainedURIs) so reserved profiles are treated as retained.
1283-1318:⚠️ Potential issue | 🟠 MajorScope the protection set per current team, not across the whole union.
At Line 1285,
teamIDsis built as a union of every affected host's current team, and Lines 1296-1317 collapse all protectors into oneactiveLocURIsset. That set is then reused for every host intargets, so an unscoped profile from team A can suppress deletes for a host still in no-team or team B. Pass 2 won't fix that because it only rechecks label-scoped protectors. Key the protection set by current team, or partitiontarget.hostUUIDsby team before classifyingsafeURIs.As per coding guidelines, queries that drive host-specific behavior should be precisely scoped to avoid unintended results.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/datastore/mysql/microsoft_mdm.go` around lines 1283 - 1318, The code currently builds a single unioned activeLocURIs/locURIToProtectingProfiles from teamIDs and applies it to every host in targets, letting unscoped profiles on team A block deletes for hosts in other teams; fix by partitioning targets by their current team (e.g., group target.hostUUIDs or targets by host.TeamID) and for each team run the mdm_windows_configuration_profiles query (the activeProfilesStmt flow that produces activeLocURIs and locURIToProtectingProfiles via ExtractLocURIsFromProfileBytes and FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll) to produce per-team maps; then use the per-team activeLocURIs when classifying safeURIs for hosts in that team (instead of the single shared activeLocURIs/teamIDs variables).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 2871-2877: The LocURI extraction in the incoming profile loop uses
fleet.ExtractLocURIsFromProfileBytes on p.SyncML without normalizing SCEP
placeholders, so replace occurrences of the
$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID placeholder with the profile UUID (p.UUID
or use incomingNameToUUID[p.Name]) the same way the delete path does before
calling fleet.ExtractLocURIsFromProfileBytes; apply the same normalization
wherever LocURIs are extracted for retained/new/old sets (the blocks populating
allRetainedURIs, oldURIs, and newURIs) and then populate allRetainedURIs and
editLocURIProtectors as before so resolved on-device LocURIs, not raw
placeholders, are used for protection checks.
---
Outside diff comments:
In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 2762-2770: The code only loads existingProfiles for incomingNames,
so reserved Windows profiles that are implicitly kept are never included and
their LocURIs may be missing from allRetainedURIs; modify the logic in the block
that builds existingProfiles (use the same loadExistingProfiles stmt or an
additional query) to also explicitly load reserved/kept profiles for profTeamID
(e.g., those marked reserved or with the known reserved names) and merge them
into existingProfiles before building the retained/protector maps (used later to
populate allRetainedURIs and in the loop around Lines ~2880-2885); ensure you
update any variables used in that flow (incomingNames, existingProfiles,
allRetainedURIs) so reserved profiles are treated as retained.
- Around line 1283-1318: The code currently builds a single unioned
activeLocURIs/locURIToProtectingProfiles from teamIDs and applies it to every
host in targets, letting unscoped profiles on team A block deletes for hosts in
other teams; fix by partitioning targets by their current team (e.g., group
target.hostUUIDs or targets by host.TeamID) and for each team run the
mdm_windows_configuration_profiles query (the activeProfilesStmt flow that
produces activeLocURIs and locURIToProtectingProfiles via
ExtractLocURIsFromProfileBytes and
FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll) to produce per-team maps;
then use the per-team activeLocURIs when classifying safeURIs for hosts in that
team (instead of the single shared activeLocURIs/teamIDs variables).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0051a101-2216-40a3-ba14-5e457321de48
📒 Files selected for processing (2)
server/datastore/mysql/microsoft_mdm.goserver/datastore/mysql/microsoft_mdm_test.go
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## windows-delete #42632 +/- ##
==================================================
+ Coverage 66.67% 66.69% +0.01%
==================================================
Files 2533 2533
Lines 203254 203397 +143
Branches 9019 9019
==================================================
+ Hits 135527 135647 +120
- Misses 55455 55467 +12
- Partials 12272 12283 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Related issue: Resolves #42591
Checklist for submitter
Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually
Summary by CodeRabbit