Continued API improvements and refactoring

- Add helpful extension methods to Interfaces project
- Auto heuristic detection now writes default protection settings to the ILogger
- Cleanup dead methods / add useful methods in DataProtectionConfiguration
- Update System.Web compatibility project to allow mapping MachineKey.Protect directly to IDataProtector.Protect
This commit is contained in:
Levi B 2015-03-12 17:54:15 -07:00
parent d3313f2b6e
commit 82d92064c5
12 changed files with 446 additions and 51 deletions

View File

@ -3,10 +3,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.AspNet.DataProtection.Interfaces;
using Microsoft.Framework.Internal;
#if DNX451 || DNXCORE50 // [[ISSUE1400]] Replace with DNX_ANY when it becomes available
using Microsoft.Framework.Runtime;
#endif
namespace Microsoft.AspNet.DataProtection
{
/// <summary>
@ -53,7 +58,7 @@ namespace Microsoft.AspNet.DataProtection
/// Creates an <see cref="IDataProtector"/> given a list of purposes.
/// </summary>
/// <param name="provider">The <see cref="IDataProtectionProvider"/> from which to generate the purpose chain.</param>
/// <param name="purpose">The primary purpose used to create the <see cref="IDataProtectionProvider"/>.</param>
/// <param name="purpose">The primary purpose used to create the <see cref="IDataProtector"/>.</param>
/// <param name="subPurposes">An optional list of secondary purposes which contribute to the purpose chain.
/// If this list is provided it cannot contain null elements.</param>
/// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
@ -75,7 +80,93 @@ namespace Microsoft.AspNet.DataProtection
}
return protector ?? CryptoUtil.Fail<IDataProtector>("CreateProtector returned null.");
}
/// <summary>
/// Returns a unique identifier for this application.
/// </summary>
/// <param name="services">The application-level <see cref="IServiceProvider"/>.</param>
/// <returns>A unique application identifier, or null if <paramref name="services"/> is null
/// or cannot provide a unique application identifier.</returns>
/// <remarks>
/// The returned identifier should be stable for repeated runs of this same application on
/// this machine. Additionally, the identifier is only unique within the scope of a single
/// machine, e.g., two different applications on two different machines may return the same
/// value.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetApplicationUniqueIdentifier(this IServiceProvider services)
{
string discriminator = (services?.GetService(typeof(IApplicationDiscriminator)) as IApplicationDiscriminator)?.Discriminator;
#if DNX451 || DNXCORE50 // [[ISSUE1400]] Replace with DNX_ANY when it becomes available
if (discriminator == null)
{
discriminator = (services?.GetService(typeof(IApplicationEnvironment)) as IApplicationEnvironment)?.ApplicationBasePath;
}
#elif NET451 // do nothing
#else
#error A new target framework was added to project.json, but it's not accounted for in this #ifdef. Please change the #ifdef accordingly.
#endif
// Remove whitespace and homogenize empty -> null
discriminator = discriminator?.Trim();
return (String.IsNullOrEmpty(discriminator)) ? null : discriminator;
}
/// <summary>
/// Retrieves an <see cref="IDataProtectionProvider"/> from an <see cref="IServiceProvider"/>.
/// </summary>
/// <param name="services">The service provider from which to retrieve the <see cref="IDataProtectionProvider"/>.</param>
/// <returns>An <see cref="IDataProtectionProvider"/>. This method is guaranteed never to return null.</returns>
/// <exception cref="InvalidOperationException">If no <see cref="IDataProtectionProvider"/> service exists in <paramref name="services"/>.</exception>
public static IDataProtectionProvider GetDataProtectionProvider([NotNull] this IServiceProvider services)
{
// We have our own implementation of GetRequiredService<T> since we don't want to
// take a dependency on DependencyInjection.Interfaces.
IDataProtectionProvider provider = (IDataProtectionProvider)services.GetService(typeof(IDataProtectionProvider));
if (provider == null)
{
throw new InvalidOperationException(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName));
}
return provider;
}
/// <summary>
/// Retrieves an <see cref="IDataProtector"/> from an <see cref="IServiceProvider"/> given a list of purposes.
/// </summary>
/// <param name="services">An <see cref="IServiceProvider"/> which contains the <see cref="IDataProtectionProvider"/>
/// from which to generate the purpose chain.</param>
/// <param name="purposes">The list of purposes which contribute to the purpose chain. This list must
/// contain at least one element, and it may not contain null elements.</param>
/// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
/// <remarks>
/// This is a convenience method which calls <see cref="GetDataProtectionProvider(IServiceProvider)"/>
/// then <see cref="CreateProtector(IDataProtectionProvider, IEnumerable{string})"/>. See those methods'
/// documentation for more information.
/// </remarks>
public static IDataProtector GetDataProtector([NotNull] this IServiceProvider services, [NotNull] IEnumerable<string> purposes)
{
return services.GetDataProtectionProvider().CreateProtector(purposes);
}
/// <summary>
/// Retrieves an <see cref="IDataProtector"/> from an <see cref="IServiceProvider"/> given a list of purposes.
/// </summary>
/// <param name="services">An <see cref="IServiceProvider"/> which contains the <see cref="IDataProtectionProvider"/>
/// from which to generate the purpose chain.</param>
/// <param name="purpose">The primary purpose used to create the <see cref="IDataProtector"/>.</param>
/// <param name="subPurposes">An optional list of secondary purposes which contribute to the purpose chain.
/// If this list is provided it cannot contain null elements.</param>
/// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
/// <remarks>
/// This is a convenience method which calls <see cref="GetDataProtectionProvider(IServiceProvider)"/>
/// then <see cref="CreateProtector(IDataProtectionProvider, string, string[])"/>. See those methods'
/// documentation for more information.
/// </remarks>
public static IDataProtector GetDataProtector([NotNull] this IServiceProvider services, [NotNull] string purpose, params string[] subPurposes)
{
return services.GetDataProtectionProvider().CreateProtector(purpose, subPurposes);
}
/// <summary>
/// Cryptographically protects a piece of plaintext data.
/// </summary>

View File

@ -58,6 +58,22 @@ namespace Microsoft.AspNet.DataProtection.Interfaces
return GetString("CryptCommon_GenericError");
}
/// <summary>
/// No service for type '{0}' has been registered.
/// </summary>
internal static string DataProtectionExtensions_NoService
{
get { return GetString("DataProtectionExtensions_NoService"); }
}
/// <summary>
/// No service for type '{0}' has been registered.
/// </summary>
internal static string FormatDataProtectionExtensions_NoService(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DataProtectionExtensions_NoService"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -126,4 +126,7 @@
<data name="CryptCommon_GenericError" xml:space="preserve">
<value>An error occurred during a cryptographic operation.</value>
</data>
<data name="DataProtectionExtensions_NoService" xml:space="preserve">
<value>No service for type '{0}' has been registered.</value>
</data>
</root>

View File

@ -7,9 +7,15 @@
},
"frameworks": {
"net451": { },
"dnx451": { },
"dnx451": {
"dependencies": {
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*"
}
},
"dnxcore50": {
"dependencies": {
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*",
"System.ComponentModel": "4.0.0-beta-*",
"System.Diagnostics.Debug": "4.0.10-beta-*",
"System.Reflection": "4.0.10-beta-*",
"System.Resources.ResourceManager": "4.0.0-beta-*",

View File

@ -18,7 +18,11 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
{
private static readonly Lazy<IDataProtectionProvider> _lazyProtectionProvider = new Lazy<IDataProtectionProvider>(CreateProtectionProvider);
[ThreadStatic]
private static bool _suppressPrimaryPurpose;
private readonly Lazy<IDataProtector> _lazyProtector;
private readonly Lazy<IDataProtector> _lazyProtectorSuppressedPrimaryPurpose;
public CompatibilityDataProtector(string applicationName, string primaryPurpose, string[] specificPurposes)
: base("application-name", "primary-purpose", null) // we feed dummy values to the base ctor
@ -28,11 +32,27 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
// up a good error message to the developer.
_lazyProtector = new Lazy<IDataProtector>(() => _lazyProtectionProvider.Value.CreateProtector(primaryPurpose, specificPurposes));
// System.Web always provides "User.MachineKey.Protect" as the primary purpose for calls
// to MachineKey.Protect. Only in this case should we allow suppressing the primary
// purpose, as then we can easily map calls to MachineKey.Protect(userData, purposes)
// into calls to provider.GetProtector(purposes).Protect(userData).
if (primaryPurpose == "User.MachineKey.Protect")
{
_lazyProtectorSuppressedPrimaryPurpose = new Lazy<IDataProtector>(() => _lazyProtectionProvider.Value.CreateProtector(specificPurposes));
}
else
{
_lazyProtectorSuppressedPrimaryPurpose = _lazyProtector;
}
}
// We take care of flowing purposes ourselves.
protected override bool PrependHashedPurposeToPlaintext { get; } = false;
// Retrieves the appropriate protector (potentially with a suppressed primary purpose) for this operation.
private IDataProtector Protector => ((_suppressPrimaryPurpose) ? _lazyProtectorSuppressedPrimaryPurpose : _lazyProtector).Value;
private static IDataProtectionProvider CreateProtectionProvider()
{
// Read from <appSettings> the startup type we need to use, then create it
@ -60,7 +80,7 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
{
try
{
return _lazyProtector.Value.Protect(userData);
return Protector.Protect(userData);
}
catch (Exception ex)
{
@ -76,7 +96,38 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
protected override byte[] ProviderUnprotect(byte[] encryptedData)
{
return _lazyProtector.Value.Unprotect(encryptedData);
return Protector.Unprotect(encryptedData);
}
/// <summary>
/// Invokes a delegate where calls to <see cref="ProviderProtect(byte[])"/>
/// and <see cref="ProviderUnprotect(byte[])"/> will ignore the primary
/// purpose and instead use only the sub-purposes.
/// </summary>
public static byte[] RunWithSuppressedPrimaryPurpose(Func<object, byte[], byte[]> callback, object state, byte[] input)
{
if (_suppressPrimaryPurpose)
{
return callback(state, input); // already suppressed - just forward call
}
try
{
try
{
_suppressPrimaryPurpose = true;
return callback(state, input);
}
finally
{
_suppressPrimaryPurpose = false;
}
}
catch
{
// defeat exception filters
throw;
}
}
}
}

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
/// </remarks>
public virtual IDataProtectionProvider CreateDataProtectionProvider(IServiceProvider services)
{
return services.GetRequiredService<IDataProtectionProvider>();
return services.GetDataProtectionProvider();
}
/// <summary>
@ -56,30 +56,12 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
/// </summary>
internal IDataProtectionProvider InternalConfigureServicesAndCreateProtectionProvider()
{
// Configure the default implementation, passing in our custom discriminator
var services = new ServiceCollection();
services.AddDataProtection();
services.Configure<DataProtectionOptions>(options =>
{
// Try reading the discriminator from <machineKey applicationName="..." /> defined
// at the web app root. If the value was set explicitly (even if the value is empty),
// honor it as the discriminator. Otherwise, fall back to the metabase config path.
var machineKeySection = (MachineKeySection)WebConfigurationManager.GetWebApplicationSection("system.web/machineKey");
if (machineKeySection.ElementInformation.Properties["applicationName"].ValueOrigin != PropertyValueOrigin.Default)
{
options.ApplicationDiscriminator = machineKeySection.ApplicationName;
}
else
{
options.ApplicationDiscriminator = HttpRuntime.AppDomainAppId;
}
services.AddInstance<IApplicationDiscriminator>(new SystemWebApplicationDiscriminator());
if (String.IsNullOrEmpty(options.ApplicationDiscriminator))
{
options.ApplicationDiscriminator = null; // homogenize to null
}
});
// Run configuration and get an instance of the provider.
// Run user-specified configuration and get an instance of the provider
ConfigureServices(services);
var provider = CreateDataProtectionProvider(services.BuildServiceProvider());
if (provider == null)
@ -90,5 +72,30 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb
// And we're done!
return provider;
}
private sealed class SystemWebApplicationDiscriminator : IApplicationDiscriminator
{
private readonly Lazy<string> _lazyDiscriminator = new Lazy<string>(GetAppDiscriminatorCore);
public string Discriminator => _lazyDiscriminator.Value;
private static string GetAppDiscriminatorCore()
{
// Try reading the discriminator from <machineKey applicationName="..." /> defined
// at the web app root. If the value was set explicitly (even if the value is empty),
// honor it as the discriminator.
var machineKeySection = (MachineKeySection)WebConfigurationManager.GetWebApplicationSection("system.web/machineKey");
if (machineKeySection.ElementInformation.Properties["applicationName"].ValueOrigin != PropertyValueOrigin.Default)
{
return machineKeySection.ApplicationName;
}
else
{
// Otherwise, fall back to the IIS metabase config path.
// This is unique per machine.
return HttpRuntime.AppDomainAppId;
}
}
}
}
}

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNet.DataProtection
/// <typeparam name="TImplementation">The concrete type of the <see cref="IKeyEscrowSink"/> to register.</typeparam>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Registrations are additive.
/// Registrations are additive. The factory is registered as <see cref="ServiceLifetime.Singleton"/>.
/// </remarks>
public DataProtectionConfiguration AddKeyEscrowSink<TImplementation>()
where TImplementation : IKeyEscrowSink
@ -94,7 +94,7 @@ namespace Microsoft.AspNet.DataProtection
/// <param name="factory">A factory that creates the <see cref="IKeyEscrowSink"/> instance.</param>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Registrations are additive.
/// Registrations are additive. The factory is registered as <see cref="ServiceLifetime.Singleton"/>.
/// </remarks>
public DataProtectionConfiguration AddKeyEscrowSink([NotNull] Func<IServiceProvider, IKeyEscrowSink> factory)
{
@ -130,19 +130,6 @@ namespace Microsoft.AspNet.DataProtection
return this;
}
/// <summary>
/// Configures the data protection system to persist keys in storage as plaintext.
/// </summary>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Caution: cryptographic key material will not be protected at rest.
/// </remarks>
public DataProtectionConfiguration DisableProtectionOfKeysAtRest()
{
RemoveAllServicesOfType(typeof(IXmlEncryptor));
return this;
}
/// <summary>
/// Configures the data protection system to persist keys to the specified directory.
/// This path may be on the local machine or may point to a UNC share.
@ -267,6 +254,23 @@ namespace Microsoft.AspNet.DataProtection
return this;
}
/// <summary>
/// Sets the unique name of this application within the data protection system.
/// </summary>
/// <param name="applicationName">The application name.</param>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// This API corresponds to setting the <see cref="DataProtectionOptions.ApplicationDiscriminator"/> property
/// to the value of <paramref name="applicationName"/>.
/// </remarks>
public DataProtectionConfiguration SetApplicationName(string applicationName)
{
return ConfigureGlobalOptions(options =>
{
options.ApplicationDiscriminator = applicationName;
});
}
/// <summary>
/// Sets the default lifetime of keys created by the data protection system.
/// </summary>

View File

@ -33,7 +33,7 @@ namespace Microsoft.Framework.DependencyInjection
{
return new ConfigureOptions<DataProtectionOptions>(options =>
{
options.ApplicationDiscriminator = services.GetService<IApplicationDiscriminator>()?.Discriminator;
options.ApplicationDiscriminator = services.GetApplicationUniqueIdentifier();
});
});
}

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
using Microsoft.AspNet.DataProtection.Cng;
using Microsoft.AspNet.DataProtection.KeyManagement;
using Microsoft.AspNet.DataProtection.Repositories;
using Microsoft.Framework.Logging;
namespace Microsoft.Framework.DependencyInjection
{
@ -31,6 +32,8 @@ namespace Microsoft.Framework.DependencyInjection
// we'll not use the fallback at all.
yield return ServiceDescriptor.Singleton<IDefaultKeyServices>(services =>
{
ILogger log = services.GetLogger(typeof(DataProtectionServices));
ServiceDescriptor keyEncryptorDescriptor = null;
ServiceDescriptor keyRepositoryDescriptor = null;
@ -38,6 +41,11 @@ namespace Microsoft.Framework.DependencyInjection
var azureWebSitesKeysFolder = FileSystemXmlRepository.GetKeyStorageDirectoryForAzureWebSites();
if (azureWebSitesKeysFolder != null)
{
if (log.IsInformationLevelEnabled())
{
log.LogInformation("Azure Web Sites environment detected. Using '{0}' as key repository; keys will not be encrypted at rest.", azureWebSitesKeysFolder.FullName);
}
// Cloud DPAPI isn't yet available, so we don't encrypt keys at rest.
// This isn't all that different than what Azure Web Sites does today, and we can always add this later.
keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_FileSystem(azureWebSitesKeysFolder);
@ -55,6 +63,18 @@ namespace Microsoft.Framework.DependencyInjection
keyEncryptorDescriptor = DataProtectionServiceDescriptors.IXmlEncryptor_Dpapi(protectToMachine: !DpapiSecretSerializerHelper.CanProtectToCurrentUserAccount());
}
keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_FileSystem(localAppDataKeysFolder);
if (log.IsInformationLevelEnabled())
{
if (keyEncryptorDescriptor != null)
{
log.LogInformation("User profile is available. Using '{0}' as key repository and Windows DPAPI to encrypt keys at rest.", localAppDataKeysFolder.FullName);
}
else
{
log.LogInformation("User profile is available. Using '{0}' as key repository; keys will not be encrypted at rest.", localAppDataKeysFolder.FullName);
}
}
}
else
{
@ -68,12 +88,29 @@ namespace Microsoft.Framework.DependencyInjection
keyEncryptorDescriptor = DataProtectionServiceDescriptors.IXmlEncryptor_Dpapi(protectToMachine: true);
}
keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_Registry(regKeyStorageKey);
if (log.IsInformationLevelEnabled())
{
if (keyEncryptorDescriptor != null)
{
log.LogInformation("User profile not available. Using '{0}' as key repository and Windows DPAPI to encrypt keys at rest.", regKeyStorageKey.Name);
}
else
{
log.LogInformation("User profile not available. Using '{0}' as key repository; keys will not be encrypted at rest.", regKeyStorageKey.Name);
}
}
}
else
{
// Final fallback - use an ephemeral repository since we don't know where else to go.
// This can only be used for development scenarios.
keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_InMemory();
if (log.IsWarningLevelEnabled())
{
log.LogWarning("Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.");
}
}
}
}

View File

@ -8,19 +8,36 @@ using Microsoft.Framework.Logging;
namespace System
{
/// <summary>
/// Helpful extension methods on IServiceProvider.
/// Helpful logging-related extension methods on <see cref="IServiceProvider"/>.
/// </summary>
internal static class LoggingServiceProviderExtensions
{
/// <summary>
/// Retrieves an instance of ILogger given the type name of the caller.
/// The caller's type name is used as the name of the ILogger created.
/// This method returns null if the IServiceProvider is null or if it
/// does not contain a registered ILoggerFactory.
/// Retrieves an instance of <see cref="ILogger"/> given the type name <typeparamref name="T"/>.
/// This is equivalent to <see cref="LoggerFactoryExtensions.CreateLogger{T}(ILoggerFactory)"/>.
/// </summary>
/// <returns>
/// An <see cref="ILogger"/> instance, or null if <paramref name="services"/> is null or the
/// <see cref="IServiceProvider"/> cannot produce an <see cref="ILoggerFactory"/>.
/// </returns>
public static ILogger GetLogger<T>(this IServiceProvider services)
{
return services?.GetService<ILoggerFactory>()?.CreateLogger<T>();
return GetLogger(services, typeof(T));
}
/// <summary>
/// Retrieves an instance of <see cref="ILogger"/> given the type name <paramref name="type"/>.
/// This is equivalent to <see cref="LoggerFactoryExtensions.CreateLogger{T}(ILoggerFactory)"/>.
/// </summary>
/// <returns>
/// An <see cref="ILogger"/> instance, or null if <paramref name="services"/> is null or the
/// <see cref="IServiceProvider"/> cannot produce an <see cref="ILoggerFactory"/>.
/// </returns>
public static ILogger GetLogger(this IServiceProvider services, Type type)
{
// Compiler won't allow us to use static types as the type parameter
// for the call to CreateLogger<T>, so we'll duplicate its logic here.
return services?.GetService<ILoggerFactory>()?.CreateLogger(type.FullName);
}
}
}

View File

@ -13,7 +13,6 @@
"frameworks": {
"net451": {
"frameworkAssemblies": {
"System.Runtime": { "version": "", "type": "build" },
"System.Security": "",
"System.Xml": "",
"System.Xml.Linq": ""
@ -21,7 +20,6 @@
},
"dnx451": {
"frameworkAssemblies": {
"System.Runtime": { "version": "", "type": "build" },
"System.Security": "",
"System.Xml": "",
"System.Xml.Linq": ""

View File

@ -7,6 +7,7 @@ using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.DataProtection.Interfaces;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.Runtime;
using Moq;
using Xunit;
@ -106,6 +107,170 @@ namespace Microsoft.AspNet.DataProtection
Assert.Same(finalExpectedProtector, retVal);
}
[Theory]
[InlineData(" discriminator", "app-path ", "discriminator")] // normalized trim
[InlineData("", "app-path", null)] // app discriminator not null -> overrides app base path
[InlineData(null, "app-path ", "app-path")] // normalized trim
[InlineData(null, " ", null)] // normalized whitespace -> null
[InlineData(null, null, null)] // nothing provided at all
public void GetApplicationUniqueIdentifier(string appDiscriminator, string appBasePath, string expected)
{
// Arrange
var mockAppDiscriminator = new Mock<IApplicationDiscriminator>();
mockAppDiscriminator.Setup(o => o.Discriminator).Returns(appDiscriminator);
var mockAppEnvironment = new Mock<IApplicationEnvironment>();
mockAppEnvironment.Setup(o => o.ApplicationBasePath).Returns(appBasePath);
var mockServiceProvider = new Mock<IServiceProvider>();
mockServiceProvider.Setup(o => o.GetService(typeof(IApplicationDiscriminator))).Returns(mockAppDiscriminator.Object);
mockServiceProvider.Setup(o => o.GetService(typeof(IApplicationEnvironment))).Returns(mockAppEnvironment.Object);
// Act
string actual = mockServiceProvider.Object.GetApplicationUniqueIdentifier();
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void GetApplicationUniqueIdentifier_NoServiceProvider_ReturnsNull()
{
Assert.Null(((IServiceProvider)null).GetApplicationUniqueIdentifier());
}
[Fact]
public void GetDataProtectionProvider_NoServiceFound_Throws()
{
// Arrange
var services = new Mock<IServiceProvider>().Object;
// Act & assert
var ex = Assert.Throws<InvalidOperationException>(() => services.GetDataProtectionProvider());
Assert.Equal(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName), ex.Message);
}
[Fact]
public void GetDataProtectionProvider_ServiceFound_ReturnsService()
{
// Arrange
var expected = new Mock<IDataProtectionProvider>().Object;
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(expected);
var services = mockServices.Object;
// Act
var actual = services.GetDataProtectionProvider();
// Assert
Assert.Same(expected, actual);
}
[Theory]
[InlineData(new object[] { new string[0] })]
[InlineData(new object[] { new string[] { null } })]
[InlineData(new object[] { new string[] { "the next value is bad", null } })]
public void GetDataProtector_ChainedAsIEnumerable_FailureCases(string[] purposes)
{
// Arrange
var mockProtector = new Mock<IDataProtector>();
mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
var services = mockServices.Object;
// Act & assert
ExceptionAssert.ThrowsArgument(
testCode: () => services.GetDataProtector((IEnumerable<string>)purposes),
paramName: "purposes",
exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
}
[Theory]
[InlineData(new object[] { new string[] { null } })]
[InlineData(new object[] { new string[] { "the next value is bad", null } })]
public void GetDataProtector_ChainedAsParams_FailureCases(string[] subPurposes)
{
// Arrange
var mockProtector = new Mock<IDataProtector>();
mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
var services = mockServices.Object;
// Act & assert
ExceptionAssert.ThrowsArgument(
testCode: () => services.GetDataProtector("primary-purpose", subPurposes),
paramName: "purposes",
exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
}
[Fact]
public void GetDataProtector_ChainedAsIEnumerable_SuccessCase()
{
// Arrange
var finalExpectedProtector = new Mock<IDataProtector>().Object;
var thirdMock = new Mock<IDataProtector>();
thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
var secondMock = new Mock<IDataProtector>();
secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
var firstMock = new Mock<IDataProtector>();
firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
var services = mockServices.Object;
// Act
var retVal = services.GetDataProtector((IEnumerable<string>)new string[] { "first", "second", "third" });
// Assert
Assert.Same(finalExpectedProtector, retVal);
}
[Fact]
public void GetDataProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
{
// Arrange
var finalExpectedProtector = new Mock<IDataProtector>().Object;
var thirdMock = new Mock<IDataProtector>();
thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
var secondMock = new Mock<IDataProtector>();
secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
var firstMock = new Mock<IDataProtector>();
firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
var services = mockServices.Object;
// Act
var retVal = services.GetDataProtector("first", "second", "third");
// Assert
Assert.Same(finalExpectedProtector, retVal);
}
[Theory]
[InlineData(new object[] { null })]
[InlineData(new object[] { new string[0] })]
public void GetDataProtector_ChainedAsParams_EmptyParams_SuccessCases(string[] subPurposes)
{
// Arrange
var finalExpectedProtector = new Mock<IDataProtector>().Object;
var firstMock = new Mock<IDataProtector>();
firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);
var mockServices = new Mock<IServiceProvider>();
mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
var services = mockServices.Object;
// Act
var retVal = services.GetDataProtector("first", subPurposes);
// Assert
Assert.Same(finalExpectedProtector, retVal);
}
[Fact]
public void Protect_InvalidUtf8_Failure()
{