mirror of
https://github.com/fergalmoran/Readarr.git
synced 2025-12-22 09:29:59 +00:00
New: Setting to disable authentication for local addresses
(cherry picked from commit b154b00c6156512e7fbd0a2c4833c116a74f23ca) Closes #1804 Closes #2077
This commit is contained in:
@@ -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('Username')}
|
||||
</FormLabel>
|
||||
<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>
|
||||
|
||||
<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
|
||||
{
|
||||
}
|
||||
}
|
||||
45
src/Readarr.Http/Authentication/UiAuthorizationHandler.cs
Normal file
45
src/Readarr.Http/Authentication/UiAuthorizationHandler.cs
Normal 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 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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user