diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 616d641751..4045421eb1 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -46,6 +46,7 @@ namespace JsonSchema public UnattendedSettings Unattended { get; set; } public RichTextEditorSettings RichTextEditor { get; set; } public RuntimeMinificationSettings RuntimeMinification { get; set; } + public BasicAuthSettings BasicAuth { get; set; } } /// diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Core/Services/BasicAuthService.cs new file mode 100644 index 0000000000..99a6f930c5 --- /dev/null +++ b/src/Umbraco.Core/Services/BasicAuthService.cs @@ -0,0 +1,33 @@ +using System.Net; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Services +{ + public class BasicAuthService : IBasicAuthService + { + private BasicAuthSettings _basicAuthSettings; + + public BasicAuthService(IOptionsMonitor optionsMonitor) + { + _basicAuthSettings = optionsMonitor.CurrentValue; + + optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings); + } + + public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled; + + public bool IsIpAllowListed(IPAddress clientIpAddress) + { + foreach (var allowedIpString in _basicAuthSettings.AllowedIPs) + { + if(IPAddress.TryParse(allowedIpString, out var allowedIp) && clientIpAddress.Equals(allowedIp)) + { + return true; + }; + } + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs new file mode 100644 index 0000000000..84173a629a --- /dev/null +++ b/src/Umbraco.Core/Services/IBasicAuthService.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace Umbraco.Cms.Core.Services +{ + public interface IBasicAuthService + { + bool IsBasicAuthEnabled(); + bool IsIpAllowListed(IPAddress clientIpAddress); + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 8b34289c9c..e535b399e4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -40,6 +40,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs new file mode 100644 index 0000000000..f406acc03d --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Net; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services +{ + [TestFixture] + public class BasicAuthServiceTests + { + [TestCase(true, ExpectedResult = true)] + [TestCase(false, ExpectedResult = false)] + public bool IsBasicAuthEnabled(bool enabled) + { + var sut = new BasicAuthService(Mock.Of>(_ => _.CurrentValue == new BasicAuthSettings() {Enabled = enabled})); + + return sut.IsBasicAuthEnabled(); + } + + [TestCase("::1", "1.1.1.1", ExpectedResult = false)] + [TestCase("::1", "1.1.1.1, ::1", ExpectedResult = true)] + [TestCase("127.0.0.1", "127.0.0.1, ::1", ExpectedResult = true)] + [TestCase("127.0.0.1", "", ExpectedResult = false)] + public bool IsBasicAuthEnabled(string clientIpAddress, string commaSeperatedAllowlist) + { + var allowedIPs = commaSeperatedAllowlist.Split(",").Select(x=>x.Trim()).ToArray(); + var sut = new BasicAuthService(Mock.Of>(_ => _.CurrentValue == new BasicAuthSettings() {AllowedIPs = allowedIPs})); + + return sut.IsIpAllowListed(IPAddress.Parse(clientIpAddress)); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs index 3c04fe5f40..9754c2b0ab 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs @@ -30,7 +30,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security var mgr = new BackOfficeCookieManager( Mock.Of(), runtime, - new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment())); + new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment()), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -48,7 +49,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -69,7 +71,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest(remainingTimeoutSecondsPath); Assert.IsTrue(result); @@ -90,7 +93,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/notbackoffice"); Assert.IsFalse(result); diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs index a036858929..7027285bf3 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs @@ -1,15 +1,11 @@ using System; -using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Extensions; @@ -22,32 +18,28 @@ namespace Umbraco.Cms.Web.Common.Middleware /// public class BasicAuthAuthenticationMiddleware : IMiddleware { - private readonly ILogger _logger; - private readonly IOptionsSnapshot _basicAuthSettings; private readonly IRuntimeState _runtimeState; + private readonly IBasicAuthService _basicAuthService; public BasicAuthAuthenticationMiddleware( - ILogger logger, - IOptionsSnapshot basicAuthSettings, - IRuntimeState runtimeState) + IRuntimeState runtimeState, + IBasicAuthService basicAuthService) { - _logger = logger; - _basicAuthSettings = basicAuthSettings; _runtimeState = runtimeState; + _basicAuthService = basicAuthService; } /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - var options = _basicAuthSettings.Value; - if (!options.Enabled || _runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level < RuntimeLevel.Run || !_basicAuthService.IsBasicAuthEnabled()) { await next(context); return; } var clientIPAddress = context.Connection.RemoteIpAddress; - if (IsIpAllowListed(clientIPAddress, options.AllowedIPs)) + if (_basicAuthService.IsIpAllowListed(clientIPAddress)) { await next(context); return; @@ -98,18 +90,7 @@ namespace Umbraco.Cms.Web.Common.Middleware } } - private bool IsIpAllowListed(IPAddress clientIpAddress, string[] allowlist) - { - foreach (var allowedIpString in allowlist) - { - if(IPAddress.TryParse(allowedIpString, out var allowedIp) && clientIpAddress.Equals(allowedIp)) - { - return true; - }; - } - return false; - } private static void SetUnauthorizedHeader(HttpContext context) { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index 5ba2fff613..5d50981f6a 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security private readonly IRuntimeState _runtime; private readonly string[] _explicitPaths; private readonly UmbracoRequestPaths _umbracoRequestPaths; - private readonly IOptionsMonitor _basicAuthSettingsMonitor; + private readonly IBasicAuthService _basicAuthService; /// /// Initializes a new instance of the class. @@ -34,8 +34,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor basicAuthSettings) - : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthSettings) + IBasicAuthService basicAuthService) + : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthService) { } @@ -47,13 +47,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security IRuntimeState runtime, IEnumerable explicitPaths, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor basicAuthSettingsMonitor) + IBasicAuthService basicAuthService) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; _explicitPaths = explicitPaths?.ToArray(); _umbracoRequestPaths = umbracoRequestPaths; - _basicAuthSettingsMonitor = basicAuthSettingsMonitor; + _basicAuthService = basicAuthService; } /// @@ -94,7 +94,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security return true; } - if (_basicAuthSettingsMonitor.CurrentValue.Enabled) + if (_basicAuthService.IsBasicAuthEnabled()) { return true; } diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 2d87735cab..1457732c53 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -34,6 +34,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security private readonly IIpResolver _ipResolver; private readonly ISystemClock _systemClock; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IBasicAuthService _basicAuthService; private readonly IOptionsMonitor _optionsSnapshot; /// @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security IIpResolver ipResolver, ISystemClock systemClock, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor optionsSnapshot) + IBasicAuthService basicAuthService) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -74,7 +75,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _ipResolver = ipResolver; _systemClock = systemClock; _umbracoRequestPaths = umbracoRequestPaths; - _optionsSnapshot = optionsSnapshot; + _basicAuthService = basicAuthService; } /// @@ -119,7 +120,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _umbracoContextAccessor, _runtimeState, _umbracoRequestPaths, - _optionsSnapshot + _basicAuthService ); options.Events = new CookieAuthenticationEvents