From b340b0f0f73db72045b3c6577eaff60b5772906d Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Fri, 2 Sep 2016 11:56:47 -0700 Subject: [PATCH] Add Redis IXmlRepository implementation (#173) --- DataProtection.sln | 38 +++++++++- NuGetPackageVerifier.json | 1 + samples/Redis/Program.cs | 33 ++++++++ samples/Redis/Redis.xproj | 19 +++++ samples/Redis/project.json | 16 ++++ ...soft.AspNetCore.DataProtection.Redis.xproj | 19 +++++ .../RedisDataProtectionBuilderExtensions.cs | 76 +++++++++++++++++++ .../RedisXmlRepository.cs | 59 ++++++++++++++ .../project.json | 31 ++++++++ .../DataProtectionRedisTests.cs | 59 ++++++++++++++ ...AspNetCore.DataProtection.Redis.Test.xproj | 19 +++++ .../project.json | 21 +++++ 12 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 samples/Redis/Program.cs create mode 100644 samples/Redis/Redis.xproj create mode 100644 samples/Redis/project.json create mode 100644 src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.xproj create mode 100644 src/Microsoft.AspNetCore.DataProtection.Redis/RedisDataProtectionBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.Redis/RedisXmlRepository.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.Redis/project.json create mode 100644 test/Microsoft.AspNetCore.DataProtection.Redis.Test/DataProtectionRedisTests.cs create mode 100644 test/Microsoft.AspNetCore.DataProtection.Redis.Test/Microsoft.AspNetCore.DataProtection.Redis.Test.xproj create mode 100644 test/Microsoft.AspNetCore.DataProtection.Redis.Test/project.json diff --git a/DataProtection.sln b/DataProtection.sln index 684647bcdc..462c11dab8 100644 --- a/DataProtection.sln +++ b/DataProtection.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22710.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}" EndProject @@ -33,6 +32,14 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataPr EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Extensions", "src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.xproj", "{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis", "src\Microsoft.AspNetCore.DataProtection.Redis\Microsoft.AspNetCore.DataProtection.Redis.xproj", "{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Redis", "samples\Redis\Redis.xproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis.Test", "test\Microsoft.AspNetCore.DataProtection.Redis.Test\Microsoft.AspNetCore.DataProtection.Redis.Test.xproj", "{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -141,6 +148,30 @@ Global {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.Build.0 = Release|Any CPU {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.ActiveCfg = Release|Any CPU {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.Build.0 = Release|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|x86.Build.0 = Debug|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.Build.0 = Release|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.ActiveCfg = Release|Any CPU + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.Build.0 = Release|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.Build.0 = Debug|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.Build.0 = Release|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.ActiveCfg = Release|Any CPU + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.Build.0 = Release|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.ActiveCfg = Debug|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.Build.0 = Debug|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.Build.0 = Release|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.ActiveCfg = Release|Any CPU + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -159,5 +190,8 @@ Global {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA} {04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814} {BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA} + {0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA} + {24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2} + {ABCF00E5-5B2F-469C-90DC-908C5A04C08D} = {60336AB3-948D-4D15-A5FB-F32A2B91E814} EndGlobalSection EndGlobal diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index f5a8168e06..af9e7d025d 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -9,6 +9,7 @@ "Microsoft.AspNetCore.DataProtection": { }, "Microsoft.AspNetCore.DataProtection.Abstractions": { }, "Microsoft.AspNetCore.DataProtection.Extensions": { }, + "Microsoft.AspNetCore.DataProtection.Redis": { }, "Microsoft.AspNetCore.DataProtection.SystemWeb": { } } }, diff --git a/samples/Redis/Program.cs b/samples/Redis/Program.cs new file mode 100644 index 0000000000..6bc61bdb70 --- /dev/null +++ b/samples/Redis/Program.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.DataProtection.Redis; +using StackExchange.Redis; + +namespace Redis +{ + public class Program + { + public static void Main(string[] args) + { + // Connect + var redis = ConnectionMultiplexer.Connect("localhost:6379"); + + // Configure + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddDataProtection() + .PersistKeysToRedis(redis, "DataProtection-Keys"); + + var services = serviceCollection.BuildServiceProvider(); + var loggerFactory = services.GetService(); + loggerFactory.AddConsole(LogLevel.Trace); + + // Run a sample payload + var protector = services.GetDataProtector("sample-purpose"); + var protectedData = protector.Protect("Hello world!"); + Console.WriteLine(protectedData); + } + } +} \ No newline at end of file diff --git a/samples/Redis/Redis.xproj b/samples/Redis/Redis.xproj new file mode 100644 index 0000000000..29ab6c0dc5 --- /dev/null +++ b/samples/Redis/Redis.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 24aaec96-df46-4f61-b2ff-3d5e056685d9 + Redis + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/samples/Redis/project.json b/samples/Redis/project.json new file mode 100644 index 0000000000..a09b7bda3a --- /dev/null +++ b/samples/Redis/project.json @@ -0,0 +1,16 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "debugType": "portable", + "emitEntryPoint": true + }, + "dependencies": { + "Microsoft.AspNetCore.DataProtection.Redis": "1.1.0-*", + "Microsoft.Extensions.DependencyInjection": "1.1.0-*", + "Microsoft.Extensions.Logging": "1.1.0-*", + "Microsoft.Extensions.Logging.Console": "1.1.0-*" + }, + "frameworks": { + "net451": { } + } +} diff --git a/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.xproj b/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.xproj new file mode 100644 index 0000000000..ed52548f61 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 0508adb0-9d2e-4506-9aa3-c15d7beae7c9 + Microsoft.AspNetCore.DataProtection.Redis + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.Redis/RedisDataProtectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.DataProtection.Redis/RedisDataProtectionBuilderExtensions.cs new file mode 100644 index 0000000000..2974d23ce9 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.Redis/RedisDataProtectionBuilderExtensions.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using StackExchange.Redis; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.DataProtection +{ + /// + /// Contains Redis-specific extension methods for modifying a . + /// + public static class RedisDataProtectionBuilderExtensions + { + private const string DataProtectionKeysName = "DataProtection-Keys"; + + /// + /// Configures the data protection system to persist keys to specified key in Redis database + /// + /// The builder instance to modify. + /// The delegate used to create instances. + /// The used to store key list. + /// A reference to the after this operation has completed. + public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, Func databaseFactory, RedisKey key) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (databaseFactory == null) + { + throw new ArgumentNullException(nameof(databaseFactory)); + } + return PersistKeysToRedisInternal(builder, databaseFactory, key); + } + + /// + /// Configures the data protection system to persist keys to the default key ('DataProtection-Keys') in Redis database + /// + /// The builder instance to modify. + /// The for database access. + /// A reference to the after this operation has completed. + public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer) + { + return PersistKeysToRedis(builder, connectionMultiplexer, DataProtectionKeysName); + } + + /// + /// Configures the data protection system to persist keys to the specified key in Redis database + /// + /// The builder instance to modify. + /// The for database access. + /// The used to store key list. + /// A reference to the after this operation has completed. + public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer, RedisKey key) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (connectionMultiplexer == null) + { + throw new ArgumentNullException(nameof(connectionMultiplexer)); + } + return PersistKeysToRedisInternal(builder, () => connectionMultiplexer.GetDatabase(), key); + } + + private static IDataProtectionBuilder PersistKeysToRedisInternal(IDataProtectionBuilder config, Func databaseFactory, RedisKey key) + { + config.Services.TryAddSingleton(services => new RedisXmlRepository(databaseFactory, key)); + return config; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.Redis/RedisXmlRepository.cs b/src/Microsoft.AspNetCore.DataProtection.Redis/RedisXmlRepository.cs new file mode 100644 index 0000000000..87a9338f64 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.Redis/RedisXmlRepository.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using StackExchange.Redis; +using Microsoft.AspNetCore.DataProtection.Repositories; + +namespace Microsoft.AspNetCore.DataProtection +{ + /// + /// An XML repository backed by a Redis list entry. + /// + public class RedisXmlRepository: IXmlRepository + { + private readonly Func _databaseFactory; + private readonly RedisKey _key; + + /// + /// Creates a with keys stored at the given directory. + /// + /// The delegate used to create instances. + /// The used to store key list. + public RedisXmlRepository(Func databaseFactory, RedisKey key) + { + _databaseFactory = databaseFactory; + _key = key; + } + + /// + public IReadOnlyCollection GetAllElements() + { + return GetAllElementsCore().ToList().AsReadOnly(); + } + + private IEnumerable GetAllElementsCore() + { + // Note: Inability to read any value is considered a fatal error (since the file may contain + // revocation information), and we'll fail the entire operation rather than return a partial + // set of elements. If a value contains well-formed XML but its contents are meaningless, we + // won't fail that operation here. The caller is responsible for failing as appropriate given + // that scenario. + var database = _databaseFactory(); + foreach (var value in database.ListRange(_key)) + { + yield return XElement.Parse(value); + } + } + + /// + public void StoreElement(XElement element, string friendlyName) + { + var database = _databaseFactory(); + database.ListRightPush(_key, element.ToString(SaveOptions.DisableFormatting)); + } + } +} diff --git a/src/Microsoft.AspNetCore.DataProtection.Redis/project.json b/src/Microsoft.AspNetCore.DataProtection.Redis/project.json new file mode 100644 index 0000000000..b4aa251e99 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.Redis/project.json @@ -0,0 +1,31 @@ +{ + "version": "0.1.0-*", + "description": "Redis storrage support as key store.", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/aspnet/dataprotection" + }, + "tags": [ + "aspnetcore", + "dataprotection", + "redis" + ] + }, + "dependencies": { + "Microsoft.AspNetCore.DataProtection": "1.1.0-*", + "StackExchange.Redis.StrongName": "1.1.603" + }, + "frameworks": { + "net451": {} + }, + "buildOptions": { + "allowUnsafe": true, + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "nowarn": [ + "CS1591" + ], + "xmlDoc": true + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.DataProtection.Redis.Test/DataProtectionRedisTests.cs b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/DataProtectionRedisTests.cs new file mode 100644 index 0000000000..9e010090f8 --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/DataProtectionRedisTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Moq; +using StackExchange.Redis; +using Xunit; + +namespace Microsoft.AspNetCore.DataProtection +{ + public class DataProtectionRedisTests + { + [Fact] + public void GetAllElements_ReturnsAllXmlValuesForGivenKey() + { + var database = new Mock(); + database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[] + { + "", + "", + }).Verifiable(); + var repo = new RedisXmlRepository(() => database.Object, "Key"); + + var elements = repo.GetAllElements().ToArray(); + + database.Verify(); + Assert.Equal(new XElement("Element1").ToString(), elements[0].ToString()); + Assert.Equal(new XElement("Element2").ToString(), elements[1].ToString()); + } + + [Fact] + public void GetAllElements_ThrowsParsingException() + { + var database = new Mock(); + database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[] + { + "", + " database.Object, "Key"); + + Assert.Throws(() => repo.GetAllElements()); + } + + [Fact] + public void StoreElement_PushesValueToList() + { + var database = new Mock(); + database.Setup(d => d.ListRightPush("Key", "", When.Always, CommandFlags.None)).Verifiable(); + var repo = new RedisXmlRepository(() => database.Object, "Key"); + + repo.StoreElement(new XElement("Element2"), null); + + database.Verify(); + } + } +} diff --git a/test/Microsoft.AspNetCore.DataProtection.Redis.Test/Microsoft.AspNetCore.DataProtection.Redis.Test.xproj b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/Microsoft.AspNetCore.DataProtection.Redis.Test.xproj new file mode 100644 index 0000000000..123ca898a3 --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/Microsoft.AspNetCore.DataProtection.Redis.Test.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + abcf00e5-5b2f-469c-90dc-908c5a04c08d + Microsoft.AspNetCore.DataProtection.Redis.Test + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.DataProtection.Redis.Test/project.json b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/project.json new file mode 100644 index 0000000000..32f76c1230 --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.Redis.Test/project.json @@ -0,0 +1,21 @@ +{ + "dependencies": { + "dotnet-test-xunit": "2.2.0-*", + "Microsoft.AspNetCore.DataProtection.Abstractions": "1.1.0-*", + "Microsoft.AspNetCore.DataProtection.Redis": "1.1.0-*", + "Microsoft.AspNetCore.Testing": "1.1.0-*", + "Moq": "4.6.36-*", + "xunit": "2.2.0-*" + }, + "frameworks": { + "net451": {} + }, + "testRunner": "xunit", + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "compile": { + "include": "../common/**/*.cs" + } + } +} \ No newline at end of file