diff --git a/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs
new file mode 100644
index 000000000000..07a50a41a4da
--- /dev/null
+++ b/src/Accounts/Accounts.Test/ChangeSafetyParameterContractTests.cs
@@ -0,0 +1,64 @@
+// ----------------------------------------------------------------------------------
+//
+// 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.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..de5932dc565f
--- /dev/null
+++ b/src/Accounts/Accounts.Test/UnitTest/AcquirePolicyTokenHandlerTests.cs
@@ -0,0 +1,137 @@
+// ----------------------------------------------------------------------------------
+//
+// 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.Azure.Commands.Common.Authentication;
+using Microsoft.Azure.Commands.Common.Authentication.Models;
+using Microsoft.WindowsAzure.Commands.Common;
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+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
+ // AddChangeSafetyPolicyTokenHandler overload can be invoked directly from tests.
+ using PipelineStep = Func, Task>, Func, Task>, Task>, Task>;
+
+ public class AcquirePolicyTokenHandlerTests
+ {
+ public AcquirePolicyTokenHandlerTests()
+ {
+ // ContextAdapter's constructor reads AzureSession.Instance, so ensure a session exists.
+ 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.AddChangeSafetyPolicyTokenHandler(CreateInvocationInfo(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));
+ }
+ }
+}
diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md
index ad96ffcc67b4..5cd5252647e5 100644
--- a/src/Accounts/Accounts/ChangeLog.md
+++ b/src/Accounts/Accounts/ChangeLog.md
@@ -23,6 +23,7 @@
## Version 5.5.2
* 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`.
## 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..e7b6f7dde411 100644
--- a/src/Accounts/Accounts/CommonModule/ContextAdapter.cs
+++ b/src/Accounts/Accounts/CommonModule/ContextAdapter.cs
@@ -123,6 +123,43 @@ internal void AddAuthorizeRequestHandler(
});
}
+ ///
+ /// 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).
+ /// 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)
+ {
+ var boundParameters = invocationInfo?.BoundParameters;
+ 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)
+
+ var acquirer = new Microsoft.WindowsAzure.Commands.Common.PolicyTokenAcquirer();
+ appendStep(
+ async (request, cancelToken, cancelAction, signal, next) =>
+ {
+ await acquirer.StampPolicyTokenAsync(
+ request,
+ shouldAcquire: shouldAcquire,
+ changeReference: changeReference,
+ 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
///
diff --git a/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs b/src/Accounts/Accounts/CommonModule/RegisterAzModule.cs
index 6302d66a596b..7ab81597d9a6 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
+ 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 3470cd327e9e..ec41b94fdd69 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 ChangeSafetyPolicyTokenDelegate = 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 ChangeSafetyPolicyTokenDelegate AddChangeSafetyPolicyTokenHandler;
+
public SanitizerDelegate SanitizerHandler;
public GetTelemetryInfoDelegate GetTelemetryInfo;
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\