New: Setting to disable authentication for local addresses

(cherry picked from commit b154b00c6156512e7fbd0a2c4833c116a74f23ca)

Closes #1804
Closes #2077
pull/3505/head
Mark McDowall 3 years ago committed by Bogdan
parent 1b1290efac
commit b5ef0cda1e

@ -11,16 +11,69 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
export const authenticationMethodOptions = [
{
key: 'none',
get value() {
return translate('None');
},
isDisabled: true
},
{
key: 'external',
get value() {
return translate('External');
},
isHidden: true
},
{
key: 'basic',
get value() {
return translate('AuthBasic');
}
},
{
key: 'forms',
get value() {
return translate('AuthForm');
}
}
];
export const authenticationRequiredOptions = [
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
}
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
{ key: 'disabled', value: 'Disabled' }
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
},
{
key: 'disabled',
get value() {
return translate('Disabled');
}
}
];
class SecuritySettings extends Component {
@ -68,6 +121,7 @@ class SecuritySettings extends Component {
const {
authenticationMethod,
authenticationRequired,
username,
password,
apiKey,
@ -79,26 +133,40 @@ class SecuritySettings extends Component {
return (
<FieldSet legend={translate('Security')}>
<FormGroup>
<FormLabel>
{translate('Authentication')}
</FormLabel>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>
{translate('Username')}
</FormLabel>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
@ -106,15 +174,14 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...username}
/>
</FormGroup>
</FormGroup> :
null
}
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>
{translate('Password')}
</FormLabel>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
@ -122,19 +189,18 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...password}
/>
</FormGroup>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>
{translate('APIKey')}
</FormLabel>
<FormLabel>{translate('ApiKey')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="apiKey"
readOnly={true}
helpTextWarning={translate('ApiKeyHelpTextWarning')}
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
buttons={[
<ClipboardButton
key="copy"
@ -160,9 +226,7 @@ class SecuritySettings extends Component {
</FormGroup>
<FormGroup>
<FormLabel>
{translate('CertificateValidation')}
</FormLabel>
<FormLabel>{translate('CertificateValidation')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}

@ -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";

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

@ -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
}
}

@ -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

@ -2,7 +2,6 @@
"20MinutesTwenty": "20 Minutes: {0}",
"45MinutesFourtyFive": "45 Minutes: {0}",
"60MinutesSixty": "60 Minutes: {0}",
"APIKey": "API Key",
"ASIN": "ASIN",
"About": "About",
"Actions": "Actions",
@ -33,7 +32,7 @@
"AnalyticsEnabledHelpText": "Send anonymous usage and error information to Readarr's servers. This includes information on your browser, which Readarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.",
"AnalyticsEnabledHelpTextWarning": "Requires restart to take effect",
"AnyEditionOkHelpText": "Readarr will automatically switch to the edition best matching downloaded files",
"ApiKeyHelpTextWarning": "Requires restart to take effect",
"ApiKey": "API Key",
"ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file",
"AppDataDirectory": "AppData Directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
@ -51,8 +50,13 @@
"ApplyTagsHelpTextRemove": "Remove: Remove the entered tags",
"ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)",
"AudioFileMetadata": "Write Metadata to Audio Files",
"AuthBasic": "Basic (Browser Popup)",
"AuthForm": "Forms (Login Page)",
"Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Readarr",
"AuthenticationMethodHelpText": "Require Username and Password to access {appName}",
"AuthenticationRequired": "Authentication Required",
"AuthenticationRequiredHelpText": "Change which requests authentication is required for. Do not change unless you understand the risks.",
"AuthenticationRequiredWarning": "To prevent remote access without authentication, {appName} now requires authentication to be enabled. You can optionally disable authentication from local addresses.",
"Author": "Author",
"AuthorClickToChangeBook": "Click to change book",
"AuthorEditor": "Author Editor",
@ -277,6 +281,7 @@
"DetailedProgressBarHelpText": "Show text on progress bar",
"Development": "Development",
"Disabled": "Disabled",
"DisabledForLocalAddresses": "Disabled for Local Addresses",
"DiscCount": "Disc Count",
"DiscNumber": "Disc Number",
"DiskSpace": "Disk Space",
@ -331,6 +336,7 @@
"EnableRssHelpText": "Will be used when Readarr periodically looks for releases via RSS Sync",
"EnableSSL": "Enable SSL",
"EnableSslHelpText": " Requires restart running as administrator to take effect",
"Enabled": "Enabled",
"EnabledHelpText": "Check to enable release profile",
"Ended": "Ended",
"EndedAllBooksDownloaded": "Ended (All books downloaded)",
@ -345,6 +351,7 @@
"ExistingTag": "Existing tag",
"ExistingTagsScrubbed": "Existing tags scrubbed",
"ExportCustomFormat": "Export Custom Format",
"External": "External",
"ExtraFileExtensionsHelpText": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'",
"FailedDownloadHandling": "Failed Download Handling",

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

@ -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();

@ -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

@ -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;

@ -22,10 +22,16 @@ namespace Readarr.Http.Authentication
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)
{
return services.AddAuthentication()
.AddNone(AuthenticationType.None.ToString())
.AddExternal(AuthenticationType.External.ToString())
.AddBasic(AuthenticationType.Basic.ToString())
.AddCookie(AuthenticationType.Forms.ToString(), options =>
{

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

@ -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<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;
}
}
}

@ -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());
}

Loading…
Cancel
Save