From 82d92064c50c13f2737f96c6d76b45d68e9a9d05 Mon Sep 17 00:00:00 2001 From: Levi B Date: Thu, 12 Mar 2015 17:54:15 -0700 Subject: [PATCH] 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 --- .../DataProtectionExtensions.cs | 95 +++++++++- .../Properties/Resources.Designer.cs | 16 ++ .../Resources.resx | 3 + .../project.json | 8 +- .../CompatibilityDataProtector.cs | 55 +++++- .../DataProtectionStartup.cs | 51 +++--- .../DataProtectionConfiguration.cs | 34 ++-- .../DataProtectionServiceDescriptors.cs | 2 +- .../DataProtectionServices.cs | 37 ++++ .../LoggingServiceProviderExtensions.cs | 29 ++- .../project.json | 2 - .../DataProtectionExtensionsTests.cs | 165 ++++++++++++++++++ 12 files changed, 446 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs index 291ab59633..393d9b5307 100644 --- a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs +++ b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs @@ -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 { /// @@ -53,7 +58,7 @@ namespace Microsoft.AspNet.DataProtection /// Creates an given a list of purposes. /// /// The from which to generate the purpose chain. - /// The primary purpose used to create the . + /// The primary purpose used to create the . /// An optional list of secondary purposes which contribute to the purpose chain. /// If this list is provided it cannot contain null elements. /// An tied to the provided purpose chain. @@ -75,7 +80,93 @@ namespace Microsoft.AspNet.DataProtection } return protector ?? CryptoUtil.Fail("CreateProtector returned null."); } - + + /// + /// Returns a unique identifier for this application. + /// + /// The application-level . + /// A unique application identifier, or null if is null + /// or cannot provide a unique application identifier. + /// + /// 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. + /// + [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; + } + + /// + /// Retrieves an from an . + /// + /// The service provider from which to retrieve the . + /// An . This method is guaranteed never to return null. + /// If no service exists in . + public static IDataProtectionProvider GetDataProtectionProvider([NotNull] this IServiceProvider services) + { + // We have our own implementation of GetRequiredService 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; + } + + /// + /// Retrieves an from an given a list of purposes. + /// + /// An which contains the + /// from which to generate the purpose chain. + /// 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. + /// An tied to the provided purpose chain. + /// + /// This is a convenience method which calls + /// then . See those methods' + /// documentation for more information. + /// + public static IDataProtector GetDataProtector([NotNull] this IServiceProvider services, [NotNull] IEnumerable purposes) + { + return services.GetDataProtectionProvider().CreateProtector(purposes); + } + + /// + /// Retrieves an from an given a list of purposes. + /// + /// An which contains the + /// from which to generate the purpose chain. + /// The primary purpose used to create the . + /// An optional list of secondary purposes which contribute to the purpose chain. + /// If this list is provided it cannot contain null elements. + /// An tied to the provided purpose chain. + /// + /// This is a convenience method which calls + /// then . See those methods' + /// documentation for more information. + /// + public static IDataProtector GetDataProtector([NotNull] this IServiceProvider services, [NotNull] string purpose, params string[] subPurposes) + { + return services.GetDataProtectionProvider().CreateProtector(purpose, subPurposes); + } + /// /// Cryptographically protects a piece of plaintext data. /// diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs index 9c0eed3510..c0b13a79e6 100644 --- a/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs @@ -58,6 +58,22 @@ namespace Microsoft.AspNet.DataProtection.Interfaces return GetString("CryptCommon_GenericError"); } + /// + /// No service for type '{0}' has been registered. + /// + internal static string DataProtectionExtensions_NoService + { + get { return GetString("DataProtectionExtensions_NoService"); } + } + + /// + /// No service for type '{0}' has been registered. + /// + 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); diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx b/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx index 84fa596602..daa9e2cbd9 100644 --- a/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx +++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx @@ -126,4 +126,7 @@ An error occurred during a cryptographic operation. + + No service for type '{0}' has been registered. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/project.json b/src/Microsoft.AspNet.DataProtection.Interfaces/project.json index f8543204e6..a29ad1792a 100644 --- a/src/Microsoft.AspNet.DataProtection.Interfaces/project.json +++ b/src/Microsoft.AspNet.DataProtection.Interfaces/project.json @@ -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-*", diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs b/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs index 5bf5b5b6d4..3f67e256ab 100644 --- a/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs +++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs @@ -18,7 +18,11 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb { private static readonly Lazy _lazyProtectionProvider = new Lazy(CreateProtectionProvider); + [ThreadStatic] + private static bool _suppressPrimaryPurpose; + private readonly Lazy _lazyProtector; + private readonly Lazy _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(() => _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(() => _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 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); + } + + /// + /// Invokes a delegate where calls to + /// and will ignore the primary + /// purpose and instead use only the sub-purposes. + /// + public static byte[] RunWithSuppressedPrimaryPurpose(Func 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; + } } } } diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs b/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs index b6792c9882..664a68aa73 100644 --- a/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs +++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs @@ -47,7 +47,7 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb /// public virtual IDataProtectionProvider CreateDataProtectionProvider(IServiceProvider services) { - return services.GetRequiredService(); + return services.GetDataProtectionProvider(); } /// @@ -56,30 +56,12 @@ namespace Microsoft.AspNet.DataProtection.SystemWeb /// internal IDataProtectionProvider InternalConfigureServicesAndCreateProtectionProvider() { + // Configure the default implementation, passing in our custom discriminator var services = new ServiceCollection(); services.AddDataProtection(); - services.Configure(options => - { - // Try reading the discriminator from 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(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 _lazyDiscriminator = new Lazy(GetAppDiscriminatorCore); + + public string Discriminator => _lazyDiscriminator.Value; + + private static string GetAppDiscriminatorCore() + { + // Try reading the discriminator from 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; + } + } + } } } diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs index e2350cd642..371852cb20 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs @@ -79,7 +79,7 @@ namespace Microsoft.AspNet.DataProtection /// The concrete type of the to register. /// The 'this' instance. /// - /// Registrations are additive. + /// Registrations are additive. The factory is registered as . /// public DataProtectionConfiguration AddKeyEscrowSink() where TImplementation : IKeyEscrowSink @@ -94,7 +94,7 @@ namespace Microsoft.AspNet.DataProtection /// A factory that creates the instance. /// The 'this' instance. /// - /// Registrations are additive. + /// Registrations are additive. The factory is registered as . /// public DataProtectionConfiguration AddKeyEscrowSink([NotNull] Func factory) { @@ -130,19 +130,6 @@ namespace Microsoft.AspNet.DataProtection return this; } - /// - /// Configures the data protection system to persist keys in storage as plaintext. - /// - /// The 'this' instance. - /// - /// Caution: cryptographic key material will not be protected at rest. - /// - public DataProtectionConfiguration DisableProtectionOfKeysAtRest() - { - RemoveAllServicesOfType(typeof(IXmlEncryptor)); - return this; - } - /// /// 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; } + /// + /// Sets the unique name of this application within the data protection system. + /// + /// The application name. + /// The 'this' instance. + /// + /// This API corresponds to setting the property + /// to the value of . + /// + public DataProtectionConfiguration SetApplicationName(string applicationName) + { + return ConfigureGlobalOptions(options => + { + options.ApplicationDiscriminator = applicationName; + }); + } + /// /// Sets the default lifetime of keys created by the data protection system. /// diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs index 3276c76575..f7a8c3da0b 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs @@ -33,7 +33,7 @@ namespace Microsoft.Framework.DependencyInjection { return new ConfigureOptions(options => { - options.ApplicationDiscriminator = services.GetService()?.Discriminator; + options.ApplicationDiscriminator = services.GetApplicationUniqueIdentifier(); }); }); } diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs index 79d640b5e3..234099ea4d 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs @@ -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(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."); + } } } } diff --git a/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs b/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs index 4b9f05ec59..267b1d8c99 100644 --- a/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs +++ b/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs @@ -8,19 +8,36 @@ using Microsoft.Framework.Logging; namespace System { /// - /// Helpful extension methods on IServiceProvider. + /// Helpful logging-related extension methods on . /// internal static class LoggingServiceProviderExtensions { /// - /// 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 given the type name . + /// This is equivalent to . /// + /// + /// An instance, or null if is null or the + /// cannot produce an . + /// public static ILogger GetLogger(this IServiceProvider services) { - return services?.GetService()?.CreateLogger(); + return GetLogger(services, typeof(T)); + } + + /// + /// Retrieves an instance of given the type name . + /// This is equivalent to . + /// + /// + /// An instance, or null if is null or the + /// cannot produce an . + /// + 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, so we'll duplicate its logic here. + return services?.GetService()?.CreateLogger(type.FullName); } } } diff --git a/src/Microsoft.AspNet.DataProtection/project.json b/src/Microsoft.AspNet.DataProtection/project.json index 8de03164c8..525328fdea 100644 --- a/src/Microsoft.AspNet.DataProtection/project.json +++ b/src/Microsoft.AspNet.DataProtection/project.json @@ -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": "" diff --git a/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs index 268e3e1d21..a174b4a4fa 100644 --- a/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs @@ -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(); + mockAppDiscriminator.Setup(o => o.Discriminator).Returns(appDiscriminator); + var mockAppEnvironment = new Mock(); + mockAppEnvironment.Setup(o => o.ApplicationBasePath).Returns(appBasePath); + var mockServiceProvider = new Mock(); + 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().Object; + + // Act & assert + var ex = Assert.Throws(() => services.GetDataProtectionProvider()); + Assert.Equal(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName), ex.Message); + } + + [Fact] + public void GetDataProtectionProvider_ServiceFound_ReturnsService() + { + // Arrange + var expected = new Mock().Object; + var mockServices = new Mock(); + 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(); + mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object); + var mockServices = new Mock(); + mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object); + var services = mockServices.Object; + + // Act & assert + ExceptionAssert.ThrowsArgument( + testCode: () => services.GetDataProtector((IEnumerable)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(); + mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object); + var mockServices = new Mock(); + 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().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); + + var mockServices = new Mock(); + mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object); + var services = mockServices.Object; + + // Act + var retVal = services.GetDataProtector((IEnumerable)new string[] { "first", "second", "third" }); + + // Assert + Assert.Same(finalExpectedProtector, retVal); + } + + [Fact] + public void GetDataProtector_ChainedAsParams_NonEmptyParams_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); + + var mockServices = new Mock(); + 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().Object; + var firstMock = new Mock(); + firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector); + var mockServices = new Mock(); + 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() {