From 0fdae719250d956a5bb27b38785f9e11bc194444 Mon Sep 17 00:00:00 2001 From: Yang An Date: Tue, 21 Jul 2026 15:14:38 +1000 Subject: [PATCH 01/10] [Change Safety] Az.Accounts: AutoRest AcquirePolicyToken pipeline step (Stage A) ContextAdapter.OnNewRequest appends an AcquirePolicyToken SendAsyncStep that reads -AcquirePolicyToken / -ChangeReference from the cmdlet's BoundParameters and stamps the x-ms-policy-external-evaluations header via the shared PolicyTokenAcquirer. Guarded no-op when neither is set (whitespace ChangeReference treated as empty). Includes handler step-gating tests and a contract pin test for the parameter names/help text. Rebased onto current upstream/main; carries no dependency changes (common 1.3.113-preview is already pinned upstream). --- .../ChangeSafetyParameterContractTests.cs | 65 +++++++++ .../AcquirePolicyTokenHandlerTests.cs | 126 ++++++++++++++++++ src/Accounts/Accounts/ChangeLog.md | 1 + .../Accounts/CommonModule/ContextAdapter.cs | 49 +++++++ 4 files changed, 241 insertions(+) create mode 100644 src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs create mode 100644 src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs diff --git a/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs new file mode 100644 index 000000000000..bdbd57401117 --- /dev/null +++ b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs @@ -0,0 +1,65 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.ScenarioTest; +using Microsoft.WindowsAzure.Commands.Common; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using System.Linq; +using System.Management.Automation; +using Xunit; + +namespace Microsoft.Azure.Commands.Profile.Test +{ + /// + /// Pins the Change Safety parameter names and help text that Az.Accounts reads from the cmdlet's + /// BoundParameters (via the pipeline step in ). + /// + /// The AutoRest generator hardcodes these same literal strings when it emits the static + /// -AcquirePolicyToken / -ChangeReference parameters on write-verb cmdlets, and there is no + /// compile-time link between the generator (powershell/cmdlets/class.ts) and this library. If a name + /// or help message changes here, these tests fail as a reminder to update the generator to match and + /// regenerate the modules; otherwise the header would silently stop being stamped. + /// + public class ChangeSafetyParameterContractTests + { + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ParameterNamesMatchGeneratorLiterals() + { + Assert.Equal("AcquirePolicyToken", ChangeSafetyParameters.AcquirePolicyTokenParamName); + Assert.Equal("ChangeReference", ChangeSafetyParameters.ChangeReferenceParamName); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ParameterHelpTextMatchesGeneratorLiterals() + { + var dict = new RuntimeDefinedParameterDictionary(); + ChangeSafetyParameters.AddChangeSafetyParameters(dict); + + Assert.Equal( + "Acquire an Azure Policy token automatically for this resource operation.", + GetHelpMessage(dict, ChangeSafetyParameters.AcquirePolicyTokenParamName)); + Assert.Equal( + "The change reference resource ID for this resource operation.", + GetHelpMessage(dict, ChangeSafetyParameters.ChangeReferenceParamName)); + } + + private static string GetHelpMessage(RuntimeDefinedParameterDictionary dict, string name) + { + var attribute = dict[name].Attributes.OfType().First(); + return attribute.HelpMessage; + } + } +} diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs new file mode 100644 index 000000000000..57d022d09794 --- /dev/null +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -0,0 +1,126 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common; +using Microsoft.WindowsAzure.Commands.Common; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Azure.Commands.Profile.Test.UnitTest +{ + // Matches the PipelineChangeDelegate alias declared in ContextAdapter.cs so the internal + // AddAcquirePolicyTokenHandler overload can be invoked directly from tests. + using PipelineStep = Func, Task>, Func, Task>, Task>, Task>; + + public class AcquirePolicyTokenHandlerTests + { + private static int CountAppendedSteps(IDictionary boundParameters) + { + int appended = 0; + Action appendStep = _ => appended++; + ContextAdapter.AddAcquirePolicyTokenHandler(boundParameters, appendStep); + return appended; + } + + [Fact] + public void NoChangeSafetyParameters_DoesNotAppendStep() + { + var boundParameters = new Dictionary(); + Assert.Equal(0, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void NullBoundParameters_DoesNotAppendStep() + { + Assert.Equal(0, CountAppendedSteps(null)); + } + + [Fact] + public void AcquirePolicyTokenSwitchPresent_AppendsSingleStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.AcquirePolicyTokenParamName, new SwitchParameter(true) } + }; + Assert.Equal(1, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void AcquirePolicyTokenSwitchFalse_DoesNotAppendStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.AcquirePolicyTokenParamName, new SwitchParameter(false) } + }; + Assert.Equal(0, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void ChangeReferenceNonEmpty_AppendsSingleStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.ChangeReferenceParamName, "/subscriptions/change-ref" } + }; + Assert.Equal(1, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void ChangeReferenceEmpty_DoesNotAppendStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.ChangeReferenceParamName, string.Empty } + }; + Assert.Equal(0, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void ChangeReferenceWhitespace_DoesNotAppendStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.ChangeReferenceParamName, " " } + }; + Assert.Equal(0, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void BothAcquireSwitchAndChangeReference_AppendsSingleStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.AcquirePolicyTokenParamName, new SwitchParameter(true) }, + { ChangeSafetyParameters.ChangeReferenceParamName, "/subscriptions/change-ref" } + }; + Assert.Equal(1, CountAppendedSteps(boundParameters)); + } + + [Fact] + public void WhatIfWithAcquirePolicyToken_AppendsSingleStep() + { + var boundParameters = new Dictionary + { + { ChangeSafetyParameters.AcquirePolicyTokenParamName, new SwitchParameter(true) }, + { "WhatIf", new SwitchParameter(true) } + }; + Assert.Equal(1, CountAppendedSteps(boundParameters)); + } + } +} diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index c41d09c9268b..432671769201 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -21,6 +21,7 @@ ## Upcoming Release * Upgraded `Azure.Core` dependency from 1.56.0 to 1.57.0. * Upgraded `System.ClientModel` dependency from 1.12.0 to 1.13.0. +* Upgraded common library version. ## Version 5.5.1 * Upgraded `Azure.Core` dependency from 1.50.0 to 1.56.0. diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index e0490507c7b8..41025fded9fb 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -77,6 +77,7 @@ public void OnNewRequest(InvocationInfo invocationInfo, string correlationId, st prependStep(UniqueId.Instance.SendAsync); appendStep(new UserAgent(invocationInfo).SendAsync); appendStep(this.SendHandler(GetDefaultContext(_provider, invocationInfo), AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId)); + this.AddAcquirePolicyTokenHandler(invocationInfo, appendStep); } internal void AddRequestUserAgentHandler( @@ -123,6 +124,54 @@ internal void AddAuthorizeRequestHandler( }); } + /// + /// Conditionally appends a change safety step to the HTTP pipeline that acquires an Azure Policy + /// token and stamps it onto outgoing write requests. When the feature is off (neither + /// -AcquirePolicyToken nor -ChangeReference supplied) no pipeline step is added, so there is no + /// added cost for the common case. + /// + private void AddAcquirePolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) + { + AddAcquirePolicyTokenHandler(invocationInfo?.BoundParameters, appendStep); + } + + /// + /// Testable core of . + /// Evaluates the change safety bound parameters up front and only appends a pipeline step when the + /// feature is requested. The write-verb gate lives inside StampPolicyTokenAsync, so GET + /// sub-requests are skipped even though the step is added for the cmdlet. + /// + internal static void AddAcquirePolicyTokenHandler(IDictionary boundParameters, PipelineChangeDelegate appendStep) + { + if (boundParameters == null) { return; } + + bool acquire = boundParameters.TryGetValue(Microsoft.WindowsAzure.Commands.Common.ChangeSafetyParameters.AcquirePolicyTokenParamName, out var acquireVal) + && acquireVal is SwitchParameter sp && sp.ToBool(); + string changeReference = boundParameters.TryGetValue(Microsoft.WindowsAzure.Commands.Common.ChangeSafetyParameters.ChangeReferenceParamName, out var crVal) + ? crVal as string : null; + // Treat a whitespace-only change reference as not provided, so it doesn't trigger acquisition. + if (string.IsNullOrWhiteSpace(changeReference)) { changeReference = null; } + bool shouldAcquire = acquire || changeReference != null; + if (!shouldAcquire) { return; } // feature off -> no added pipeline step (zero cost) + + // Generated write cmdlets gate the HTTP call behind ShouldProcess, so under -WhatIf the write + // request (and therefore this pipeline step) is never sent. No WhatIf handling is needed here. + var acquirer = new Microsoft.WindowsAzure.Commands.Common.PolicyTokenAcquirer(); + appendStep( + async (request, cancelToken, cancelAction, signal, next) => + { + await acquirer.StampPolicyTokenAsync( + request, + shouldAcquire: shouldAcquire, + changeReference: changeReference, + isWhatIf: false, + debugMessages: null, + tokenHttpClient: null, + cancellationToken: cancelToken).ConfigureAwait(false); + return await next(request, cancelToken, cancelAction, signal).ConfigureAwait(false); + }); + } + /// /// Called for well-known parameters that require argument completers /// From 2d7e5e17cacdf9c3667b6957c670c9eb5dbb364f Mon Sep 17 00:00:00 2001 From: Yang An Date: Wed, 22 Jul 2026 10:54:44 +1000 Subject: [PATCH 02/10] [Change Safety] Bump common library to 1.3.114-preview (Stage A) Pins all Microsoft.Azure.PowerShell.* common packages to 1.3.114-preview, which brings in the shared change-safety PolicyTokenAcquirer used by the AutoRest AcquirePolicyToken pipeline step. --- src/Accounts/Accounts/ChangeLog.md | 2 +- tools/Common.Netcore.Dependencies.targets | 34 +++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 432671769201..fac087acdcd2 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -21,7 +21,7 @@ ## Upcoming Release * Upgraded `Azure.Core` dependency from 1.56.0 to 1.57.0. * Upgraded `System.ClientModel` dependency from 1.12.0 to 1.13.0. -* Upgraded common library version. +* Upgraded common library to `1.3.114-preview`, adding the shared change-safety `PolicyTokenAcquirer` that stamps the policy-external-evaluations header for the `-AcquirePolicyToken`/`-ChangeReference` pipeline step. ## Version 5.5.1 * Upgraded `Azure.Core` dependency from 1.50.0 to 1.56.0. diff --git a/tools/Common.Netcore.Dependencies.targets b/tools/Common.Netcore.Dependencies.targets index 42f6fa8ad9a5..4ae06c971e8b 100644 --- a/tools/Common.Netcore.Dependencies.targets +++ b/tools/Common.Netcore.Dependencies.targets @@ -3,22 +3,22 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -37,7 +37,7 @@ - $(NugetPackageRoot)\microsoft.azure.powershell.storage\1.3.113-preview\tools\ + $(NugetPackageRoot)\microsoft.azure.powershell.storage\1.3.114-preview\tools\ From 45a338ffd0a61a4f883953e2c9d220610d8cc557 Mon Sep 17 00:00:00 2001 From: Yang An Date: Wed, 22 Jul 2026 14:32:17 +1000 Subject: [PATCH 03/10] Remove stale isWhatIf argument to match common 1.3.114 (WhatIf removed) azure-powershell-common 1.3.114 (PR #454) removed the isWhatIf parameter from PolicyTokenAcquirer.StampPolicyTokenAsync. ContextAdapter.AddAcquirePolicyTokenHandler still passed isWhatIf: false, which fails to compile against the signed 1.3.114 on the feed (CI), breaking the Accounts module build and cascading into 'Unable to find type ResourceManagementClient' across all module tests. Removes the isWhatIf argument and the now-moot WhatIf unit test. --- .../UnitTest/AcquirePolicyTokenHandlerTests.cs | 11 ----------- src/Accounts/Accounts/CommonModule/ContextAdapter.cs | 1 - 2 files changed, 12 deletions(-) diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs index 57d022d09794..1aaaa9b3a7f0 100644 --- a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -111,16 +111,5 @@ public void BothAcquireSwitchAndChangeReference_AppendsSingleStep() }; Assert.Equal(1, CountAppendedSteps(boundParameters)); } - - [Fact] - public void WhatIfWithAcquirePolicyToken_AppendsSingleStep() - { - var boundParameters = new Dictionary - { - { ChangeSafetyParameters.AcquirePolicyTokenParamName, new SwitchParameter(true) }, - { "WhatIf", new SwitchParameter(true) } - }; - Assert.Equal(1, CountAppendedSteps(boundParameters)); - } } } diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index 41025fded9fb..e2d0e02a98de 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -164,7 +164,6 @@ await acquirer.StampPolicyTokenAsync( request, shouldAcquire: shouldAcquire, changeReference: changeReference, - isWhatIf: false, debugMessages: null, tokenHttpClient: null, cancellationToken: cancelToken).ConfigureAwait(false); From b09b305037d694a61f03b305a560c7254a282be6 Mon Sep 17 00:00:00 2001 From: Yang An Date: Wed, 22 Jul 2026 16:09:11 +1000 Subject: [PATCH 04/10] Trim ChangeLog: drop non-customer-facing change-safety detail --- src/Accounts/Accounts/ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index fac087acdcd2..e70b17b836c8 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -21,7 +21,7 @@ ## Upcoming Release * Upgraded `Azure.Core` dependency from 1.56.0 to 1.57.0. * Upgraded `System.ClientModel` dependency from 1.12.0 to 1.13.0. -* Upgraded common library to `1.3.114-preview`, adding the shared change-safety `PolicyTokenAcquirer` that stamps the policy-external-evaluations header for the `-AcquirePolicyToken`/`-ChangeReference` pipeline step. +* Upgraded common library to `1.3.114-preview`. ## Version 5.5.1 * Upgraded `Azure.Core` dependency from 1.50.0 to 1.56.0. From 8281eabe3dcfbbefa8020396f68213dd9708125a Mon Sep 17 00:00:00 2001 From: Yang An Date: Wed, 22 Jul 2026 21:46:24 +1000 Subject: [PATCH 05/10] Remove unused using to fix CS8019 (warnings-as-errors) --- src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs index bdbd57401117..07a50a41a4da 100644 --- a/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs +++ b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs @@ -12,7 +12,6 @@ // limitations under the License. // ---------------------------------------------------------------------------------- -using Microsoft.Azure.Commands.ScenarioTest; using Microsoft.WindowsAzure.Commands.Common; using Microsoft.WindowsAzure.Commands.ScenarioTest; using System.Linq; From 3c24ae44c6fb5ff36351cd14183c314490dfb732 Mon Sep 17 00:00:00 2001 From: Yang An Date: Fri, 31 Jul 2026 15:28:18 +1000 Subject: [PATCH 06/10] Change Safety: extract PolicyTokenHandler VTable delegate (Stage A refactor) Policy-token logic is its own VTable delegate (PolicyTokenHandler, dedicated AcquirePolicyTokenDelegate type) invoked by the generated module right after OnNewRequest -- not composed into it. Removes the InvocationInfo wrapper overload of AddAcquirePolicyTokenHandler and makes the core an instance method, called by PolicyTokenHandler. --- .../UnitTest/AcquirePolicyTokenHandlerTests.cs | 9 ++++++++- .../Accounts/CommonModule/ContextAdapter.cs | 16 +++++++--------- .../Accounts/CommonModule/RegisterAzModule.cs | 3 +++ src/Accounts/Accounts/CommonModule/VTable.cs | 7 +++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs index 1aaaa9b3a7f0..2c01c6a1be5e 100644 --- a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -13,6 +13,7 @@ // ---------------------------------------------------------------------------------- using Microsoft.Azure.Commands.Common; +using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.WindowsAzure.Commands.Common; using System; using System.Collections.Generic; @@ -30,11 +31,17 @@ namespace Microsoft.Azure.Commands.Profile.Test.UnitTest public class AcquirePolicyTokenHandlerTests { + public AcquirePolicyTokenHandlerTests() + { + // ContextAdapter's constructor reads AzureSession.Instance, so ensure a session exists. + AzureSessionInitializer.CreateOrReplaceSession(new MemoryDataStore()); + } + private static int CountAppendedSteps(IDictionary boundParameters) { int appended = 0; Action appendStep = _ => appended++; - ContextAdapter.AddAcquirePolicyTokenHandler(boundParameters, appendStep); + ContextAdapter.Instance.AddAcquirePolicyTokenHandler(boundParameters, appendStep); return appended; } diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index e2d0e02a98de..4d2c7c157776 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -77,7 +77,6 @@ public void OnNewRequest(InvocationInfo invocationInfo, string correlationId, st prependStep(UniqueId.Instance.SendAsync); appendStep(new UserAgent(invocationInfo).SendAsync); appendStep(this.SendHandler(GetDefaultContext(_provider, invocationInfo), AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId)); - this.AddAcquirePolicyTokenHandler(invocationInfo, appendStep); } internal void AddRequestUserAgentHandler( @@ -125,23 +124,22 @@ internal void AddAuthorizeRequestHandler( } /// - /// Conditionally appends a change safety step to the HTTP pipeline that acquires an Azure Policy - /// token and stamps it onto outgoing write requests. When the feature is off (neither - /// -AcquirePolicyToken nor -ChangeReference supplied) no pipeline step is added, so there is no - /// added cost for the common case. + /// Change safety pipeline hook, exposed as its own VTable delegate and invoked by the generated + /// module right after OnNewRequest. Conditionally appends a step that acquires an Azure Policy + /// token and stamps it onto outgoing write requests, based on the -AcquirePolicyToken / + /// -ChangeReference bound parameters. When the feature is off, no step is added (zero added cost). /// - private void AddAcquirePolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) + internal void PolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) { - AddAcquirePolicyTokenHandler(invocationInfo?.BoundParameters, appendStep); + this.AddAcquirePolicyTokenHandler(invocationInfo?.BoundParameters, appendStep); } /// - /// Testable core of . /// Evaluates the change safety bound parameters up front and only appends a pipeline step when the /// feature is requested. The write-verb gate lives inside StampPolicyTokenAsync, so GET /// sub-requests are skipped even though the step is added for the cmdlet. /// - internal static void AddAcquirePolicyTokenHandler(IDictionary boundParameters, PipelineChangeDelegate appendStep) + internal void AddAcquirePolicyTokenHandler(IDictionary boundParameters, PipelineChangeDelegate appendStep) { if (boundParameters == null) { return; } diff --git a/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs b/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs index 6302d66a596b..f4a696e1a6e6 100644 --- a/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs +++ b/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs @@ -74,6 +74,9 @@ protected override void ProcessRecord() AddAuthorizeRequestHandler = ContextAdapter.Instance.AddAuthorizeRequestHandler, + // change safety policy-token step; the generated module invokes this after OnNewRequest + PolicyTokenHandler = ContextAdapter.Instance.PolicyTokenHandler, + // Called for well-known parameters that require argument completers ArgumentCompleter = ContextAdapter.Instance.CompleteArgument, diff --git a/src/Accounts/Accounts/CommonModule/VTable.cs b/src/Accounts/Accounts/CommonModule/VTable.cs index 3470cd327e9e..844ae00ecd8f 100644 --- a/src/Accounts/Accounts/CommonModule/VTable.cs +++ b/src/Accounts/Accounts/CommonModule/VTable.cs @@ -25,6 +25,7 @@ namespace Microsoft.Azure.Commands.Common using GetTelemetryIdDelegate = Func; using ModuleLoadPipelineDelegate = Action, Task>, Func, Task>, Task>, Task>>, Action, Task>, Func, Task>, Task>, Task>>>; using NewRequestPipelineDelegate = Action, Task>, Func, Task>, Task>, Task>>, Action, Task>, Func, Task>, Task>, Task>>>; + using AcquirePolicyTokenDelegate = Action, Task>, Func, Task>, Task>, Task>>>; using ArgumentCompleterDelegate = Func; using AuthorizeRequestDelegate = global::System.Action + /// Called by the generated module after OnNewRequest to conditionally add the change safety + /// policy-token step, based on the -AcquirePolicyToken / -ChangeReference bound parameters. + /// + public AcquirePolicyTokenDelegate PolicyTokenHandler; + public SanitizerDelegate SanitizerHandler; public GetTelemetryInfoDelegate GetTelemetryInfo; From b75c357685699acb70d0c7d4bd431f9f8454c76f Mon Sep 17 00:00:00 2001 From: Yang An Date: Mon, 3 Aug 2026 13:11:16 +1000 Subject: [PATCH 07/10] Rename VTable PolicyTokenHandler to AddChangeSafetyPolicyTokenHandler Match the generated module VTable slot rename and the Add...Handler naming convention. Delegate alias AcquirePolicyTokenDelegate -> ChangeSafetyPolicyTokenDelegate; ContextAdapter wrapper method renamed to match. Also add missing using for MemoryDataStore in the change-safety unit test. --- .../Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs | 1 + src/Accounts/Accounts/CommonModule/ContextAdapter.cs | 2 +- src/Accounts/Accounts/CommonModule/RegisterAzModule.cs | 2 +- src/Accounts/Accounts/CommonModule/VTable.cs | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs index 2c01c6a1be5e..79edbbe30f2d 100644 --- a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -14,6 +14,7 @@ using Microsoft.Azure.Commands.Common; using Microsoft.Azure.Commands.Common.Authentication; +using Microsoft.Azure.Commands.Common.Authentication.Models; using Microsoft.WindowsAzure.Commands.Common; using System; using System.Collections.Generic; diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index 4d2c7c157776..dabf5ba0a50f 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -129,7 +129,7 @@ internal void AddAuthorizeRequestHandler( /// token and stamps it onto outgoing write requests, based on the -AcquirePolicyToken / /// -ChangeReference bound parameters. When the feature is off, no step is added (zero added cost). /// - internal void PolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) + internal void AddChangeSafetyPolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) { this.AddAcquirePolicyTokenHandler(invocationInfo?.BoundParameters, appendStep); } diff --git a/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs b/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs index f4a696e1a6e6..7ab81597d9a6 100644 --- a/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs +++ b/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs @@ -75,7 +75,7 @@ protected override void ProcessRecord() AddAuthorizeRequestHandler = ContextAdapter.Instance.AddAuthorizeRequestHandler, // change safety policy-token step; the generated module invokes this after OnNewRequest - PolicyTokenHandler = ContextAdapter.Instance.PolicyTokenHandler, + AddChangeSafetyPolicyTokenHandler = ContextAdapter.Instance.AddChangeSafetyPolicyTokenHandler, // Called for well-known parameters that require argument completers ArgumentCompleter = ContextAdapter.Instance.CompleteArgument, diff --git a/src/Accounts/Accounts/CommonModule/VTable.cs b/src/Accounts/Accounts/CommonModule/VTable.cs index 844ae00ecd8f..ec41b94fdd69 100644 --- a/src/Accounts/Accounts/CommonModule/VTable.cs +++ b/src/Accounts/Accounts/CommonModule/VTable.cs @@ -25,7 +25,7 @@ namespace Microsoft.Azure.Commands.Common using GetTelemetryIdDelegate = Func; using ModuleLoadPipelineDelegate = Action, Task>, Func, Task>, Task>, Task>>, Action, Task>, Func, Task>, Task>, Task>>>; using NewRequestPipelineDelegate = Action, Task>, Func, Task>, Task>, Task>>, Action, Task>, Func, Task>, Task>, Task>>>; - using AcquirePolicyTokenDelegate = Action, Task>, Func, Task>, Task>, Task>>>; + using ChangeSafetyPolicyTokenDelegate = Action, Task>, Func, Task>, Task>, Task>>>; using ArgumentCompleterDelegate = Func; using AuthorizeRequestDelegate = global::System.Action - public AcquirePolicyTokenDelegate PolicyTokenHandler; + public ChangeSafetyPolicyTokenDelegate AddChangeSafetyPolicyTokenHandler; public SanitizerDelegate SanitizerHandler; From 389875e573ce48473dcc667f65867366460c881b Mon Sep 17 00:00:00 2001 From: Yang An Date: Mon, 3 Aug 2026 13:17:25 +1000 Subject: [PATCH 08/10] Remove stale WhatIf comment in change-safety handler --- src/Accounts/Accounts/CommonModule/ContextAdapter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index dabf5ba0a50f..bc4ee7641c84 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -152,8 +152,6 @@ internal void AddAcquirePolicyTokenHandler(IDictionary boundPara bool shouldAcquire = acquire || changeReference != null; if (!shouldAcquire) { return; } // feature off -> no added pipeline step (zero cost) - // Generated write cmdlets gate the HTTP call behind ShouldProcess, so under -WhatIf the write - // request (and therefore this pipeline step) is never sent. No WhatIf handling is needed here. var acquirer = new Microsoft.WindowsAzure.Commands.Common.PolicyTokenAcquirer(); appendStep( async (request, cancelToken, cancelAction, signal, next) => From c613ed7a0a2f5cd38ec24a9e5c1548ece40cfe76 Mon Sep 17 00:00:00 2001 From: Yang An Date: Tue, 11 Aug 2026 10:37:24 +1000 Subject: [PATCH 09/10] Inline change-safety policy-token handler and drop redundant wrapper Merge AddChangeSafetyPolicyTokenHandler and AddAcquirePolicyTokenHandler into a single InvocationInfo-based method, and update unit tests to construct an InvocationInfo via its non-public BoundParameters setter. --- .../AcquirePolicyTokenHandlerTests.cs | 19 +++++++++++++++++-- .../Accounts/CommonModule/ContextAdapter.cs | 13 +++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs index 79edbbe30f2d..8d9d4b4ae9b1 100644 --- a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -20,6 +20,8 @@ using System.Collections.Generic; using System.Management.Automation; using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -27,7 +29,7 @@ namespace Microsoft.Azure.Commands.Profile.Test.UnitTest { // Matches the PipelineChangeDelegate alias declared in ContextAdapter.cs so the internal - // AddAcquirePolicyTokenHandler overload can be invoked directly from tests. + // AddChangeSafetyPolicyTokenHandler overload can be invoked directly from tests. using PipelineStep = Func, Task>, Func, Task>, Task>, Task>; public class AcquirePolicyTokenHandlerTests @@ -38,11 +40,24 @@ public AcquirePolicyTokenHandlerTests() AzureSessionInitializer.CreateOrReplaceSession(new MemoryDataStore()); } + // InvocationInfo has no public constructor, so build an uninitialized instance and set its + // BoundParameters via the non-public setter to exercise the handler's parameter evaluation. + private static InvocationInfo CreateInvocationInfo(IDictionary boundParameters) + { + var invocationInfo = (InvocationInfo)RuntimeHelpers.GetUninitializedObject(typeof(InvocationInfo)); + if (boundParameters != null) + { + var setter = typeof(InvocationInfo).GetProperty(nameof(InvocationInfo.BoundParameters)).GetSetMethod(nonPublic: true); + setter.Invoke(invocationInfo, new object[] { new Dictionary(boundParameters) }); + } + return invocationInfo; + } + private static int CountAppendedSteps(IDictionary boundParameters) { int appended = 0; Action appendStep = _ => appended++; - ContextAdapter.Instance.AddAcquirePolicyTokenHandler(boundParameters, appendStep); + ContextAdapter.Instance.AddChangeSafetyPolicyTokenHandler(CreateInvocationInfo(boundParameters), appendStep); return appended; } diff --git a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs index bc4ee7641c84..e7b6f7dde411 100644 --- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs +++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs @@ -128,19 +128,12 @@ internal void AddAuthorizeRequestHandler( /// module right after OnNewRequest. Conditionally appends a step that acquires an Azure Policy /// token and stamps it onto outgoing write requests, based on the -AcquirePolicyToken / /// -ChangeReference bound parameters. When the feature is off, no step is added (zero added cost). + /// The write-verb gate lives inside StampPolicyTokenAsync, so GET sub-requests are skipped + /// even though the step is added for the cmdlet. /// internal void AddChangeSafetyPolicyTokenHandler(InvocationInfo invocationInfo, PipelineChangeDelegate appendStep) { - this.AddAcquirePolicyTokenHandler(invocationInfo?.BoundParameters, appendStep); - } - - /// - /// Evaluates the change safety bound parameters up front and only appends a pipeline step when the - /// feature is requested. The write-verb gate lives inside StampPolicyTokenAsync, so GET - /// sub-requests are skipped even though the step is added for the cmdlet. - /// - internal void AddAcquirePolicyTokenHandler(IDictionary boundParameters, PipelineChangeDelegate appendStep) - { + var boundParameters = invocationInfo?.BoundParameters; if (boundParameters == null) { return; } bool acquire = boundParameters.TryGetValue(Microsoft.WindowsAzure.Commands.Common.ChangeSafetyParameters.AcquirePolicyTokenParamName, out var acquireVal) From 49291a37c846bd064fafd998f8b6237e22a6ae4b Mon Sep 17 00:00:00 2001 From: Yang An Date: Tue, 11 Aug 2026 10:46:33 +1000 Subject: [PATCH 10/10] Remove unused System.Reflection using in AcquirePolicyTokenHandlerTests --- .../Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs index 8d9d4b4ae9b1..de5932dc565f 100644 --- a/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs +++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs @@ -20,7 +20,6 @@ using System.Collections.Generic; using System.Management.Automation; using System.Net.Http; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks;