diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs index 3d5155ad0..b0c28db36 100644 --- a/PlexRequests.Helpers/Permissions/Permissions.cs +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -46,6 +46,9 @@ namespace PlexRequests.Helpers.Permissions RequestMusic = 8, [Display(Name = "Report Issue")] - ReportIssue = 16 + ReportIssue = 16, + + [Display(Name = "Read Only User")] + ReadOnlyUser = 32, } } \ No newline at end of file diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index 38c703c97..90aac0c64 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -39,6 +39,10 @@ ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True + + ..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll + True + ..\packages\NLog.4.3.6\lib\net45\NLog.dll True diff --git a/PlexRequests.Helpers/packages.config b/PlexRequests.Helpers/packages.config index 97b45cbe0..db4648bf2 100644 --- a/PlexRequests.Helpers/packages.config +++ b/PlexRequests.Helpers/packages.config @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/PlexRequests.Store/Repository/UserRepository.cs b/PlexRequests.Store/Repository/UserRepository.cs index 56ad461cb..b05609c82 100644 --- a/PlexRequests.Store/Repository/UserRepository.cs +++ b/PlexRequests.Store/Repository/UserRepository.cs @@ -49,21 +49,21 @@ namespace PlexRequests.Store.Repository public UsersModel GetUser(string userGuid) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE Userguid = @UserGuid"; return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); } public UsersModel GetUserByUsername(string username) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE UserName = @UserName"; return Db.QueryFirstOrDefault(sql, new {UserName = username}); } public async Task GetUserAsync(string userguid) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE UserGuid = @UserGuid"; return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); } diff --git a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs index bd08b772b..3d1751257 100644 --- a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs +++ b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs @@ -27,23 +27,36 @@ using Nancy.Security; using Nancy.ViewEngines.Razor; +using Ninject; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store.Repository; namespace PlexRequests.UI.Helpers { public static class HtmlSecurityHelper { - public static bool HasAnyPermission(this HtmlHelpers helper, params string[] claims) + private static SecurityExtensions Security { - if (!helper.CurrentUser.IsAuthenticated()) + + get { - return false; + var userRepo = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, null)); } - return helper.CurrentUser.HasAnyClaim(claims); } - public static bool DoesNotHaveAnyPermission(this HtmlHelpers helper, params string[] claims) + private static SecurityExtensions _security; + + + public static bool HasAnyPermission(this HtmlHelpers helper, int permission) + { + return helper.CurrentUser.IsAuthenticated() + && Security.HasPermissions(helper.CurrentUser, (Permissions) permission); + } + + public static bool DoesNotHavePermission(this HtmlHelpers helper, int permission) { - return SecurityExtensions.DoesNotHaveClaims(claims, helper.CurrentUser); + return Security.DoesNotHavePermissions(permission, helper.CurrentUser); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/SecurityExtensions.cs b/PlexRequests.UI/Helpers/SecurityExtensions.cs index bf43935d6..36f01a1a5 100644 --- a/PlexRequests.UI/Helpers/SecurityExtensions.cs +++ b/PlexRequests.UI/Helpers/SecurityExtensions.cs @@ -31,14 +31,25 @@ using System.Linq; using Nancy; using Nancy.Extensions; using Nancy.Security; +using Ninject; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store.Repository; using PlexRequests.UI.Models; namespace PlexRequests.UI.Helpers { - public static class SecurityExtensions + public class SecurityExtensions { + public SecurityExtensions(IUserRepository userRepository, NancyModule context) + { + UserRepository = userRepository; + Module = context; + } + + private IUserRepository UserRepository { get; } + private NancyModule Module { get; } - public static bool IsLoggedIn(this NancyContext context) + public bool IsLoggedIn(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var realUser = false; @@ -52,7 +63,7 @@ namespace PlexRequests.UI.Helpers return realUser || plexUser; } - public static bool IsPlexUser(this NancyContext context) + public bool IsPlexUser(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var plexUser = userName != null; @@ -62,7 +73,7 @@ namespace PlexRequests.UI.Helpers return plexUser && !isAuth; } - public static bool IsNormalUser(this NancyContext context) + public bool IsNormalUser(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var plexUser = userName != null; @@ -72,63 +83,72 @@ namespace PlexRequests.UI.Helpers return isAuth && !plexUser; } + /// - /// This module requires authentication and NO certain claims to be present. + /// Creates a hook to be used in a pipeline before a route handler to ensure + /// that the request was made by an authenticated user does not have the claims. /// - /// Module to enable - /// Claim(s) required - public static void DoesNotHaveClaim(this INancyModule module, params string[] bannedClaims) - { - module.AddBeforeHookOrExecute(SecurityHooks.RequiresAuthentication(), "Requires Authentication"); - module.AddBeforeHookOrExecute(DoesNotHaveClaims(bannedClaims), "Has Banned Claims"); - } - - public static bool DoesNotHaveClaimCheck(this INancyModule module, params string[] bannedClaims) - { - if (!module.Context?.CurrentUser?.IsAuthenticated() ?? false) - { - return false; - } - if (DoesNotHaveClaims(bannedClaims, module.Context)) + /// Claims the authenticated user needs to have + /// Hook that returns an Unauthorized response if the user is not + /// authenticated or does have the claims, null otherwise + private Func DoesNotHavePermissions(int perm) + { + return ForbiddenIfNot(ctx => { - return false; - } - return true; + var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag((Permissions)perm); + return !result; + }); } - public static bool DoesNotHaveClaimCheck(this NancyContext context, params string[] bannedClaims) + public bool DoesNotHavePermissions(int perm, IUserIdentity currentUser) { - if (!context?.CurrentUser?.IsAuthenticated() ?? false) - { - return false; - } - if (DoesNotHaveClaims(bannedClaims, context)) - { - return false; - } - return true; + return DoesNotHavePermissions((Permissions) perm, currentUser); } - /// - /// Creates a hook to be used in a pipeline before a route handler to ensure - /// that the request was made by an authenticated user does not have the claims. - /// - /// Claims the authenticated user needs to have - /// Hook that returns an Unauthorized response if the user is not - /// authenticated or does have the claims, null otherwise - private static Func DoesNotHaveClaims(IEnumerable claims) + public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser) { - return ForbiddenIfNot(ctx => !ctx.CurrentUser.HasAnyClaim(claims)); + var dbUser = UserRepository.GetUserByUsername(currentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag((Permissions)perm); + return !result; } - public static bool DoesNotHaveClaims(IEnumerable claims, NancyContext ctx) + public bool HasPermissions(IUserIdentity user, Permissions perm) { - return !ctx.CurrentUser.HasAnyClaim(claims); + if (user == null) return false; + + var dbUser = UserRepository.GetUserByUsername(user.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag(perm); + return result; } - public static bool DoesNotHaveClaims(IEnumerable claims, IUserIdentity identity) + public void HasPermissionsResponse(Permissions perm) { - return !identity?.HasAnyClaim(claims) ?? true; + Module.AddBeforeHookOrExecute( + ForbiddenIfNot(ctx => + { + if (ctx.CurrentUser == null) return false; + + var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag(perm); + return result; + }), "Requires Claims"); } @@ -140,7 +160,7 @@ namespace PlexRequests.UI.Helpers /// /// Test that must return true for the request to continue /// Hook that returns an Forbidden response if the test fails, null otherwise - private static Func ForbiddenIfNot(Func test) + public Func ForbiddenIfNot(Func test) { return HttpStatusCodeIfNot(HttpStatusCode.Forbidden, test); } @@ -152,7 +172,7 @@ namespace PlexRequests.UI.Helpers /// HttpStatusCode to use for the response /// Test that must return true for the request to continue /// Hook that returns a response with a specific HttpStatusCode if the test fails, null otherwise - private static Func HttpStatusCodeIfNot(HttpStatusCode statusCode, Func test) + public Func HttpStatusCodeIfNot(HttpStatusCode statusCode, Func test) { return ctx => { diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 9caffcfde..efe797f24 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -56,6 +56,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Exceptions; +using PlexRequests.Helpers.Permissions; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Jobs; using PlexRequests.Services.Notification; @@ -153,7 +154,7 @@ namespace PlexRequests.UI.Modules NotifySettings = notifyService; RecentlyAdded = recentlyAdded; - this.RequiresClaims(UserClaims.Admin); + Security.HasPermissionsResponse(Permissions.Administrator); Get["/"] = _ => Admin(); @@ -849,7 +850,8 @@ namespace PlexRequests.UI.Modules private Response CreateApiKey() { - this.RequiresClaims(UserClaims.Admin); + Security.HasPermissionsResponse(Permissions.Administrator); + Analytics.TrackEventAsync(Category.Admin, Action.Create, "Created API Key", Username, CookieHelper.GetAnalyticClientId(Cookies)); var apiKey = Guid.NewGuid().ToString("N"); var settings = PrService.GetSettings(); diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 99b34358b..35bd4817f 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -30,6 +30,7 @@ using System.Linq; using System.Threading; using Nancy; +using Nancy.Security; using Ninject; using PlexRequests.Core; using PlexRequests.Core.SettingModels; @@ -121,10 +122,6 @@ namespace PlexRequests.UI.Modules protected IDictionary Cookies => Request?.Cookies; - // This is not ideal, but it's cleaner than having to pass it down through each module. - [Inject] - protected IUserRepository UserRepository { get; set; } - protected bool IsAdmin { get @@ -134,7 +131,9 @@ namespace PlexRequests.UI.Modules return false; } - var user = UserRepository.GetUserByUsername(Context?.CurrentUser?.UserName); + var userRepo = ServiceLocator.Instance.Resolve(); + + var user = userRepo.GetUserByUsername(Context?.CurrentUser?.UserName); if (user == null) return false; @@ -144,6 +143,22 @@ namespace PlexRequests.UI.Modules } } + protected IUserIdentity User => Context?.CurrentUser; + + protected SecurityExtensions Security + { + + get + { + var userRepo = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, this)); + } + } + + private SecurityExtensions _security; + + + protected bool LoggedIn => Context?.CurrentUser != null; protected string Culture { get; set; } diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 35dfeea65..e10e5b92d 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -55,6 +55,7 @@ using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Tv; using PlexRequests.Core.Models; using PlexRequests.Helpers.Analytics; +using PlexRequests.Helpers.Permissions; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; @@ -444,7 +445,7 @@ namespace PlexRequests.UI.Modules private async Task RequestMovie(int movieId) { - if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) { return Response.AsJson(new JsonResponseModel() @@ -553,7 +554,7 @@ namespace PlexRequests.UI.Modules /// private async Task RequestTvShow(int showId, string seasons) { - if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) { return Response.AsJson(new JsonResponseModel() diff --git a/PlexRequests.UI/Startup.cs b/PlexRequests.UI/Startup.cs index 1917ccc53..023e0b912 100644 --- a/PlexRequests.UI/Startup.cs +++ b/PlexRequests.UI/Startup.cs @@ -33,7 +33,6 @@ using NLog; using Owin; using PlexRequests.Core.Migration; -using PlexRequests.Services.Jobs; using PlexRequests.UI.Helpers; using PlexRequests.UI.Jobs; using PlexRequests.UI.NinjectModules; @@ -57,7 +56,7 @@ namespace PlexRequests.UI Debug.WriteLine("Modules found finished."); - var kernel = new StandardKernel(modules); + var kernel = new StandardKernel(new NinjectSettings { InjectNonPublic = true }, modules); Debug.WriteLine("Created Kernel and Injected Modules"); Debug.WriteLine("Added Contravariant Binder"); diff --git a/PlexRequests.UI/Views/Admin/Settings.cshtml b/PlexRequests.UI/Views/Admin/Settings.cshtml index acebf8e31..8f0edb028 100644 --- a/PlexRequests.UI/Views/Admin/Settings.cshtml +++ b/PlexRequests.UI/Views/Admin/Settings.cshtml @@ -57,7 +57,8 @@
- + +
@@ -65,16 +66,16 @@
-
- -
- +
+ +
+ +
-
-
+
@if (Model.SearchForMovies) @@ -192,7 +193,7 @@ }
- +
@@ -278,12 +279,12 @@
- -

A comma separated list of users whose requests do not require approval (These users also do not have a request limit).

+ +

A comma separated list of users whose requests do not require approval (These users also do not have a request limit).

@@ -293,23 +294,23 @@

If the request limits are set to 0 then no request limit is applied.

-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
@@ -320,7 +321,7 @@
-
+
@@ -373,7 +374,7 @@ success: function(response) { if (response) { generateNotify("Success!", "success"); - $('#apiKey').val(response); + $('#ApiKey').val(response); } }, error: function(e) {