diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 9830e8ade..730879de6 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -57,6 +57,8 @@ namespace NzbDrone.Common.Instrumentation RegisterAppFile(appFolderInfo); } + RegisterAuthLogger(); + LogManager.ReconfigExistingLoggers(); } @@ -195,6 +197,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.", "")); diff --git a/src/Sonarr.Http/Authentication/AuthenticationModule.cs b/src/Sonarr.Http/Authentication/AuthenticationModule.cs index cc4ea879a..aa8cc6a2c 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationModule.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationModule.cs @@ -3,6 +3,8 @@ using Nancy; using Nancy.Authentication.Forms; using Nancy.Extensions; using Nancy.ModelBinding; +using NLog; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -10,12 +12,12 @@ namespace Sonarr.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(); @@ -23,7 +25,7 @@ namespace Sonarr.Http.Authentication private Response Login(LoginResource resource) { - var user = _userService.FindUser(resource.Username, resource.Password); + var user = _authService.Login(Context, resource.Username, resource.Password); if (user == null) { @@ -43,6 +45,8 @@ namespace Sonarr.Http.Authentication private Response Logout() { + _authService.Logout(Context); + return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/"); } } diff --git a/src/Sonarr.Http/Authentication/AuthenticationService.cs b/src/Sonarr.Http/Authentication/AuthenticationService.cs index 3c761c9e4..de9e5f0aa 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationService.cs +++ b/src/Sonarr.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 Sonarr.Http.Extensions; @@ -13,24 +15,75 @@ namespace Sonarr.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 Sonarr.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 Sonarr.Http.Authentication return new NzbDroneUser { UserName = user.Username }; } + LogInvalidated(_context); + return null; } @@ -138,5 +201,30 @@ namespace Sonarr.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/Sonarr.Http/Authentication/EnableAuthInNancy.cs b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs index 942750742..7e7b5373e 100644 --- a/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs +++ b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs @@ -43,18 +43,28 @@ namespace Sonarr.Http.Authentication else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) { pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr")); + 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 }; }