From b88030935675c9b1464c40d63a5bd08c6e8be84f Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Tue, 27 Aug 2019 23:29:16 +0200 Subject: [PATCH] New: Added Auth-* log entries for fail2ban purposes closes #2760 --- .../Authentication/AuthenticationModule.cs | 20 ++-- .../Authentication/AuthenticationService.cs | 94 ++++++++++++++++++- .../Authentication/EnableAuthInNancy.cs | 13 ++- .../Instrumentation/NzbDroneLogger.cs | 19 ++++ 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/Lidarr.Http/Authentication/AuthenticationModule.cs b/src/Lidarr.Http/Authentication/AuthenticationModule.cs index 1cee2a789..45bba5e65 100644 --- a/src/Lidarr.Http/Authentication/AuthenticationModule.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationModule.cs @@ -5,6 +5,8 @@ using Nancy.Extensions; using Nancy.ModelBinding; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; +using NLog; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -12,12 +14,12 @@ namespace Lidarr.Http.Authentication { public class AuthenticationModule : NancyModule { - private readonly IUserService _userService; + private readonly IAuthenticationService _authService; private readonly IConfigFileProvider _configFileProvider; - public AuthenticationModule(IUserService userService, IConfigFileProvider configFileProvider) + public AuthenticationModule(IAuthenticationService authService, IConfigFileProvider configFileProvider) { - _userService = userService; + _authService = authService; _configFileProvider = configFileProvider; Post["/login"] = x => Login(this.Bind()); Get["/logout"] = x => Logout(); @@ -25,15 +27,7 @@ namespace Lidarr.Http.Authentication private Response Login(LoginResource resource) { - var username = resource.Username; - var password = resource.Password; - - if (username.IsNullOrWhiteSpace() || password.IsNullOrWhiteSpace()) - { - return LoginFailed(); - } - - var user = _userService.FindUser(username, password); + var user = _authService.Login(Context, resource.Username, resource.Password); if (user == null) { @@ -52,6 +46,8 @@ namespace Lidarr.Http.Authentication private Response Logout() { + _authService.Logout(Context); + return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/"); } diff --git a/src/Lidarr.Http/Authentication/AuthenticationService.cs b/src/Lidarr.Http/Authentication/AuthenticationService.cs index 97436e979..a1e5bc27e 100644 --- a/src/Lidarr.Http/Authentication/AuthenticationService.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationService.cs @@ -4,7 +4,9 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Authentication.Forms; using Nancy.Security; +using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using Lidarr.Http.Extensions; @@ -13,24 +15,75 @@ namespace Lidarr.Http.Authentication { public interface IAuthenticationService : IUserValidator, IUserMapper { + void SetContext(NancyContext context); + + void LogUnauthorized(NancyContext context); + User Login(NancyContext context, string username, string password); + void Logout(NancyContext context); bool IsAuthenticated(NancyContext context); } public class AuthenticationService : IAuthenticationService { - private readonly IUserService _userService; + private static readonly Logger _authLogger = LogManager.GetLogger("Auth"); private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" }; - + private readonly IUserService _userService; + private readonly NancyContext _nancyContext; + private static string API_KEY; private static AuthenticationType AUTH_METHOD; - public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) + [ThreadStatic] + private static NancyContext _context; + + public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService, NancyContext nancyContext) { _userService = userService; + _nancyContext = nancyContext; API_KEY = configFileProvider.ApiKey; AUTH_METHOD = configFileProvider.AuthenticationMethod; } + public void SetContext(NancyContext context) + { + // Validate and GetUserIdentifier don't have access to the NancyContext so get it from the pipeline earlier + _context = context; + } + + public User Login(NancyContext context, string username, string password) + { + if (AUTH_METHOD == AuthenticationType.None) + { + return null; + } + + var user = _userService.FindUser(username, password); + + if (user != null) + { + LogSuccess(context, username); + + return user; + } + + LogFailure(context, username); + + return null; + } + + public void Logout(NancyContext context) + { + if (AUTH_METHOD == AuthenticationType.None) + { + return; + } + + if (context.CurrentUser != null) + { + LogLogout(context, context.CurrentUser.UserName); + } + } + public IUserIdentity Validate(string username, string password) { if (AUTH_METHOD == AuthenticationType.None) @@ -42,9 +95,17 @@ namespace Lidarr.Http.Authentication if (user != null) { + if (AUTH_METHOD != AuthenticationType.Basic) + { + // Don't log success for basic auth + LogSuccess(_context, username); + } + return new NzbDroneUser { UserName = user.Username }; } + LogFailure(_context, username); + return null; } @@ -62,6 +123,8 @@ namespace Lidarr.Http.Authentication return new NzbDroneUser { UserName = user.Username }; } + LogInvalidated(_context); + return null; } @@ -138,5 +201,30 @@ namespace Lidarr.Http.Authentication return context.Request.Headers.Authorization; } + + public void LogUnauthorized(NancyContext context) + { + _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.Request.UserHostAddress, context.Request.Url.ToString()); + } + + private void LogInvalidated(NancyContext context) + { + _authLogger.Info("Auth-Invalidated ip {0}", context.Request.UserHostAddress); + } + + private void LogFailure(NancyContext context, string username) + { + _authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.Request.UserHostAddress, username); + } + + private void LogSuccess(NancyContext context, string username) + { + _authLogger.Info("Auth-Success ip {0} username '{1}'", context.Request.UserHostAddress, username); + } + + private void LogLogout(NancyContext context, string username) + { + _authLogger.Info("Auth-Logout ip {0} username '{1}'", context.Request.UserHostAddress, username); + } } } diff --git a/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs b/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs index 70887f9af..7b916066f 100644 --- a/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs +++ b/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using Lidarr.Http.Extensions; using Lidarr.Http.Extensions.Pipelines; +using NzbDrone.Common.EnvironmentInfo; namespace Lidarr.Http.Authentication { @@ -42,19 +43,29 @@ namespace Lidarr.Http.Authentication else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) { - pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Lidarr")); + pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, BuildInfo.AppName)); + pipelines.BeforeRequest.AddItemToStartOfPipeline(CaptureContext); } pipelines.BeforeRequest.AddItemToEndOfPipeline((Func)RequiresAuthentication); pipelines.AfterRequest.AddItemToEndOfPipeline((Action)RemoveLoginHooksForApiCalls); } + private Response CaptureContext(NancyContext context) + { + _authenticationService.SetContext(context); + + return null; + } + + private Response RequiresAuthentication(NancyContext context) { Response response = null; if (!_authenticationService.IsAuthenticated(context)) { + _authenticationService.LogUnauthorized(context); response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index a4cff59e5..ac9982a9e 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -55,6 +55,8 @@ namespace NzbDrone.Common.Instrumentation RegisterAppFile(appFolderInfo); } + RegisterAuthLogger(); + LogManager.ReconfigExistingLoggers(); } @@ -167,6 +169,23 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration.LoggingRules.Add(loggingRule); } + private static void RegisterAuthLogger() + { + var consoleTarget = LogManager.Configuration.FindTargetByName("console"); + var fileTarget = LogManager.Configuration.FindTargetByName("appFileInfo"); + + var target = consoleTarget ?? fileTarget ?? new NullTarget(); + + // Send Auth to Console and info app file, but not the log database + var rule = new LoggingRule("Auth", LogLevel.Info, target) { Final = true }; + if (consoleTarget != null && fileTarget != null) + { + rule.Targets.Add(fileTarget); + } + + LogManager.Configuration.LoggingRules.Insert(0, rule); + } + public static Logger GetLogger(Type obj) { return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));