diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js index bb20a9305..d3946b5bd 100644 --- a/frontend/src/Settings/General/SecuritySettings.js +++ b/frontend/src/Settings/General/SecuritySettings.js @@ -17,6 +17,11 @@ const authenticationMethodOptions = [ { key: 'forms', value: 'Forms (Login Page)' } ]; +const authenticationRequiredOptions = [ + { key: 'enabled', value: 'Enabled' }, + { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' } +]; + const certificateValidationOptions = [ { key: 'enabled', value: 'Enabled' }, { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }, @@ -68,6 +73,7 @@ class SecuritySettings extends Component { const { authenticationMethod, + authenticationRequired, username, password, apiKey, @@ -94,7 +100,24 @@ class SecuritySettings extends Component { { - authenticationEnabled && + authenticationEnabled ? + + Authentication Required + + + : + null + } + + { + authenticationEnabled ? {translate('Username')} @@ -106,11 +129,12 @@ class SecuritySettings extends Component { onChange={onInputChange} {...username} /> - + : + null } { - authenticationEnabled && + authenticationEnabled ? {translate('Password')} @@ -122,7 +146,8 @@ class SecuritySettings extends Component { onChange={onInputChange} {...password} /> - + : + null } diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index 615e73cb6..3351ceaac 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner.KillAll(); - _runner.Start(); + _runner.Start(true); driver.Url = "http://localhost:8787"; diff --git a/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs b/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs new file mode 100644 index 000000000..dc3c2c770 --- /dev/null +++ b/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Authentication +{ + public enum AuthenticationRequiredType + { + Enabled = 0, + DisabledForLocalAddresses = 1 + } +} diff --git a/src/NzbDrone.Core/Authentication/AuthenticationType.cs b/src/NzbDrone.Core/Authentication/AuthenticationType.cs index 9f21b07a7..ca408774b 100644 --- a/src/NzbDrone.Core/Authentication/AuthenticationType.cs +++ b/src/NzbDrone.Core/Authentication/AuthenticationType.cs @@ -1,9 +1,10 @@ -namespace NzbDrone.Core.Authentication +namespace NzbDrone.Core.Authentication { public enum AuthenticationType { None = 0, Basic = 1, - Forms = 2 + Forms = 2, + External = 3 } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 94eaa67a8..9765cdccd 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration bool EnableSsl { get; } bool LaunchBrowser { get; } AuthenticationType AuthenticationMethod { get; } + AuthenticationRequiredType AuthenticationRequired { get; } bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } @@ -190,6 +191,8 @@ namespace NzbDrone.Core.Configuration } } + public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); // TODO: Change back to "master" for the first stable release diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index b14dc14b4..766a9c8bd 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -171,6 +171,8 @@ namespace NzbDrone.Host .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); services.AddSingleton(); + services.AddSingleton(); + services.AddAuthorization(options => { options.AddPolicy("SignalR", policy => diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index a84864c5d..b387e24b9 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -38,12 +38,12 @@ namespace NzbDrone.Test.Common Port = port; } - public void Start() + public void Start(bool enableAuth = false) { AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID()); Directory.CreateDirectory(AppData); - GenerateConfigFile(); + GenerateConfigFile(enableAuth); string readarrConsoleExe; if (OsInfo.IsWindows) @@ -178,7 +178,7 @@ namespace NzbDrone.Test.Common } } - private void GenerateConfigFile() + private void GenerateConfigFile(bool enableAuth) { var configFile = Path.Combine(AppData, "config.xml"); @@ -191,6 +191,8 @@ namespace NzbDrone.Test.Common new XElement(nameof(ConfigFileProvider.ApiKey), apiKey), new XElement(nameof(ConfigFileProvider.LogLevel), "trace"), new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false), + new XElement(nameof(ConfigFileProvider.AuthenticationMethod), enableAuth ? "Forms" : "None"), + new XElement(nameof(ConfigFileProvider.AuthenticationRequired), "DisabledForLocalAddresses"), new XElement(nameof(ConfigFileProvider.Port), Port))); var data = xDoc.ToString(); diff --git a/src/Readarr.Api.V1/Config/HostConfigResource.cs b/src/Readarr.Api.V1/Config/HostConfigResource.cs index 7ca7393ea..ecd4b8ee0 100644 --- a/src/Readarr.Api.V1/Config/HostConfigResource.cs +++ b/src/Readarr.Api.V1/Config/HostConfigResource.cs @@ -15,6 +15,7 @@ namespace Readarr.Api.V1.Config public bool EnableSsl { get; set; } public bool LaunchBrowser { get; set; } public AuthenticationType AuthenticationMethod { get; set; } + public AuthenticationRequiredType AuthenticationRequired { get; set; } public bool AnalyticsEnabled { get; set; } public string Username { get; set; } public string Password { get; set; } @@ -57,6 +58,7 @@ namespace Readarr.Api.V1.Config EnableSsl = model.EnableSsl, LaunchBrowser = model.LaunchBrowser, AuthenticationMethod = model.AuthenticationMethod, + AuthenticationRequired = model.AuthenticationRequired, AnalyticsEnabled = model.AnalyticsEnabled, //Username diff --git a/src/Readarr.Http/Authentication/ApiKeyAuthenticationHandler.cs b/src/Readarr.Http/Authentication/ApiKeyAuthenticationHandler.cs index f0b35afdc..c41eb55be 100644 --- a/src/Readarr.Http/Authentication/ApiKeyAuthenticationHandler.cs +++ b/src/Readarr.Http/Authentication/ApiKeyAuthenticationHandler.cs @@ -13,6 +13,7 @@ namespace Readarr.Http.Authentication public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions { public const string DefaultScheme = "API Key"; + public string Scheme => DefaultScheme; public string AuthenticationType = DefaultScheme; diff --git a/src/Readarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Readarr.Http/Authentication/AuthenticationBuilderExtensions.cs index e0d78555d..0507830d9 100644 --- a/src/Readarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Readarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -22,10 +22,16 @@ namespace Readarr.Http.Authentication return authenticationBuilder.AddScheme(name, options => { }); } + public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authenticationBuilder, string name) + { + return authenticationBuilder.AddScheme(name, options => { }); + } + public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) { return services.AddAuthentication() .AddNone(AuthenticationType.None.ToString()) + .AddExternal(AuthenticationType.External.ToString()) .AddBasic(AuthenticationType.Basic.ToString()) .AddCookie(AuthenticationType.Forms.ToString(), options => { diff --git a/src/Readarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs b/src/Readarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs new file mode 100644 index 000000000..3ad4edcba --- /dev/null +++ b/src/Readarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace NzbDrone.Http.Authentication +{ + public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement + { + } +} diff --git a/src/Readarr.Http/Authentication/UiAuthorizationHandler.cs b/src/Readarr.Http/Authentication/UiAuthorizationHandler.cs new file mode 100644 index 000000000..d8f08f3ea --- /dev/null +++ b/src/Readarr.Http/Authentication/UiAuthorizationHandler.cs @@ -0,0 +1,45 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Messaging.Events; +using Readarr.Http.Extensions; + +namespace NzbDrone.Http.Authentication +{ + public class UiAuthorizationHandler : AuthorizationHandler, IAuthorizationRequirement, IHandle + { + private readonly IConfigFileProvider _configService; + private static AuthenticationRequiredType _authenticationRequired; + + public UiAuthorizationHandler(IConfigFileProvider configService) + { + _configService = configService; + _authenticationRequired = configService.AuthenticationRequired; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BypassableDenyAnonymousAuthorizationRequirement requirement) + { + if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses) + { + if (context.Resource is HttpContext httpContext && + IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) && + ipAddress.IsLocalAddress()) + { + context.Succeed(requirement); + } + } + + return Task.CompletedTask; + } + + public void Handle(ConfigSavedEvent message) + { + _authenticationRequired = _configService.AuthenticationRequired; + } + } +} diff --git a/src/Readarr.Http/Authentication/UiAuthorizationPolicyProvider.cs b/src/Readarr.Http/Authentication/UiAuthorizationPolicyProvider.cs index a5295a99f..50f1c3ada 100644 --- a/src/Readarr.Http/Authentication/UiAuthorizationPolicyProvider.cs +++ b/src/Readarr.Http/Authentication/UiAuthorizationPolicyProvider.cs @@ -29,7 +29,8 @@ namespace NzbDrone.Http.Authentication if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) { var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) - .RequireAuthenticatedUser(); + .AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement()); + return Task.FromResult(policy.Build()); }