From ab18f52e987d301a5ba433cd025e690e00b5cdf9 Mon Sep 17 00:00:00 2001 From: Levi B Date: Tue, 24 Feb 2015 17:48:29 -0800 Subject: [PATCH] Add CreateProtector convenience extension method --- .../DataProtectionExtensions.cs | 33 +++++++++++++++++ .../Properties/Resources.Designer.cs | 16 ++++++++ .../Resources.resx | 3 ++ .../DataProtectionExtensionsTests.cs | 37 +++++++++++++++++++ .../ExceptionHelpers.cs | 20 ++++++++++ 5 files changed, 109 insertions(+) create mode 100644 test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs index 1e84b49be4..5178eb7bc1 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; +using Microsoft.AspNet.Cryptography; namespace Microsoft.AspNet.Security.DataProtection { @@ -21,6 +23,37 @@ namespace Microsoft.AspNet.Security.DataProtection ?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString)); } + /// + /// Creates an IDataProtector given an array of purposes. + /// + /// The provider from which to generate the purpose chain. + /// + /// This is a convenience method used for chaining several purposes together + /// in a single call to CreateProtector. See the documentation of + /// IDataProtectionProvider.CreateProtector for more information. + /// + /// An IDataProtector tied to the provided purpose chain. + public static IDataProtector CreateProtector([NotNull] this IDataProtectionProvider provider, params string[] purposes) + { + if (purposes == null || purposes.Length == 0) + { + throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes)); + } + + IDataProtectionProvider retVal = provider; + foreach (string purpose in purposes) + { + if (String.IsNullOrEmpty(purpose)) + { + throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes)); + } + retVal = retVal.CreateProtector(purpose) ?? CryptoUtil.Fail("CreateProtector returned null."); + } + + Debug.Assert(retVal is IDataProtector); // CreateProtector is supposed to return an instance of this interface + return (IDataProtector)retVal; + } + /// /// Cryptographically protects a piece of plaintext data. /// diff --git a/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs index 35f9a8dc33..ae6746ee91 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs @@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Security.DataProtection return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0); } + /// + /// The purposes array cannot be null or empty and cannot contain null or empty elements. + /// + internal static string DataProtectionExtensions_NullPurposesArray + { + get { return GetString("DataProtectionExtensions_NullPurposesArray"); } + } + + /// + /// The purposes array cannot be null or empty and cannot contain null or empty elements. + /// + internal static string FormatDataProtectionExtensions_NullPurposesArray() + { + return GetString("DataProtectionExtensions_NullPurposesArray"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx index 044df24e82..3db16f062c 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx @@ -150,4 +150,7 @@ The payload expired at {0}. + + The purposes array cannot be null or empty and cannot contain null or empty elements. + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs index 6993f4ab9b..bccbafeb38 100644 --- a/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs @@ -39,6 +39,43 @@ namespace Microsoft.AspNet.Security.DataProtection.Test Assert.Same(innerProtector, timeLimitedProtector.InnerProtector); } + [Theory] + [InlineData(new object[] { null })] + [InlineData(new object[] { new string[0] })] + [InlineData(new object[] { new string[] { null } })] + [InlineData(new object[] { new string[] { "the next value is bad", "" } })] + public void CreateProtector_Chained_FailureCases(string[] purposes) + { + // Arrange + var mockProtector = new Mock(); + mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object); + var provider = mockProtector.Object; + + // Act & assert + var ex = Assert.Throws(() => provider.CreateProtector(purposes)); + ex.AssertMessage("purposes", Resources.DataProtectionExtensions_NullPurposesArray); + } + + [Fact] + public void CreateProtector_Chained_SuccessCase() + { + // Arrange + var finalExpectedProtector = new Mock().Object; + + var thirdMock = new Mock(); + thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector); + var secondMock = new Mock(); + secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object); + var firstMock = new Mock(); + firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object); + + // Act + var retVal = firstMock.Object.CreateProtector("first", "second", "third"); + + // Assert + Assert.Same(finalExpectedProtector, retVal); + } + [Fact] public void Protect_InvalidUtf_Failure() { diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs new file mode 100644 index 0000000000..e4394cbc9b --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNet.Security.DataProtection.Test +{ + internal static class ExceptionHelpers + { + public static void AssertMessage(this ArgumentException exception, string parameterName, string message) + { + Assert.Equal(parameterName, exception.ParamName); + + // We'll let ArgumentException handle the message formatting for us and treat it as our control value + var controlException = new ArgumentException(message, parameterName); + Assert.Equal(controlException.Message, exception.Message); + } + } +}