From d755754198bedf4684f7e99b4bd04d22142f0fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 11 May 2023 18:12:45 +0200 Subject: [PATCH 01/21] Fixing Json.GetValue() --- AMWD.Common/Extensions/JsonExtensions.cs | 17 +++++++++++++++-- CHANGELOG.md | 10 +++++++++- .../Common/Extensions/JsonExtensionsTests.cs | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/AMWD.Common/Extensions/JsonExtensions.cs b/AMWD.Common/Extensions/JsonExtensions.cs index 95dfaa1..7d91a91 100644 --- a/AMWD.Common/Extensions/JsonExtensions.cs +++ b/AMWD.Common/Extensions/JsonExtensions.cs @@ -146,13 +146,26 @@ namespace Newtonsoft.Json if (lvlObj == null) return defaultValue; - lvlObj = lvlObj[level]; + string lvl = level; + if (lvlObj.Type == JTokenType.Object) + { + foreach (var prop in ((JObject)lvlObj).Properties()) + { + if (prop.Name.Equals(lvl, System.StringComparison.OrdinalIgnoreCase)) + { + lvl = prop.Name; + break; + } + } + } + + lvlObj = lvlObj[lvl]; } if (lvlObj == null) return defaultValue; - return DeepConvert.ChangeType(lvlObj is JValue ? ((JValue)lvlObj).Value : lvlObj.Value()); + return DeepConvert.ChangeType(lvlObj is JValue lvlValue ? lvlValue.Value : lvlObj.Value()); } /// diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6d56e..1bae954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.11.0...main) - 0000-00-00 +## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.11.1...main) - 0000-00-00 _no changes yet_ + +## [v1.11.1](https://git.am-wd.de/AM.WD/common/compare/v1.11.0...v1.11.1) - 2023-05-11 + +### Fixed + +- `JsonExtensions.GetValue()` now is case insensitive and detects the correct property name of a `JObject`. + + ## [v1.11.0](https://git.am-wd.de/AM.WD/common/compare/v1.10.0...v1.11.0) - 2023-03-29 ### Added diff --git a/UnitTests/Common/Extensions/JsonExtensionsTests.cs b/UnitTests/Common/Extensions/JsonExtensionsTests.cs index 76faa5d..39bb0c3 100644 --- a/UnitTests/Common/Extensions/JsonExtensionsTests.cs +++ b/UnitTests/Common/Extensions/JsonExtensionsTests.cs @@ -260,7 +260,7 @@ namespace UnitTests.Common.Extensions // act string topLevelString = jObj.GetValue("stringValue"); decimal topLevelDecimal = jObj.GetValue("decimalValue"); - int subLevelInteger = jObj.GetValue("object:integerValue"); + int subLevelInteger = jObj.GetValue("object:IntegerValue"); string subLevelString = jObj.GetValue("object:stringValue"); string notExistingOnTopLevel = jObj.GetValue("fancyValue"); @@ -288,7 +288,7 @@ namespace UnitTests.Common.Extensions // act string topLevelString = jObj.GetValue("stringValue", "Test String"); decimal topLevelDecimal = jObj.GetValue("decimalValue", 13.24m); - int subLevelInteger = jObj.GetValue("object:integerValue", 55); + int subLevelInteger = jObj.GetValue("object:IntegerValue", 55); string subLevelString = jObj.GetValue("object:stringValue", "Yeah!"); string notExistingOnTopLevel = jObj.GetValue("fancyValue", "Party!"); From 371283e653ceb1bfd46cab656260e198009cbd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 1 Jun 2023 20:01:19 +0200 Subject: [PATCH 02/21] Changed behaviour of remote ip address detection, renamed ip allow/block lists --- ...stAttribute.cs => IPAllowListAttribute.cs} | 2 +- ...stAttribute.cs => IPBlockListAttribute.cs} | 12 ++-- .../Extensions/HttpContextExtensions.cs | 64 +++++++++++++++---- CHANGELOG.md | 16 ++++- Directory.Build.targets | 4 -- ...eTests.cs => IPAllowListAttributeTests.cs} | 20 +++--- ...eTests.cs => IPBlockListAttributeTests.cs} | 42 ++++++------ .../Extensions/HttpContextExtensionsTests.cs | 52 +++++++++------ 8 files changed, 135 insertions(+), 77 deletions(-) rename AMWD.Common.AspNetCore/Attributes/{IPWhitelistAttribute.cs => IPAllowListAttribute.cs} (94%) rename AMWD.Common.AspNetCore/Attributes/{IPBlacklistAttribute.cs => IPBlockListAttribute.cs} (85%) rename UnitTests/AspNetCore/Attributes/{IPWhitelistAttributeTests.cs => IPAllowListAttributeTests.cs} (91%) rename UnitTests/AspNetCore/Attributes/{IPBlacklistAttributeTests.cs => IPBlockListAttributeTests.cs} (87%) diff --git a/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPAllowListAttribute.cs similarity index 94% rename from AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs rename to AMWD.Common.AspNetCore/Attributes/IPAllowListAttribute.cs index ef2f9bb..4441633 100644 --- a/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/IPAllowListAttribute.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters /// /// Implements an IP filter. Only defined addresses are allowed to access. /// - public class IPWhitelistAttribute : ActionFilterAttribute + public class IPAllowListAttribute : ActionFilterAttribute { /// /// Gets or sets a value indicating whether local (localhost) access is granted (Default: true). diff --git a/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPBlockListAttribute.cs similarity index 85% rename from AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs rename to AMWD.Common.AspNetCore/Attributes/IPBlockListAttribute.cs index 9ca9c88..36d3e33 100644 --- a/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/IPBlockListAttribute.cs @@ -10,12 +10,12 @@ namespace Microsoft.AspNetCore.Mvc.Filters /// /// Implements an IP filter. The defined addresses are blocked. /// - public class IPBlacklistAttribute : ActionFilterAttribute + public class IPBlockListAttribute : ActionFilterAttribute { /// /// Gets or sets a value indicating whether local (localhost) access is blocked (Default: false). /// - public bool RestrictLocalAccess { get; set; } + public bool BlockLocalAccess { get; set; } /// /// Gets or sets a configuration key where the blocked IP addresses are defined. @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters /// /// Gets or sets a comma separated list of blocked IP addresses. /// - public string RestrictedIpAddresses { get; set; } + public string BlockedIpAddresses { get; set; } /// public override void OnActionExecuting(ActionExecutingContext context) @@ -43,13 +43,13 @@ namespace Microsoft.AspNetCore.Mvc.Filters base.OnActionExecuting(context); context.HttpContext.Items["RemoteAddress"] = context.HttpContext.GetRemoteIpAddress(); - if (!RestrictLocalAccess && context.HttpContext.IsLocalRequest()) + if (!BlockLocalAccess && context.HttpContext.IsLocalRequest()) return; var remoteIpAddress = context.HttpContext.GetRemoteIpAddress(); - if (!string.IsNullOrWhiteSpace(RestrictedIpAddresses)) + if (!string.IsNullOrWhiteSpace(BlockedIpAddresses)) { - string[] ipAddresses = RestrictedIpAddresses.Split(','); + string[] ipAddresses = BlockedIpAddresses.Split(','); foreach (string ipAddress in ipAddresses) { if (string.IsNullOrWhiteSpace(ipAddress)) diff --git a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs index d630d61..13f49c3 100644 --- a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Linq; +using System.Net; using Microsoft.AspNetCore.Antiforgery; using Microsoft.Extensions.DependencyInjection; @@ -9,30 +10,57 @@ namespace Microsoft.AspNetCore.Http /// public static class HttpContextExtensions { + // Search these additional headers for a remote client ip address. + private static readonly string[] defaultIpHeaderNames = new[] + { + "X-Forwarded-For", // commonly used on all known proxies + "X-Real-IP", // wide-spread alternative to X-Forwarded-For + "CF-Connecting-IP" // set by Cloudflare + }; + /// /// Retrieves the antiforgery token. /// /// The current . - /// Name and value of the token. - public static (string Name, string Value) GetAntiforgeryToken(this HttpContext httpContext) + /// FormName, HeaderName and Value of the antiforgery token. + public static (string FormName, string HeaderName, string Value) GetAntiforgeryToken(this HttpContext httpContext) { - var af = httpContext.RequestServices.GetService(); - var set = af?.GetAndStoreTokens(httpContext); + var antiforgery = httpContext.RequestServices.GetService(); + var tokenSet = antiforgery?.GetAndStoreTokens(httpContext); - return (Name: set?.FormFieldName, Value: set?.RequestToken); + return (tokenSet?.FormFieldName, tokenSet?.HeaderName, tokenSet?.RequestToken); } /// /// Returns the remote ip address. /// + /// + /// Searches for additional headers in the following order: + /// + /// X-Forwarded-For + /// X-Real-IP + /// CF-Connecting-IP + /// + /// /// The current . - /// The name of the header to resolve the when behind a proxy (Default: X-Forwarded-For). + /// The name of the header to resolve the when behind a proxy. /// The ip address of the client. - public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string headerName = "X-Forwarded-For") + public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string ipHeaderName = null) { - string forwardedHeader = httpContext.Request.Headers[headerName].ToString(); - if (!string.IsNullOrWhiteSpace(forwardedHeader) && IPAddress.TryParse(forwardedHeader, out var forwarded)) - return forwarded; + string forwardedForAddress = null; + + var headerNames = string.IsNullOrWhiteSpace(ipHeaderName) ? defaultIpHeaderNames : new[] { ipHeaderName }.Concat(defaultIpHeaderNames); + foreach (string headerName in headerNames) + { + if (!httpContext.Request.Headers.ContainsKey(headerName)) + continue; + + forwardedForAddress = httpContext.Request.Headers[headerName].ToString(); + break; + } + + if (!string.IsNullOrWhiteSpace(forwardedForAddress) && IPAddress.TryParse(forwardedForAddress, out var remoteAddress)) + return remoteAddress; return httpContext.Connection.RemoteIpAddress; } @@ -40,12 +68,20 @@ namespace Microsoft.AspNetCore.Http /// /// Returns whether the request was made locally. /// + /// + /// Searches for additional headers in the following order: + /// + /// X-Forwarded-For + /// X-Real-IP + /// CF-Connecting-IP + /// + /// /// The current . - /// The name of the header to resolve the when behind a proxy (Default: X-Forwarded-For). + /// The name of the header to resolve the when behind a proxy. /// - public static bool IsLocalRequest(this HttpContext httpContext, string headerName = "X-Forwarded-For") + public static bool IsLocalRequest(this HttpContext httpContext, string ipHeaderName = null) { - var remoteIpAddress = httpContext.GetRemoteIpAddress(headerName); + var remoteIpAddress = httpContext.GetRemoteIpAddress(ipHeaderName); return httpContext.Connection.LocalIpAddress.Equals(remoteIpAddress); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bae954..4ccaa58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.11.1...main) - 0000-00-00 +## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.12.0...main) - 0000-00-00 _no changes yet_ +## [v1.12.0](https://git.am-wd.de/AM.WD/common/compare/v1.11.1...v1.12.0) - 2023-06-01 + +### Changed + +- Renamed `IPBlacklistAttribute` to `IPBlockListAttribute` +- Renamed `IPWhitelistAttribute` to `IPAllowListAttribute` +- `HttpContextExtensions` + - `GetAntiforgeryToken()` now returns the header name also + - `GetRemoteIpAddress()` checks following additional headers by default: + - `X-Forwarded-For` + - `X-Real-IP` + - `CF-Connecting-IP` + + ## [v1.11.1](https://git.am-wd.de/AM.WD/common/compare/v1.11.0...v1.11.1) - 2023-05-11 ### Fixed diff --git a/Directory.Build.targets b/Directory.Build.targets index 84ed1ef..39a9eb1 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -6,8 +6,4 @@ - - - - diff --git a/UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs b/UnitTests/AspNetCore/Attributes/IPAllowListAttributeTests.cs similarity index 91% rename from UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs rename to UnitTests/AspNetCore/Attributes/IPAllowListAttributeTests.cs index 0545aac..12b2bc3 100644 --- a/UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs +++ b/UnitTests/AspNetCore/Attributes/IPAllowListAttributeTests.cs @@ -14,7 +14,7 @@ namespace UnitTests.AspNetCore.Attributes { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class IPWhitelistAttributeTests + public class IPAllowListAttributeTests { private Dictionary requestHeaders; private Dictionary itemsCallback; @@ -37,7 +37,7 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse("192.168.178.1"); - var attribute = new IPWhitelistAttribute(); + var attribute = new IPAllowListAttribute(); var context = GetContext(remote); // act @@ -57,7 +57,7 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse("192.168.178.1"); - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowedIpAddresses = "192.168.178:1" }; @@ -79,7 +79,7 @@ namespace UnitTests.AspNetCore.Attributes public void ShouldAllowLocalAccess() { // arrange - var attribute = new IPWhitelistAttribute(); + var attribute = new IPAllowListAttribute(); var context = GetContext(); // act @@ -95,7 +95,7 @@ namespace UnitTests.AspNetCore.Attributes public void ShouldDenyLocalAccess() { // arrange - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = false }; @@ -120,7 +120,7 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse(address); - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = false, AllowedIpAddresses = ",127.0.0.0/8,192.168.178.10" @@ -154,7 +154,7 @@ namespace UnitTests.AspNetCore.Attributes configExists = true; allowedIpsConfig.Add("127.0.0.0/8"); allowedIpsConfig.Add("192.168.178.10"); - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = true, ConfigurationKey = configKey @@ -178,7 +178,7 @@ namespace UnitTests.AspNetCore.Attributes configExists = true; allowedIpsConfig.Add(""); allowedIpsConfig.Add("192.168.178.10"); - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = false, ConfigurationKey = configKey @@ -206,7 +206,7 @@ namespace UnitTests.AspNetCore.Attributes configKey = "White:List"; configExists = true; allowedIpsConfig.Add("192.168.178.10"); - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = false, ConfigurationKey = configKey @@ -239,7 +239,7 @@ namespace UnitTests.AspNetCore.Attributes // arrange configKey = "White:List"; configExists = false; - var attribute = new IPWhitelistAttribute + var attribute = new IPAllowListAttribute { AllowLocalAccess = false, ConfigurationKey = configKey diff --git a/UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs b/UnitTests/AspNetCore/Attributes/IPBlockListAttributeTests.cs similarity index 87% rename from UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs rename to UnitTests/AspNetCore/Attributes/IPBlockListAttributeTests.cs index 5ca53c5..0062362 100644 --- a/UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs +++ b/UnitTests/AspNetCore/Attributes/IPBlockListAttributeTests.cs @@ -14,7 +14,7 @@ namespace UnitTests.AspNetCore.Attributes { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class IPBlacklistAttributeTests + public class IPBlockListAttributeTests { private Dictionary requestHeaders; private Dictionary itemsCallback; @@ -37,7 +37,7 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse("192.168.178.1"); - var attribute = new IPBlacklistAttribute(); + var attribute = new IPBlockListAttribute(); var context = GetContext(remote); // act @@ -54,9 +54,9 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse("192.168.178.1"); - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictedIpAddresses = "192.168.178:1" + BlockedIpAddresses = "192.168.178:1" }; var context = GetContext(remote); @@ -73,10 +73,10 @@ namespace UnitTests.AspNetCore.Attributes public void ShouldAllowLocalAccess() { // arrange - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = false, - RestrictedIpAddresses = "127.0.0.0/8" + BlockLocalAccess = false, + BlockedIpAddresses = "127.0.0.0/8" }; var context = GetContext(); @@ -93,10 +93,10 @@ namespace UnitTests.AspNetCore.Attributes public void ShouldBlockLocalAccess() { // arrange - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = true, - RestrictedIpAddresses = ",127.0.0.0/8" + BlockLocalAccess = true, + BlockedIpAddresses = ",127.0.0.0/8" }; var context = GetContext(); @@ -119,10 +119,10 @@ namespace UnitTests.AspNetCore.Attributes { // arrange var remote = IPAddress.Parse(address); - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = true, - RestrictedIpAddresses = "127.0.0.0/8,192.168.178.10" + BlockLocalAccess = true, + BlockedIpAddresses = "127.0.0.0/8,192.168.178.10" }; var context = GetContext(remote); @@ -153,9 +153,9 @@ namespace UnitTests.AspNetCore.Attributes configExists = true; restrictedIpsConfig.Add("127.0.0.0/8"); restrictedIpsConfig.Add("192.168.178.10"); - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = false, + BlockLocalAccess = false, ConfigurationKey = configKey }; var context = GetContext(); @@ -178,9 +178,9 @@ namespace UnitTests.AspNetCore.Attributes restrictedIpsConfig.Add(""); restrictedIpsConfig.Add("127.0.0.0/8"); restrictedIpsConfig.Add("192.168.178.10"); - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = true, + BlockLocalAccess = true, ConfigurationKey = configKey }; var context = GetContext(); @@ -207,9 +207,9 @@ namespace UnitTests.AspNetCore.Attributes configExists = true; restrictedIpsConfig.Add("127.0.0.0/8"); restrictedIpsConfig.Add("192.168.178.10"); - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = true, + BlockLocalAccess = true, ConfigurationKey = configKey }; var remote = IPAddress.Parse(address); @@ -240,9 +240,9 @@ namespace UnitTests.AspNetCore.Attributes // arrange configKey = "Black:List"; configExists = false; - var attribute = new IPBlacklistAttribute + var attribute = new IPBlockListAttribute { - RestrictLocalAccess = true, + BlockLocalAccess = true, ConfigurationKey = configKey }; var context = GetContext(); diff --git a/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs b/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs index 247459b..9a6dc52 100644 --- a/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs +++ b/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs @@ -14,7 +14,8 @@ namespace UnitTests.AspNetCore.Extensions { private Mock sessionMock; - private string tokenName; + private string tokenFormName; + private string tokenHeaderName; private string tokenValue; private Dictionary requestHeaders; @@ -26,7 +27,8 @@ namespace UnitTests.AspNetCore.Extensions [TestInitialize] public void InitializeTests() { - tokenName = null; + tokenFormName = null; + tokenHeaderName = null; tokenValue = null; requestHeaders = new Dictionary(); @@ -42,34 +44,38 @@ namespace UnitTests.AspNetCore.Extensions public void ShouldReturnAntiforgery() { // arrange - tokenName = "af-token"; + tokenFormName = "af-token"; + tokenHeaderName = "af-header"; tokenValue = "security_first"; var context = GetContext(); // act - var result = context.GetAntiforgeryToken(); + var (formName, headerName, value) = context.GetAntiforgeryToken(); // assert - Assert.AreEqual(tokenName, result.Name); - Assert.AreEqual(tokenValue, result.Value); + Assert.AreEqual(tokenFormName, formName); + Assert.AreEqual(tokenHeaderName, headerName); + Assert.AreEqual(tokenValue, value); } [TestMethod] public void ShouldReturnAntiforgeryNullService() { // arrange - tokenName = "af-token"; + tokenFormName = "af-token"; + tokenHeaderName = "af-header"; tokenValue = "security_first"; var context = GetContext(hasAntiforgery: false); // act - var result = context.GetAntiforgeryToken(); + var (formName, headerName, value) = context.GetAntiforgeryToken(); // assert - Assert.AreEqual(null, result.Name); - Assert.AreEqual(null, result.Value); + Assert.IsNull(formName); + Assert.IsNull(headerName); + Assert.IsNull(value); } [TestMethod] @@ -79,11 +85,12 @@ namespace UnitTests.AspNetCore.Extensions var context = GetContext(); // act - var result = context.GetAntiforgeryToken(); + var (formName, headerName, value) = context.GetAntiforgeryToken(); // assert - Assert.AreEqual(null, result.Name); - Assert.AreEqual(null, result.Value); + Assert.IsNull(formName); + Assert.IsNull(headerName); + Assert.IsNull(value); } #endregion Antiforgery @@ -105,13 +112,16 @@ namespace UnitTests.AspNetCore.Extensions Assert.AreEqual(remote, result); } - [TestMethod] - public void ShouldReturnDefaultHeader() + [DataTestMethod] + [DataRow("X-Forwarded-For")] + [DataRow("X-Real-IP")] + [DataRow("CF-Connecting-IP")] + public void ShouldReturnDefaultHeader(string headerName) { // arrange remote = IPAddress.Parse("1.2.3.4"); var header = IPAddress.Parse("5.6.7.8"); - requestHeaders.Add("X-Forwarded-For", header.ToString()); + requestHeaders.Add(headerName, header.ToString()); var context = GetContext(); @@ -130,12 +140,14 @@ namespace UnitTests.AspNetCore.Extensions remote = IPAddress.Parse("1.2.3.4"); string headerName = "FooBar"; var headerIp = IPAddress.Parse("5.6.7.8"); + requestHeaders.Add(headerName, headerIp.ToString()); + requestHeaders.Add("X-Forwarded-For", remote.ToString()); var context = GetContext(); // act - var result = context.GetRemoteIpAddress(headerName: headerName); + var result = context.GetRemoteIpAddress(ipHeaderName: headerName); // assert Assert.AreNotEqual(remote, result); @@ -221,7 +233,7 @@ namespace UnitTests.AspNetCore.Extensions var context = GetContext(); // act - bool result = context.IsLocalRequest(headerName: headerName); + bool result = context.IsLocalRequest(ipHeaderName: headerName); // assert Assert.IsTrue(result); @@ -254,7 +266,7 @@ namespace UnitTests.AspNetCore.Extensions var context = GetContext(); // act - bool result = context.IsLocalRequest(headerName: headerName); + bool result = context.IsLocalRequest(ipHeaderName: headerName); // assert Assert.IsFalse(result); @@ -385,7 +397,7 @@ namespace UnitTests.AspNetCore.Extensions var antiforgeryMock = new Mock(); antiforgeryMock .Setup(af => af.GetAndStoreTokens(It.IsAny())) - .Returns(string.IsNullOrWhiteSpace(tokenName) ? null : new AntiforgeryTokenSet(tokenValue, tokenValue, tokenName, tokenName)); + .Returns(() => string.IsNullOrWhiteSpace(tokenValue) ? null : new AntiforgeryTokenSet(tokenValue, tokenValue, tokenFormName, tokenHeaderName)); requestServicesMock .Setup(rs => rs.GetService(typeof(IAntiforgery))) From 28377a89eb78dd66ebd963bddb9b63ced3e4e8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 27 Jun 2023 11:01:54 +0200 Subject: [PATCH 03/21] Added new middleware to protect paths by authorization (also static files) --- .../BasicAuthenticationAttribute.cs | 2 +- .../BasicAuthenticationHandler.cs | 2 +- .../BasicAuthenticationMiddleware.cs | 2 +- .../IBasicAuthenticationValidator.cs | 2 +- .../PathProtection/ProtectedPathExtensions.cs | 19 +++ .../PathProtection/ProtectedPathMiddleware.cs | 50 +++++++ .../PathProtection/ProtectedPathOptions.cs | 20 +++ CHANGELOG.md | 13 +- .../BasicAuthenticationAttributeTests.cs | 2 +- .../BasicAuthenticationMiddlewareTests.cs | 4 +- .../ProtectedPathMiddlewareTests.cs | 141 ++++++++++++++++++ 11 files changed, 249 insertions(+), 8 deletions(-) rename AMWD.Common.AspNetCore/{ => Security}/BasicAuthentication/BasicAuthenticationHandler.cs (95%) rename AMWD.Common.AspNetCore/{ => Security}/BasicAuthentication/BasicAuthenticationMiddleware.cs (95%) rename AMWD.Common.AspNetCore/{ => Security}/BasicAuthentication/IBasicAuthenticationValidator.cs (91%) create mode 100644 AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathExtensions.cs create mode 100644 AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs create mode 100644 AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathOptions.cs rename UnitTests/AspNetCore/{ => Security}/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs (95%) create mode 100644 UnitTests/AspNetCore/Security/PathProtection/ProtectedPathMiddlewareTests.cs diff --git a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs index 3a357d5..56a17ad 100644 --- a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs @@ -4,7 +4,7 @@ using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using AMWD.Common.AspNetCore.BasicAuthentication; +using AMWD.Common.AspNetCore.Security.BasicAuthentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs similarity index 95% rename from AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs rename to AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs index 6d0f33b..8ce5bc3 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace AMWD.Common.AspNetCore.BasicAuthentication +namespace AMWD.Common.AspNetCore.Security.BasicAuthentication { /// /// Implements the for Basic Authentication. diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs similarity index 95% rename from AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs rename to AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs index 4e14801..35268f8 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace AMWD.Common.AspNetCore.BasicAuthentication +namespace AMWD.Common.AspNetCore.Security.BasicAuthentication { /// /// Implements a basic authentication. diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/IBasicAuthenticationValidator.cs similarity index 91% rename from AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs rename to AMWD.Common.AspNetCore/Security/BasicAuthentication/IBasicAuthenticationValidator.cs index 47fd1ef..0d440d1 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/IBasicAuthenticationValidator.cs @@ -3,7 +3,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -namespace AMWD.Common.AspNetCore.BasicAuthentication +namespace AMWD.Common.AspNetCore.Security.BasicAuthentication { /// /// Interface representing the validation of a basic authentication. diff --git a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathExtensions.cs b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathExtensions.cs new file mode 100644 index 0000000..cf56de0 --- /dev/null +++ b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; + +namespace AMWD.Common.AspNetCore.Security.PathProtection +{ + /// + /// Extnsion for to enable folder protection. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public static class ProtectedPathExtensions + { + /// + /// Provide protected paths even for static files. + /// + /// The . + /// The with path and policy name. + public static IApplicationBuilder UseProtectedPath(this IApplicationBuilder app, ProtectedPathOptions options) + => app.UseMiddleware(options); + } +} diff --git a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs new file mode 100644 index 0000000..3982333 --- /dev/null +++ b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace AMWD.Common.AspNetCore.Security.PathProtection +{ + /// + /// Implements a check to provide protected paths. + /// + public class ProtectedPathMiddleware + { + private readonly RequestDelegate next; + private readonly PathString path; + private readonly string policyName; + + /// + /// Initializes a new instance of the class. + /// + /// The following delegate in the process chain. + /// The options to configure the middleware. + public ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options) + { + this.next = next; + path = options.Path; + policyName = options.PolicyName; + } + + /// + /// The delegate invokation. + /// Performs the protection check. + /// + /// The corresponding HTTP context. + /// The . + /// An awaitable task. + public async Task InvokeAsync(HttpContext httpContext, IAuthorizationService authorizationService) + { + if (httpContext.Request.Path.StartsWithSegments(path)) + { + var result = await authorizationService.AuthorizeAsync(httpContext.User, null, policyName); + if (!result.Succeeded) + { + await httpContext.ChallengeAsync(); + return; + } + } + await next.Invoke(httpContext); + } + } +} diff --git a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathOptions.cs b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathOptions.cs new file mode 100644 index 0000000..aa46d86 --- /dev/null +++ b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathOptions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; + +namespace AMWD.Common.AspNetCore.Security.PathProtection +{ + /// + /// Options to define which folder should be protected. + /// + public class ProtectedPathOptions + { + /// + /// Gets or sets the path to the protected folder. + /// + public PathString Path { get; set; } + + /// + /// Gets or sets the policy name to use. + /// + public string PolicyName { get; set; } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ccaa58..aed38b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.12.0...main) - 0000-00-00 +## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.13.0...main) - 0000-00-00 _no changes yet_ +## [v1.13.0](https://git.am-wd.de/AM.WD/common/compare/v1.12.0...v1.13.0) - 2023-06-27 + +### Added + +- `ProtectedPathMiddleware` to secure even static file paths + +### Changed + +- Moved `BasicAuthentication`* into sub-namespace `Security` + + ## [v1.12.0](https://git.am-wd.de/AM.WD/common/compare/v1.11.1...v1.12.0) - 2023-06-01 ### Changed diff --git a/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs b/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs index 31b490c..6984486 100644 --- a/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs +++ b/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs @@ -5,7 +5,7 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; -using AMWD.Common.AspNetCore.BasicAuthentication; +using AMWD.Common.AspNetCore.Security.BasicAuthentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs similarity index 95% rename from UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs rename to UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs index 3166753..e942f39 100644 --- a/UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs +++ b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs @@ -6,13 +6,13 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; -using AMWD.Common.AspNetCore.BasicAuthentication; +using AMWD.Common.AspNetCore.Security.BasicAuthentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace UnitTests.AspNetCore.BasicAuthentication +namespace UnitTests.AspNetCore.Security.BasicAuthentication { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/UnitTests/AspNetCore/Security/PathProtection/ProtectedPathMiddlewareTests.cs b/UnitTests/AspNetCore/Security/PathProtection/ProtectedPathMiddlewareTests.cs new file mode 100644 index 0000000..e31c9ff --- /dev/null +++ b/UnitTests/AspNetCore/Security/PathProtection/ProtectedPathMiddlewareTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using AMWD.Common.AspNetCore.Security.PathProtection; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.Security.PathProtection +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ProtectedPathMiddlewareTests + { + private Mock nextMock; + private Mock httpContextMock; + private Mock authorizationServiceMock; + private Mock authenticationServiceMock; + + private ProtectedPathOptions options; + + [TestInitialize] + public void InitializeTest() + { + options = new ProtectedPathOptions + { + Path = "/secure/protected", + PolicyName = "Protection" + }; + } + + [TestMethod] + public async Task ShouldValidateAccessSuccessful() + { + // arrange + var middleware = GetMiddleware(); + var context = GetHttpContext(options.Path); + var auth = GetAuthService(true); + + // act + await middleware.InvokeAsync(context, auth); + + // assert + authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny(), It.IsAny(), options.PolicyName), Times.Once); + authorizationServiceMock.VerifyNoOtherCalls(); + + authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + authenticationServiceMock.VerifyNoOtherCalls(); + + nextMock.Verify(n => n.Invoke(It.IsAny()), Times.Once); + nextMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldNotValidate() + { + // arrange + var middleware = GetMiddleware(); + var context = GetHttpContext("/some/path"); + var auth = GetAuthService(true); + + // act + await middleware.InvokeAsync(context, auth); + + // assert + authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny(), It.IsAny(), options.PolicyName), Times.Never); + authorizationServiceMock.VerifyNoOtherCalls(); + + authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + authenticationServiceMock.VerifyNoOtherCalls(); + + nextMock.Verify(n => n.Invoke(It.IsAny()), Times.Once); + nextMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldValidateAccessFailure() + { + // arrange + var middleware = GetMiddleware(); + var context = GetHttpContext(options.Path); + var auth = GetAuthService(false); + + // act + await middleware.InvokeAsync(context, auth); + + // assert + authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny(), It.IsAny(), options.PolicyName), Times.Once); + authorizationServiceMock.VerifyNoOtherCalls(); + + authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + authenticationServiceMock.VerifyNoOtherCalls(); + + nextMock.Verify(n => n.Invoke(It.IsAny()), Times.Never); + nextMock.VerifyNoOtherCalls(); + } + + private ProtectedPathMiddleware GetMiddleware() + { + nextMock = new Mock(); + return new ProtectedPathMiddleware(nextMock.Object, options); + } + + private HttpContext GetHttpContext(string requestPath) + { + var requestMock = new Mock(); + requestMock + .Setup(r => r.Path) + .Returns(new PathString(requestPath)); + + authenticationServiceMock = new Mock(); + + var requestServicesMock = new Mock(); + requestServicesMock + .Setup(s => s.GetService(typeof(IAuthenticationService))) + .Returns(authenticationServiceMock.Object); + + httpContextMock = new Mock(); + httpContextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + httpContextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + + return httpContextMock.Object; + } + + private IAuthorizationService GetAuthService(bool success) + { + authorizationServiceMock = new Mock(); + authorizationServiceMock + .Setup(service => service.AuthorizeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(() => success ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + return authorizationServiceMock.Object; + } + } +} From 7469583ed44b5ea213f39e58dd46830acb84ca5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 1 Aug 2023 08:00:45 +0200 Subject: [PATCH 04/21] Removing .NET Core 3.1 support --- .../AMWD.Common.AspNetCore.csproj | 10 +++------ .../AMWD.Common.EntityFrameworkCore.csproj | 22 +++++++------------ .../Converters/DateOnlyConverter.cs | 6 +---- .../Converters/NullableDateOnlyConverter.cs | 6 +---- .../Converters/NullableTimeOnlyConverter.cs | 6 +---- .../Converters/TimeOnlyConverter.cs | 6 +---- .../Extensions/ModelBuilderExtensions.cs | 22 ------------------- .../ModelConfigurationBuilderExtensions.cs | 6 +---- AMWD.Common.Moq/AMWD.Common.Moq.csproj | 4 ---- AMWD.Common/AMWD.Common.csproj | 8 ++----- AMWD.Common/Extensions/StreamExtensions.cs | 14 ++++-------- Directory.Build.props | 7 ++++++ UnitTests/UnitTests.csproj | 10 ++++----- 13 files changed, 34 insertions(+), 93 deletions(-) diff --git a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj index d960fa8..19c391f 100644 --- a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj +++ b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj @@ -1,8 +1,8 @@ - + Debug;Release;DebugLocal - netcoreapp3.1;net6.0 + net6.0;net7.0 10.0 AMWD.Common.AspNetCore @@ -30,12 +30,8 @@ - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file diff --git a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj index 6d425e2..d6697c1 100644 --- a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj +++ b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ Debug;Release;DebugLocal - netcoreapp3.1;net6.0 + net6.0;net7.0 10.0 AMWD.Common.EntityFrameworkCore @@ -28,24 +28,18 @@ - - - - - - - - + + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + diff --git a/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs index 460e6a7..6dd9ea2 100644 --- a/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs +++ b/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER - -using System; +using System; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace AMWD.Common.EntityFrameworkCore.Converters @@ -24,5 +22,3 @@ namespace AMWD.Common.EntityFrameworkCore.Converters { } } } - -#endif diff --git a/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs index 2587912..a002be5 100644 --- a/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs +++ b/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER - -using System; +using System; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace AMWD.Common.EntityFrameworkCore.Converters @@ -24,5 +22,3 @@ namespace AMWD.Common.EntityFrameworkCore.Converters { } } } - -#endif diff --git a/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs index bafc8ca..5399ace 100644 --- a/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs +++ b/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER - -using System; +using System; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace AMWD.Common.EntityFrameworkCore.Converters @@ -24,5 +22,3 @@ namespace AMWD.Common.EntityFrameworkCore.Converters { } } } - -#endif diff --git a/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs index acb4ccc..4a56df5 100644 --- a/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs +++ b/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER - -using System; +using System; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace AMWD.Common.EntityFrameworkCore.Converters @@ -24,5 +22,3 @@ namespace AMWD.Common.EntityFrameworkCore.Converters { } } } - -#endif diff --git a/AMWD.Common.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs b/AMWD.Common.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs index cccaf0c..0b5329a 100644 --- a/AMWD.Common.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs +++ b/AMWD.Common.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs @@ -3,9 +3,7 @@ using System.Reflection; using System.Text; using AMWD.Common.EntityFrameworkCore.Attributes; using Microsoft.EntityFrameworkCore; -#if NET6_0_OR_GREATER using Microsoft.EntityFrameworkCore.Metadata; -#endif namespace AMWD.Common.EntityFrameworkCore.Extensions { @@ -35,13 +33,7 @@ namespace AMWD.Common.EntityFrameworkCore.Extensions index.IsUnique = indexAttribute.IsUnique; if (!string.IsNullOrWhiteSpace(indexAttribute.Name)) - { -#if NET6_0_OR_GREATER index.SetDatabaseName(indexAttribute.Name.Trim()); -#else - index.SetName(indexAttribute.Name.Trim()); -#endif - } } } } @@ -60,28 +52,14 @@ namespace AMWD.Common.EntityFrameworkCore.Extensions { // skip conversion when table name is explicitly set if ((entityType.ClrType.GetCustomAttribute(typeof(TableAttribute), false) as TableAttribute) == null) - { -#if NET6_0_OR_GREATER entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName())); -#else - entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName())); -#endif - } -#if NET6_0_OR_GREATER var identifier = StoreObjectIdentifier.Table(entityType.GetTableName(), entityType.GetSchema()); -#endif foreach (var property in entityType.GetProperties()) { // skip conversion when column name is explicitly set if ((entityType.ClrType.GetProperty(property.Name)?.GetCustomAttribute(typeof(ColumnAttribute), false) as ColumnAttribute) == null) - { -#if NET6_0_OR_GREATER property.SetColumnName(ConvertToSnakeCase(property.GetColumnName(identifier))); -#else - property.SetColumnName(ConvertToSnakeCase(property.GetColumnName())); -#endif - } } } diff --git a/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs b/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs index 8c8f8b1..18289d2 100644 --- a/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs +++ b/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER - -using System; +using System; using AMWD.Common.EntityFrameworkCore.Converters; using Microsoft.EntityFrameworkCore; @@ -39,5 +37,3 @@ namespace AMWD.Common.EntityFrameworkCore.Extensions } } } - -#endif diff --git a/AMWD.Common.Moq/AMWD.Common.Moq.csproj b/AMWD.Common.Moq/AMWD.Common.Moq.csproj index 9f28ded..a57cd5d 100644 --- a/AMWD.Common.Moq/AMWD.Common.Moq.csproj +++ b/AMWD.Common.Moq/AMWD.Common.Moq.csproj @@ -30,10 +30,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/AMWD.Common/AMWD.Common.csproj b/AMWD.Common/AMWD.Common.csproj index 333d45a..07eca69 100644 --- a/AMWD.Common/AMWD.Common.csproj +++ b/AMWD.Common/AMWD.Common.csproj @@ -29,14 +29,10 @@ - + - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/AMWD.Common/Extensions/StreamExtensions.cs b/AMWD.Common/Extensions/StreamExtensions.cs index 0b8c0a2..c56409e 100644 --- a/AMWD.Common/Extensions/StreamExtensions.cs +++ b/AMWD.Common/Extensions/StreamExtensions.cs @@ -20,11 +20,8 @@ namespace System.IO /// public static string ReadLine(this Stream stream, Encoding encoding = null, char? eol = null) { - if (encoding == null) - encoding = Encoding.Default; - - if (eol == null) - eol = Environment.NewLine.Last(); + encoding ??= Encoding.Default; + eol ??= Environment.NewLine.Last(); if (!stream.CanRead) return null; @@ -56,11 +53,8 @@ namespace System.IO /// public static async Task ReadLineAsync(this Stream stream, Encoding encoding = null, char? eol = null, CancellationToken cancellationToken = default) { - if (encoding == null) - encoding = Encoding.Default; - - if (eol == null) - eol = Environment.NewLine.Last(); + encoding ??= Encoding.Default; + eol ??= Environment.NewLine.Last(); if (!stream.CanRead) return null; diff --git a/Directory.Build.props b/Directory.Build.props index e903063..325a8a7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,4 +22,11 @@ © {copyright:2020-} AM.WD MIT + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 26dc979..1c14201 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -9,16 +9,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + From 0d7e5912b870947cbded81e95d14473612ff78ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 9 Aug 2023 06:51:47 +0200 Subject: [PATCH 05/21] Added CommandLineParser to AMWD.Common collection --- .../AMWD.Common.AspNetCore.csproj | 2 +- .../AMWD.Common.EntityFrameworkCore.csproj | 11 +- AMWD.Common/Cli/Argument.cs | 36 ++ AMWD.Common/Cli/CommandLineParser.cs | 366 ++++++++++++++++++ AMWD.Common/Cli/EnumerableWalker.cs | 58 +++ AMWD.Common/Cli/Option.cs | 113 ++++++ CHANGELOG.md | 9 +- .../Common/Cli/CommandLineParserTests.cs | 306 +++++++++++++++ UnitTests/Common/Cli/EnumerableWalkerTests.cs | 96 +++++ 9 files changed, 986 insertions(+), 11 deletions(-) create mode 100644 AMWD.Common/Cli/Argument.cs create mode 100644 AMWD.Common/Cli/CommandLineParser.cs create mode 100644 AMWD.Common/Cli/EnumerableWalker.cs create mode 100644 AMWD.Common/Cli/Option.cs create mode 100644 UnitTests/Common/Cli/CommandLineParserTests.cs create mode 100644 UnitTests/Common/Cli/EnumerableWalkerTests.cs diff --git a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj index 19c391f..34fa151 100644 --- a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj +++ b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj @@ -2,7 +2,7 @@ Debug;Release;DebugLocal - net6.0;net7.0 + net6.0 10.0 AMWD.Common.AspNetCore diff --git a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj index d6697c1..abe3b58 100644 --- a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj +++ b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ Debug;Release;DebugLocal - net6.0;net7.0 + net6.0 10.0 AMWD.Common.EntityFrameworkCore @@ -28,18 +28,11 @@ - + - - - - - - - diff --git a/AMWD.Common/Cli/Argument.cs b/AMWD.Common/Cli/Argument.cs new file mode 100644 index 0000000..79a0b9b --- /dev/null +++ b/AMWD.Common/Cli/Argument.cs @@ -0,0 +1,36 @@ +namespace AMWD.Common.Cli +{ + /// + /// Represents a logical argument in the command line. Options with their additional + /// parameters are combined in one argument. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class Argument + { + /// + /// Initialises a new instance of the class. + /// + /// The that is set in this argument; or null. + /// The additional parameter values for the option; or the argument value. + internal Argument(Option option, string[] values) + { + Option = option; + Values = values; + } + + /// + /// Gets the that is set in this argument; or null. + /// + public Option Option { get; private set; } + + /// + /// Gets the additional parameter values for the option; or the argument value. + /// + public string[] Values { get; private set; } + + /// + /// Gets the first item of ; or null. + /// + public string Value => Values.Length > 0 ? Values[0] : null; + } +} diff --git a/AMWD.Common/Cli/CommandLineParser.cs b/AMWD.Common/Cli/CommandLineParser.cs new file mode 100644 index 0000000..2936d2a --- /dev/null +++ b/AMWD.Common/Cli/CommandLineParser.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AMWD.Common.Cli +{ + /// + /// Provides options and arguments parsing from command line arguments or a single string. + /// + public class CommandLineParser + { + #region Private data + + private string[] args; + private List parsedArguments; + private readonly List