diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index a9db70741..24e44999f 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -126,17 +126,17 @@ namespace PlexRequests.Core public Guid? CreateAdmin(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.User, UserClaims.PowerUser, UserClaims.Admin }, properties); + return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser, UserClaims.Admin }, properties); } public Guid? CreatePowerUser(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.User, UserClaims.PowerUser }, properties); + return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser }, properties); } public Guid? CreateRegularUser(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.User }, properties); + return CreateUser(username, password, new[] { UserClaims.RegularUser }, properties); } public IEnumerable GetAllClaims() diff --git a/PlexRequests.Helpers/UserClaims.cs b/PlexRequests.Helpers/UserClaims.cs index 0f1c0c4c9..a42023e44 100644 --- a/PlexRequests.Helpers/UserClaims.cs +++ b/PlexRequests.Helpers/UserClaims.cs @@ -6,7 +6,7 @@ namespace PlexRequests.Helpers { public const string Admin = nameof(Admin); // Can do everything including creating new users and editing settings public const string PowerUser = nameof(PowerUser); // Can only manage the requests, approve etc. - public const string User = nameof(User); // Can only request + public const string RegularUser = nameof(RegularUser); // Can only request public const string ReadOnlyUser = nameof(ReadOnlyUser); // Can only view stuff public const string Newsletter = nameof(Newsletter); // Has newsletter feature enabled } diff --git a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs new file mode 100644 index 000000000..bd08b772b --- /dev/null +++ b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs @@ -0,0 +1,49 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HtmlSecurityHelper.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using Nancy.Security; +using Nancy.ViewEngines.Razor; + +namespace PlexRequests.UI.Helpers +{ + public static class HtmlSecurityHelper + { + public static bool HasAnyPermission(this HtmlHelpers helper, params string[] claims) + { + if (!helper.CurrentUser.IsAuthenticated()) + { + return false; + } + return helper.CurrentUser.HasAnyClaim(claims); + } + + public static bool DoesNotHaveAnyPermission(this HtmlHelpers helper, params string[] claims) + { + return SecurityExtensions.DoesNotHaveClaims(claims, helper.CurrentUser); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/SecurityExtensions.cs b/PlexRequests.UI/Helpers/SecurityExtensions.cs new file mode 100644 index 000000000..bf43935d6 --- /dev/null +++ b/PlexRequests.UI/Helpers/SecurityExtensions.cs @@ -0,0 +1,170 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SecurityExtensions.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using Nancy.Extensions; +using Nancy.Security; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Helpers +{ + public static class SecurityExtensions + { + + public static bool IsLoggedIn(this NancyContext context) + { + var userName = context.Request.Session[SessionKeys.UsernameKey]; + var realUser = false; + var plexUser = userName != null; + + if (context.CurrentUser?.IsAuthenticated() ?? false) + { + realUser = true; + } + + return realUser || plexUser; + } + + public static bool IsPlexUser(this NancyContext context) + { + var userName = context.Request.Session[SessionKeys.UsernameKey]; + var plexUser = userName != null; + + var isAuth = context.CurrentUser?.IsAuthenticated() ?? false; + + return plexUser && !isAuth; + } + + public static bool IsNormalUser(this NancyContext context) + { + var userName = context.Request.Session[SessionKeys.UsernameKey]; + var plexUser = userName != null; + + var isAuth = context.CurrentUser?.IsAuthenticated() ?? false; + + return isAuth && !plexUser; + } + + /// + /// This module requires authentication and NO certain claims to be present. + /// + /// 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)) + { + return false; + } + return true; + } + + public static bool DoesNotHaveClaimCheck(this NancyContext context, params string[] bannedClaims) + { + if (!context?.CurrentUser?.IsAuthenticated() ?? false) + { + return false; + } + if (DoesNotHaveClaims(bannedClaims, context)) + { + return false; + } + return true; + } + + /// + /// 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) + { + return ForbiddenIfNot(ctx => !ctx.CurrentUser.HasAnyClaim(claims)); + } + + public static bool DoesNotHaveClaims(IEnumerable claims, NancyContext ctx) + { + return !ctx.CurrentUser.HasAnyClaim(claims); + } + + public static bool DoesNotHaveClaims(IEnumerable claims, IUserIdentity identity) + { + return !identity?.HasAnyClaim(claims) ?? true; + } + + + // BELOW IS A COPY FROM THE SecurityHooks CLASS! + + /// + /// Creates a hook to be used in a pipeline before a route handler to ensure that + /// the request satisfies a specific test. + /// + /// 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) + { + return HttpStatusCodeIfNot(HttpStatusCode.Forbidden, test); + } + + /// + /// Creates a hook to be used in a pipeline before a route handler to ensure that + /// the request satisfies a specific test. + /// + /// 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) + { + return ctx => + { + Response response = null; + if (!test(ctx)) + response = new Response + { + StatusCode = statusCode + }; + return response; + }; + } + + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index 9d907b95e..e0ba7314d 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -52,7 +52,7 @@ namespace PlexRequests.UI.Modules ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr) : base("approval", pr) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); Service = service; CpService = cpService; diff --git a/PlexRequests.UI/Modules/IssuesModule.cs b/PlexRequests.UI/Modules/IssuesModule.cs index e2ed05aae..864764f9f 100644 --- a/PlexRequests.UI/Modules/IssuesModule.cs +++ b/PlexRequests.UI/Modules/IssuesModule.cs @@ -366,7 +366,7 @@ namespace PlexRequests.UI.Modules { try { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var issue = await IssuesService.GetAsync(issueId); var request = await RequestService.GetAsync(issue.RequestId); if (request.Id > 0) @@ -399,7 +399,7 @@ namespace PlexRequests.UI.Modules { try { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var issue = await IssuesService.GetAsync(issueId); issue.IssueStatus = status; @@ -417,7 +417,7 @@ namespace PlexRequests.UI.Modules private async Task ClearIssue(int issueId, IssueState state) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var issue = await IssuesService.GetAsync(issueId); var toRemove = issue.Issues.FirstOrDefault(x => x.Issue == state); @@ -430,7 +430,7 @@ namespace PlexRequests.UI.Modules private async Task AddNote(int requestId, string noteArea, IssueState state) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var issue = await IssuesService.GetAsync(requestId); if (issue == null) { diff --git a/PlexRequests.UI/Modules/RequestsBetaModule.cs b/PlexRequests.UI/Modules/RequestsBetaModule.cs index a8a1a6e3f..067e10e62 100644 --- a/PlexRequests.UI/Modules/RequestsBetaModule.cs +++ b/PlexRequests.UI/Modules/RequestsBetaModule.cs @@ -260,7 +260,7 @@ namespace PlexRequests.UI.Modules private async Task DeleteRequest(int requestid) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies)); var currentEntity = await Service.GetAsync(requestid); @@ -308,7 +308,7 @@ namespace PlexRequests.UI.Modules private async Task ClearIssue(int requestId) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var originalRequest = await Service.GetAsync(requestId); if (originalRequest == null) @@ -326,7 +326,7 @@ namespace PlexRequests.UI.Modules private async Task ChangeRequestAvailability(int requestId, bool available) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies)); var originalRequest = await Service.GetAsync(requestId); if (originalRequest == null) diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index dc46b2473..13aecdc03 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -308,7 +308,7 @@ namespace PlexRequests.UI.Modules private async Task DeleteRequest(int requestid) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies)); var currentEntity = await Service.GetAsync(requestid); @@ -356,7 +356,7 @@ namespace PlexRequests.UI.Modules private async Task ClearIssue(int requestId) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); var originalRequest = await Service.GetAsync(requestId); if (originalRequest == null) @@ -374,7 +374,7 @@ namespace PlexRequests.UI.Modules private async Task ChangeRequestAvailability(int requestId, bool available) { - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies)); var originalRequest = await Service.GetAsync(requestId); if (originalRequest == null) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 6d501adcc..35dfeea65 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -444,6 +444,15 @@ namespace PlexRequests.UI.Modules private async Task RequestMovie(int movieId) { + if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + { + return + Response.AsJson(new JsonResponseModel() + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a movie!" + }); + } var settings = await PrService.GetSettingsAsync(); if (!await CheckRequestLimit(settings, RequestType.Movie)) { @@ -544,6 +553,15 @@ namespace PlexRequests.UI.Modules /// private async Task RequestTvShow(int showId, string seasons) { + if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + { + return + Response.AsJson(new JsonResponseModel() + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a TV Show!" + }); + } // Get the JSON from the request var req = (Dictionary.ValueCollection)Request.Form.Values; EpisodeRequestModel episodeModel = null; diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index a9fe57283..bc78a9f77 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -212,6 +212,8 @@ + +