New: Setting to disable authentication for local addresses

This commit is contained in:
Mark McDowall 2022-05-23 14:00:26 -07:00
parent 05ee4e6449
commit b154b00c61
13 changed files with 116 additions and 12 deletions

View File

@ -16,6 +16,11 @@ const authenticationMethodOptions = [
{ key: 'forms', value: 'Forms (Login Page)' } { key: 'forms', value: 'Forms (Login Page)' }
]; ];
const authenticationRequiredOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
];
const certificateValidationOptions = [ const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' }, { key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }, { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
@ -67,6 +72,7 @@ class SecuritySettings extends Component {
const { const {
authenticationMethod, authenticationMethod,
authenticationRequired,
username, username,
password, password,
apiKey, apiKey,
@ -91,7 +97,24 @@ class SecuritySettings extends Component {
</FormGroup> </FormGroup>
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup>
<FormLabel>Authentication Required</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText="Change which requests authentication is required for. Do not change unless you understand the risks."
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>Username</FormLabel> <FormLabel>Username</FormLabel>
@ -101,11 +124,12 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...username} {...username}
/> />
</FormGroup> </FormGroup> :
null
} }
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
@ -115,7 +139,8 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...password} {...password}
/> />
</FormGroup> </FormGroup> :
null
} }
<FormGroup> <FormGroup>

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start(true);
driver.Url = "http://localhost:8989"; driver.Url = "http://localhost:8989";

View File

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Authentication
{
public enum AuthenticationRequiredType
{
Enabled = 0,
DisabledForLocalAddresses = 1
}
}

View File

@ -1,9 +1,10 @@
namespace NzbDrone.Core.Authentication namespace NzbDrone.Core.Authentication
{ {
public enum AuthenticationType public enum AuthenticationType
{ {
None = 0, None = 0,
Basic = 1, Basic = 1,
Forms = 2 Forms = 2,
External = 3
} }
} }

View File

@ -31,6 +31,7 @@ namespace NzbDrone.Core.Configuration
bool EnableSsl { get; } bool EnableSsl { get; }
bool LaunchBrowser { get; } bool LaunchBrowser { get; }
AuthenticationType AuthenticationMethod { get; } AuthenticationType AuthenticationMethod { get; }
AuthenticationRequiredType AuthenticationRequired { get; }
bool AnalyticsEnabled { get; } bool AnalyticsEnabled { get; }
string LogLevel { get; } string LogLevel { get; }
string ConsoleLogLevel { get; } string ConsoleLogLevel { get; }
@ -181,6 +182,8 @@ namespace NzbDrone.Core.Configuration
} }
} }
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
public string Branch => GetValue("Branch", "main").ToLowerInvariant(); public string Branch => GetValue("Branch", "main").ToLowerInvariant();

View File

@ -102,6 +102,8 @@ namespace NzbDrone.Host
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>(); services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, UiAuthorizationHandler>();
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
options.AddPolicy("SignalR", policy => options.AddPolicy("SignalR", policy =>

View File

@ -31,12 +31,12 @@ namespace NzbDrone.Test.Common
Port = port; Port = port;
} }
public void Start() public void Start(bool enableAuth = false)
{ {
AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID()); AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID());
Directory.CreateDirectory(AppData); Directory.CreateDirectory(AppData);
GenerateConfigFile(); GenerateConfigFile(enableAuth);
string consoleExe; string consoleExe;
if (OsInfo.IsWindows) if (OsInfo.IsWindows)
@ -146,7 +146,7 @@ namespace NzbDrone.Test.Common
} }
} }
private void GenerateConfigFile() private void GenerateConfigFile(bool enableAuth)
{ {
var configFile = Path.Combine(AppData, "config.xml"); var configFile = Path.Combine(AppData, "config.xml");
@ -159,6 +159,8 @@ namespace NzbDrone.Test.Common
new XElement(nameof(ConfigFileProvider.ApiKey), apiKey), new XElement(nameof(ConfigFileProvider.ApiKey), apiKey),
new XElement(nameof(ConfigFileProvider.LogLevel), "trace"), new XElement(nameof(ConfigFileProvider.LogLevel), "trace"),
new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false), 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))); new XElement(nameof(ConfigFileProvider.Port), Port)));
var data = xDoc.ToString(); var data = xDoc.ToString();

View File

@ -15,6 +15,7 @@ namespace Sonarr.Api.V3.Config
public bool EnableSsl { get; set; } public bool EnableSsl { get; set; }
public bool LaunchBrowser { get; set; } public bool LaunchBrowser { get; set; }
public AuthenticationType AuthenticationMethod { get; set; } public AuthenticationType AuthenticationMethod { get; set; }
public AuthenticationRequiredType AuthenticationRequired { get; set; }
public bool AnalyticsEnabled { get; set; } public bool AnalyticsEnabled { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
@ -56,6 +57,7 @@ namespace Sonarr.Api.V3.Config
EnableSsl = model.EnableSsl, EnableSsl = model.EnableSsl,
LaunchBrowser = model.LaunchBrowser, LaunchBrowser = model.LaunchBrowser,
AuthenticationMethod = model.AuthenticationMethod, AuthenticationMethod = model.AuthenticationMethod,
AuthenticationRequired = model.AuthenticationRequired,
AnalyticsEnabled = model.AnalyticsEnabled, AnalyticsEnabled = model.AnalyticsEnabled,
//Username //Username

View File

@ -13,6 +13,7 @@ namespace Sonarr.Http.Authentication
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{ {
public const string DefaultScheme = "API Key"; public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme; public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme; public string AuthenticationType = DefaultScheme;

View File

@ -22,10 +22,16 @@ namespace Sonarr.Http.Authentication
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { }); return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
} }
public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authenticationBuilder, string name)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
}
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
{ {
return services.AddAuthentication() return services.AddAuthentication()
.AddNone(AuthenticationType.None.ToString()) .AddNone(AuthenticationType.None.ToString())
.AddExternal(AuthenticationType.External.ToString())
.AddBasic(AuthenticationType.Basic.ToString()) .AddBasic(AuthenticationType.Basic.ToString())
.AddCookie(AuthenticationType.Forms.ToString(), options => .AddCookie(AuthenticationType.Forms.ToString(), options =>
{ {

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace NzbDrone.Http.Authentication
{
public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement
{
}
}

View File

@ -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 Sonarr.Http.Extensions;
namespace NzbDrone.Http.Authentication
{
public class UiAuthorizationHandler : AuthorizationHandler<BypassableDenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement, IHandle<ConfigSavedEvent>
{
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;
}
}
}

View File

@ -29,7 +29,8 @@ namespace NzbDrone.Http.Authentication
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
{ {
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
.RequireAuthenticatedUser(); .AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());
return Task.FromResult(policy.Build()); return Task.FromResult(policy.Build());
} }