New: Auth Required

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
Qstick
2022-12-20 12:00:24 -06:00
parent 2a2e859420
commit c7eb08a0f0
33 changed files with 555 additions and 57 deletions

View File

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

View File

@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
try
{
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
if (sentryEvent.Message is not null)
{
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
{
string str => str,
_ => x.ToString()
})).ToList();
}
if (sentryEvent.Fingerprint != null)
if (sentryEvent.Fingerprint.Any())
{
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
sentryEvent.SetFingerprint(fingerprint);
}
if (sentryEvent.Extra != null)
if (sentryEvent.Extra.Any())
{
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
sentryEvent.SetExtras(extras);
}
foreach (var exception in sentryEvent.SentryExceptions)
if (sentryEvent.SentryExceptions is not null)
{
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
foreach (var frame in exception.Stacktrace.Frames)
foreach (var exception in sentryEvent.SentryExceptions)
{
frame.FileName = ShortenPath(frame.FileName);
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
if (exception.Stacktrace is not null)
{
foreach (var frame in exception.Stacktrace.Frames)
{
frame.FileName = ShortenPath(frame.FileName);
}
}
}
}
}

View File

@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
"UnauthorizedAccessException",
// Filter out people stuck in boot loops
"CorruptDatabaseException",
// This also filters some people in boot loops
"TinyIoCResolutionException"
"CorruptDatabaseException"
};
public static readonly List<string> FilteredExceptionMessages = new List<string>
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Dsn = dsn;
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.SendDefaultPii = false;
o.Debug = false;
o.DiagnosticLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release;
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null)
{
fingerPrint.Add(ex.GetType().FullName);
fingerPrint.Add(ex.TargetSite.ToString());
if (ex.TargetSite != null)
{
fingerPrint.Add(ex.TargetSite.ToString());
}
if (ex.InnerException != null)
{
fingerPrint.Add(ex.InnerException.GetType().FullName);

View File

@@ -4,13 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

View File

@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

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
{
None = 0,
Basic = 1,
Forms = 2
Forms = 2,
External = 3
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Authentication
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
public Guid Identifier { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Salt { get; set; }
public int Iterations { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
private static readonly int ITERATIONS = 10000;
private static readonly int SALT_SIZE = 128 / 8;
private static readonly int NUMBER_OF_BYTES = 256 / 8;
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{
_repo = repo;
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
public User Add(string username, string password)
{
return _repo.Insert(new User
var user = new User
{
Identifier = Guid.NewGuid(),
Username = username.ToLowerInvariant(),
Password = password.SHA256Hash()
});
Username = username.ToLowerInvariant()
};
SetUserHashedPassword(user, password);
return _repo.Insert(user);
}
public User Update(User user)
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
if (user.Password != password)
{
user.Password = password.SHA256Hash();
SetUserHashedPassword(user, password);
}
user.Username = username.ToLowerInvariant();
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
return null;
}
if (user.Password == password.SHA256Hash())
if (user.Salt.IsNullOrWhiteSpace())
{
// If password matches stored SHA256 hash, update to salted hash and verify.
if (user.Password == password.SHA256Hash())
{
SetUserHashedPassword(user, password);
return Update(user);
}
return null;
}
if (VerifyHashedPassword(user, password))
{
return user;
}
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
{
return _repo.FindUser(identifier);
}
private User SetUserHashedPassword(User user, string password)
{
var salt = GenerateSalt();
user.Iterations = ITERATIONS;
user.Salt = Convert.ToBase64String(salt);
user.Password = GetHashedPassword(password, salt, ITERATIONS);
return user;
}
private byte[] GenerateSalt()
{
var salt = new byte[SALT_SIZE];
RandomNumberGenerator.Create().GetBytes(salt);
return salt;
}
private string GetHashedPassword(string password, byte[] salt, int iterations)
{
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: iterations,
numBytesRequested: NUMBER_OF_BYTES));
}
private bool VerifyHashedPassword(User user, string password)
{
var salt = Convert.FromBase64String(user.Salt);
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
return user.Password == hashedPassword;
}
}
}

View File

@@ -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; }
@@ -193,6 +194,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.

View File

@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(024)]
public class add_salt_to_users : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Users")
.AddColumn("Salt").AsString().Nullable()
.AddColumn("Iterations").AsInt32().Nullable();
}
}
}

View File

@@ -6,7 +6,8 @@
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="MailKit" Version="3.4.1" />
<PackageReference Include="MailKit" Version="3.4.3" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.12" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
@@ -15,13 +16,13 @@
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="TinyTwitter" Version="1.1.2" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
<PackageReference Include="AngleSharp" Version="0.17.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,11 +4,11 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -22,7 +22,6 @@ using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
using Prowlarr.Api.V1.System;
using Prowlarr.Http;
@@ -172,6 +171,8 @@ namespace NzbDrone.Host
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, UiAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("SignalR", policy =>

View File

@@ -37,12 +37,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 consoleExe;
if (OsInfo.IsWindows)
@@ -167,7 +167,7 @@ namespace NzbDrone.Test.Common
}
}
private void GenerateConfigFile()
private void GenerateConfigFile(bool enableAuth)
{
var configFile = Path.Combine(AppData, "config.xml");
@@ -180,6 +180,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();

View File

@@ -6,7 +6,7 @@
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />

View File

@@ -4,9 +4,9 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />

View File

@@ -4,7 +4,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -15,6 +15,7 @@ namespace Prowlarr.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 Prowlarr.Api.V1.Config
EnableSsl = model.EnableSsl,
LaunchBrowser = model.LaunchBrowser,
AuthenticationMethod = model.AuthenticationMethod,
AuthenticationRequired = model.AuthenticationRequired,
AnalyticsEnabled = model.AnalyticsEnabled,
//Username

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />

View File

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

View File

@@ -22,10 +22,16 @@ namespace Prowlarr.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 =>
{

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Prowlarr.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 Prowlarr.Http.Extensions;
namespace Prowlarr.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

@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Http.Authentication
namespace Prowlarr.Http.Authentication
{
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
@@ -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());
}

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="ImpromptuInterface" Version="7.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />