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\