Skip to content

[Resources] Keep overrides and resource selectors in Update-AzPolicyAssignment/Exemption - #30206

Open
precondition wants to merge 7 commits into
Azure:mainfrom
precondition:fix/update-policyassignment-keep-overrides-selectors
Open

precondition wants to merge 7 commits into
Azure:mainfrom
precondition:fix/update-policyassignment-keep-overrides-selectors

Conversation

@precondition

@precondition precondition commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Updating an unrelated property of a policy assignment with Update-AzPolicyAssignment (e.g. just its display name) silently wipes out its overrides and resource selectors. Update-AzPolicyExemption does the same to the resource selectors of a policy exemption, which widens the exemption to every resource covered by its assignment.

I found this while working on #30204, which fixes the always-false EnforcementMode condition in the same merge logic. That one turned out to be harmless because the service keeps the stored enforcementMode when it is missing from the PUT, so it has no user impact. This one is a real bug with real impact: the service does not keep overrides and resource selectors, so they are actually lost.

The cmdlet merges your parameters into the existing assignment and PUTs it back, but it doesn't carry over Override and ResourceSelector, and the service clears both when they're missing from the PUT (checked with raw requests on 2025-03-01 and 2026-01-01-preview). As a result, the policy silently applies more widely or more strictly than intended.

The -Override and -ResourceSelector parameters were added to both cmdlets in #26031 (Az.Resources 7.5.0), after their merge logic had already been written, and the merge logic was never extended to cover them, so both cmdlets have been affected since then.

This PR carries them over the same way NotScope is carried over, i.e. only when the parameter is not specified, so that passing an empty collection (-Override @(), -ResourceSelector @()) still clears them like before. This mirrors what Update-AzPolicyEnrollment (added in #29909) already does for its own ResourceSelector:

if (!$calledParameters.ResourceSelector -and !($calledParameters.ResourceSelector -is [array]) -and $existing.ResourceSelector) {
    $calledParameters.ResourceSelector = $existing.ResourceSelector
}

Update-AzPolicyExemption now uses exactly that condition for its ResourceSelector.

Reproduction

The following script creates an audit-only assignment (DoNotEnforce) with one override and one resource selector, renames it, counts both properties before and after the rename, and deletes the assignment:

Details
param(
    # Az.Resources to test: a module name (the installed release) or the path to a local build's .psd1
    [string]$ModulePath = 'Az.Resources'
)

$ErrorActionPreference = 'Stop'
if (Test-Path $ModulePath) {
    # let the local build find the Az.Accounts it was built with
    $env:PSModulePath = (Split-Path (Split-Path (Resolve-Path $ModulePath))) + [IO.Path]::PathSeparator + $env:PSModulePath
}
Import-Module $ModulePath -Force -WarningAction SilentlyContinue
$module = Get-Module Az.Resources
"Az.Resources $($module.Version) from $($module.ModuleBase)"

$scope = "/subscriptions/$((Get-AzContext).Subscription.Id)"
$name = "repro-override-loss-$(Get-Random -Maximum 99999)"
# Built-in "Audit VMs that do not use managed disks" (audit only), assigned with DoNotEnforce
$definition = Get-AzPolicyDefinition -Id '/providers/Microsoft.Authorization/policyDefinitions/06a78e20-9358-41c9-923c-fb736d382a4d'

function Show([string]$Label) {
    $assignment = Get-AzPolicyAssignment -Name $name -Scope $scope
    '{0,-7} Override={1} ResourceSelector={2}' -f $Label,
        @($assignment.Override | Where-Object { $_ }).Count, @($assignment.ResourceSelector | Where-Object { $_ }).Count
}

try {
    $null = New-AzPolicyAssignment -Name $name -Scope $scope -PolicyDefinition $definition -EnforcementMode DoNotEnforce `
        -Override @(@{ Kind = 'policyEffect'; Value = 'Disabled'; Selector = @(@{ Kind = 'resourceLocation'; In = @('northeurope') }) }) `
        -ResourceSelector @(@{ Name = 'regions'; Selector = @(@{ Kind = 'resourceLocation'; In = @('northeurope', 'uksouth') }) })
    Show 'Before:'

    $null = Update-AzPolicyAssignment -Name $name -Scope $scope -DisplayName 'renamed'
    Show 'After:'
}
finally {
    Remove-AzPolicyAssignment -Name $name -Scope $scope -ErrorAction Continue | Out-Null
}

Output with the released module and with the module built from this branch (PowerShell 7.6 on Linux, each run in a fresh process):

$ pwsh -File repro-override-loss.ps1
Az.Resources 10.2.1 from /home/username/.local/share/powershell/Modules/Az.Resources/10.2.1
Before: Override=1 ResourceSelector=1
After:  Override=0 ResourceSelector=0

$ pwsh -File repro-override-loss.ps1 -ModulePath ./artifacts/Debug/Az.Resources/Az.Resources.psd1
Az.Resources 10.2.1 from /home/username/Code/Azure/azure-powershell/artifacts/Debug/Az.Resources
Before: Override=1 ResourceSelector=1
After:  Override=1 ResourceSelector=1

I could also reproduce it on an assignment I created from the Azure portal: after running Update-AzPolicyAssignment -DisplayName ... on it, both the Overrides and the Resource selectors sections were empty in the portal (Edit assignment > Basics). I also checked on a live subscription that -Override @() -ResourceSelector @() still clears both with this PR.

The same script adapted to exemptions (a Waiver exemption with a resource selector on that assignment, then Update-AzPolicyExemption -DisplayName 'renamed') gives ResourceSelector=1 → ResourceSelector=0 with 10.2.1 and ResourceSelector=1 → ResourceSelector=1 with this PR.

Tests

I added UpdatePolicyAssignmentOverridesSelectors.Tests.ps1. The existing AssignmentOverride and AssignmentResourceSelector tests are tagged LiveOnly so they never run in playback, and they only ever pass -Override/-ResourceSelector explicitly anyway, which is probably why this went unnoticed.

It covers four cases:

  1. Neither parameter is specified → the existing overrides and resource selectors are passed on (fails without this PR)
  2. Both parameters are specified → they take precedence over the existing values
  3. Empty collections are specified → they are passed on as empty, which clears them (fails if the -is [array] check is dropped)
  4. The assignment has neither and neither parameter is specified → nothing is passed

UpdatePolicyExemptionResourceSelector.Tests.ps1 covers the same four cases for Update-AzPolicyExemption and -ResourceSelector.

These two files mock Get-/New-AzPolicyAssignment and Get-/New-AzPolicyExemption, so they check the merge logic itself and need no recording.

I also added two recorded scenario tests, PolicyAssignmentOverridesSelectors.Tests.ps1 and PolicyExemptionResourceSelector.Tests.ps1, with their .Recording.json. They run the real cmdlets against the service, at subscription scope with a built-in audit policy assigned with DoNotEnforce:

  1. Create an assignment with one override and one resource selector (an exemption with one resource selector)
  2. Update an unrelated property by name, by Id and from the pipeline (by name and by Id for the exemption) → the overrides and resource selectors are still there, both in the response and in the following Get
  3. Specify new values → they replace the existing ones
  4. Specify empty collections → they are cleared

In the recordings, the PUT of the display-name-only update carries overrides and resourceSelectors, and the following GET returns them.

The full Policy suite passes through test-module.ps1 in playback on Linux (Pester 4.10.1): 922 passed, 0 failed, 2 skipped (both already skipped in the repo).

Mandatory Checklist

  • SHOULD update ChangeLog.md file(s) appropriately
    • Update src/{{SERVICE}}/{{SERVICE}}/ChangeLog.md.
      • A snippet outlining the change(s) made in the PR should be written under the ## Upcoming Release header in the past tense.
    • Should not change ChangeLog.md if no new release is required, such as fixing test case only.
  • SHOULD regenerate markdown help files if there is cmdlet API change. Instruction
  • SHOULD have proper test coverage for changes in pull request.
  • SHOULD NOT adjust version of module manually in pull request

…ssignment

Update-AzPolicyAssignment writes the whole assignment back with a PUT but
did not carry over the existing overrides and resource selectors. The
service clears both when they are omitted from the PUT, so updating an
unrelated property such as the display name silently removed them.

Carry them over the same way as NotScope: only when the parameter is not
specified, so passing an empty collection still clears them. Add mock-based
Pester tests for the merge; the existing Override and ResourceSelector
tests are tagged LiveOnly and never run in playback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 27, 2026 06:16
@azure-pipelines

Copy link
Copy Markdown
Contributor
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Thank you for your contribution precondition! We will review the pull request and get back to you soon.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Fix the -InputObject merge logic so explicitly supplied override and selector values are not replaced by existing values.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Updates Update-AzPolicyAssignment to preserve policy overrides and resource selectors during unrelated updates.

Changes:

  • Adds preservation and explicit-clearing merge logic.
  • Adds focused merge-behavior tests.
  • Documents the fix in the changelog.
File Summary
src/​Resources/​Resources/​ChangeLog.md Documents the bug fix.
src/​Resources/​Policy.Autorest/​test/​UpdatePolicyAssignmentOverridesSelectors.Tests.ps1 Tests preservation, precedence, and clearing behavior.
src/​Resources/​Policy.Autorest/​custom/​Update-AzPolicyAssignment.ps1 Adds preservation logic; an unresolved -InputObject merge issue affects explicit override and selector updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Resources/Policy.Autorest/custom/Update-AzPolicyAssignment.ps1 Outdated
…selectors

Test $calledParameters instead of the bound -Override/-ResourceSelector
parameters, as Update-AzPolicyEnrollment does for its ResourceSelector, so
values that end up in $calledParameters from any source take precedence
over the existing assignment. Behavior is unchanged for command-line use:
explicit values and empty collections are already in $calledParameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 27, 2026 06:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The fix is covered by focused tests and preserves both omitted and explicitly cleared values.

Review effort: Lite
Findings: None

Resolved since last review (1)

Update-AzPolicyExemption has the same problem as Update-AzPolicyAssignment:
it writes the whole exemption back with a PUT but did not carry over the
existing resource selectors, which the service clears when they are
omitted. Updating an unrelated property such as the display name therefore
removed them, widening the exemption to every resource of the assignment.

Carry them over like Update-AzPolicyEnrollment does, only when
-ResourceSelector is not specified, so an empty collection still clears
them. Add mock-based Pester tests for the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 27, 2026 06:53
@precondition precondition changed the title [Resources] Keep overrides and resource selectors in Update-AzPolicyAssignment [Resources] Keep overrides and resource selectors in Update-AzPolicyAssignment/Exemption Sep 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Explicit -Override $null and -ResourceSelector $null must retain their existing clear behavior.

Review effort: Lite
Findings: None

@precondition

precondition commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Copilot review overview

🔵 Needs a closer look

Explicit -Override $null and -ResourceSelector $null must retain their existing clear behavior.

Review effort: Lite Findings: None

The cmdlet deliberately ignores $null. In the released version, -Override $null builds exactly the same request as leaving the parameter out. It only "cleared" because leaving it out also cleared, which is the bug itself.

Furthermore, it makes the parameters consistent. In the same cmdlet, -NotScope $null already keeps the existing exclusions, since its carry-over uses the same !$X -and !($X -is [array]) check. Update-AzPolicyEnrollment -ResourceSelector $null also keeps. Before this PR, -Override $null was the odd one out.

The intended method for clearing the values is @().

Copilot AI lite review requested due to automatic review settings October 8, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI lite review requested due to automatic review settings October 10, 2026 08:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new regression tests fail on Linux due to non-portable paths, and the changelog needs alignment.

3 open findings

🧠 Review effort: Lite

Comment thread src/Resources/Resources/ChangeLog.md Outdated
The change was subsequently reverted in Azure#30272.
Copilot AI lite review requested due to automatic review settings October 10, 2026 10:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes address the reported data-loss bug with focused regression coverage.

0 open findings

3 resolved since last review

🧠 Review effort: Lite

Record Update-AzPolicyAssignment and Update-AzPolicyExemption keeping the
existing overrides and resource selectors when another property is updated,
replacing them when specified and clearing them with empty collections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 10, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved approval-blocking issues were identified.

0 open findings

🧠 Review effort: Lite

@precondition

precondition commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

NoriZC : since #30204 was reverted for lacking recorded tests in #30272, I added recorded tests here: PolicyAssignmentOverridesSelectors and PolicyExemptionResourceSelector, each with its .Recording.json.

Unlike #30204, this bug is visible on the service side: without the fix, the overrides and resource selectors are gone after an unrelated update. The recordings show them being sent in the PUT and returned by the following GET.

I recorded against my own subscription and replaced the subscription, tenant and user ids in the recordings with the ones from the committed env.json, so the existing recordings are untouched. The full Policy suite passes in playback (922 passed, 0 failed).

Separately: would you accept the EnforcementMode fix from #30204 again if I resubmit it with a recorded test as well?

@@ -0,0 +1,100 @@
# setup the Pester environment for policy tests

@NoriZC NoriZC Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added test hasn't been recorded. Please make sure all added/changed tests are recorded and can be palyback successfully

@NoriZC NoriZC self-assigned this Oct 19, 2026
@NoriZC NoriZC added the Request X Engineering Agent Ask the agent to triage this issue or review this PR label Oct 19, 2026
@x-engineering-agent

x-engineering-agent Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

X Engineering Agent Report

Review — Comments

precondition

Domain edge cases

P2 — Honor empty selectors on a typed exemption InputObject. In src/Resources/Policy.Autorest/custom/Update-AzPolicyExemption.ps1:279–280, the new fallback restores the live selectors when an input object's selectors were explicitly emptied. The public IPolicyExemption.ResourceSelector is a List<IResourceSelectorAutoGenerated>, not an array (custom/PolicyExemption.cs:30–38,51, under the same Policy.Autorest directory). An empty list is false in PowerShell and fails the [array]/[switch] checks in Update-AzPolicyExemption.ps1:221–227, so it never enters the merged parameters. The added fallback then replaces that clear request with $existing.ResourceSelector and forwards it through line 287 to New-AzPolicyExemption.ps1:178–183,219.

Please preserve explicit empty collection values from the typed input object before applying the fallback, normalizing this property to the accepted array type and distinguishing absent/null values from empty collections. Add a deterministic unit case using a real typed exemption with an assigned empty selector list, a nonempty mocked live exemption, and -InputObject; assert that the forwarded ResourceSelector is non-null and has zero elements. Also verify nonempty input values and explicit command-line precedence. The current unit clear case (test/UpdatePolicyExemptionResourceSelector.Tests.ps1:75–83) supplies -ResourceSelector @() directly, so it cannot expose this path. No live test is needed for this merge defect.

Existing feedback
  • 4114322487: resolved for the reported overwrite. Assignment lines 335 and 339 now inspect $calledParameters, honoring merged nonempty InputObject values rather than checking unbound local parameters. The typed empty-list case above is a separate edge case.
  • 4237007708 and 4237007719: incorrect. The current test scripts still use backslash child paths, but PowerShell Core's provider normalizes them on Linux. NavigationProviderBase.MakePath normalizes the child, and StringLiterals.AlternatePathSeparator is backslash on Unix; these are not literal single filename components.
  • 4237007732: resolved. The current Upcoming Release adds only the two preservation notes. It no longer claims an EnforcementMode fix; the unchanged always-false legacy condition is not a finding against this change.

Author-provided recordings demonstrate ordinary preserve, replace and explicit-array clear request/response mappings, including subsequent GETs. They do not cover the typed empty-list InputObject case. The handoff reports 12/12 upstream checks passed, not proof of all production scenarios. Generated internals are excluded from this repository; their execution was not independently verified.

Test validation
  • Agent live test: Not run; this is a review-only PR. Check author-provided test evidence and upstream CI separately.
  • Regression coverage: Not applicable to the changed files.
Risk assessment — 7/100 · Low

The Low rating reflects the detected change scope with no elevated security, reliability, customer, operational, dependency, sovereign-cloud, generated-output, or cross-component signal.

  • Change scope: 9 changed files, 2126 changed lines (+2126 / -0), including 2 production files.
  • Affected components: resources
  • Risk drivers: No elevated risk signal was detected.
  • Regression evidence: Changed regression tests are included, reducing risk.
  • Confidence: High because changed-line patches were available for every production file.
  • Required review: No additional owning-squad review signal was detected.

Posted by x-engineering-agent (Reviewer)

@NoriZC

NoriZC commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Separately: would you accept the EnforcementMode fix from #30204 again if I resubmit it with a recorded test as well?

Yes we will review the PR when it was raised agagin

@x-engineering-agent x-engineering-agent Bot added the X Engineering Agent Reviewed Agent reviewed this PR label Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

customer-reported Request X Engineering Agent Ask the agent to triage this issue or review this PR X Engineering Agent Reviewed Agent reviewed this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants