From 623d3883d2223a101fa58d8afa06377e2856954e Mon Sep 17 00:00:00 2001
From: Jamie Howarth <hello@jamiehowarth.me>
Date: Tue, 11 Oct 2022 02:05:28 +0100
Subject: [PATCH 1/4] Basic fixes for #154 by filtering domains and redirects
 API to only show redirects on nodes the user has access to. Doesn't solve
 risky payload issues, TBD w/ @abjerner later

---
 .../Controllers/Api/RedirectsController.cs    | 35 ++++++++++++-----
 .../Services/IRedirectsService.cs             | 11 ++++--
 .../Services/RedirectsService.cs              | 38 +++++++++++++++++--
 3 files changed, 68 insertions(+), 16 deletions(-)

diff --git a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
index 4c8833e7..94bbc7c0 100644
--- a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
+++ b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
@@ -12,12 +12,16 @@
 using Skybrud.Umbraco.Redirects.Models.Api;
 using Skybrud.Umbraco.Redirects.Models.Options;
 using Skybrud.Umbraco.Redirects.Services;
+using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Models.PublishedContent;
 using Umbraco.Cms.Core.Services;
 using Umbraco.Cms.Core.Web;
 using Umbraco.Cms.Web.BackOffice.Controllers;
 using Umbraco.Cms.Web.Common.Attributes;
+using Umbraco.Cms.Web.Common.UmbracoContext;
+using Umbraco.Extensions;
 
 #pragma warning disable 1591
 
@@ -36,6 +40,7 @@ public class RedirectsController : UmbracoAuthorizedApiController {
         private readonly RedirectsBackOfficeHelper _backOffice;
         private readonly IContentService _contentService;
         private readonly IMediaService _mediaService;
+        private readonly IUserService _userService;
         private readonly IUmbracoContextAccessor _umbracoContextAccessor;
 
         #region Constructors
@@ -43,12 +48,14 @@ public class RedirectsController : UmbracoAuthorizedApiController {
         public RedirectsController(ILogger<RedirectsController> logger, IRedirectsService redirectsService, RedirectsBackOfficeHelper backOffice,
             IContentService contentService,
             IMediaService mediaService,
+            IUserService userService,
             IUmbracoContextAccessor umbracoContextAccessor) {
             _logger = logger;
             _redirects = redirectsService;
             _backOffice = backOffice;
             _contentService = contentService;
             _mediaService = mediaService;
+            _userService = userService;
             _umbracoContextAccessor = umbracoContextAccessor;
         }
 
@@ -61,13 +68,13 @@ public RedirectsController(ILogger<RedirectsController> logger, IRedirectsServic
         [HttpGet]
         public ActionResult GetRootNodes() {
 
-            RedirectRootNode[] rootNodes = _redirects.GetRootNodes();
-
-            return new JsonResult(new {
-                total = rootNodes.Length,
-                items = rootNodes.Select(x => new RedirectRootNodeModel(x, _backOffice))
-            });
+	        RedirectRootNode[] rootNodes = _redirects.GetRootNodes(GetUser()).ToArray();
 
+	        return new JsonResult(new
+	        {
+		        total = rootNodes.Length,
+		        items = rootNodes.Select(x => new RedirectRootNodeModel(x, _backOffice))
+	        });
         }
 
         [HttpPost]
@@ -267,12 +274,17 @@ public ActionResult GetRedirects(int page = 1, int limit = 20, string type = nul
                     Page = page,
                     Limit = limit,
                     Type = EnumUtils.ParseEnum(type, RedirectTypeFilter.All),
-                    Text = text,
-                    RootNodeKey = rootNodeKey
+                    Text = text
                 };
+
+                if (rootNodeKey != null) options.RootNodeKey = rootNodeKey;
+
+                var rootKeys = _contentService.GetByIds(_redirects.GetUserAccessibleNodes(GetUser()))
+	                .Select(p => p.Key)
+	                .ToArray();
                 
                 // Make the search for redirects via the redirects service
-                RedirectsSearchResult result = _redirects.GetRedirects(options);
+                RedirectsSearchResult result = _redirects.GetRedirects(options, rootKeys);
 
                 // Map the result for the API
                 return new JsonResult(_backOffice.Map(result));
@@ -370,6 +382,11 @@ private ActionResult Error(RedirectsException ex) {
 
         }
 
+        private IUser GetUser()
+        {
+	        var currentUser = HttpContext.User.GetUmbracoIdentity();
+	        return _userService.GetByUsername(currentUser.Name);
+        }
     }
 
 }
\ No newline at end of file
diff --git a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
index 00b69a5a..091d0b76 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
@@ -4,6 +4,8 @@
 using Skybrud.Umbraco.Redirects.Models;
 using Skybrud.Umbraco.Redirects.Models.Options;
 
+using Umbraco.Cms.Core.Models.Membership;
+
 namespace Skybrud.Umbraco.Redirects.Services {
     
     /// <summary>
@@ -17,6 +19,8 @@ public interface IRedirectsService {
         /// <returns>An array of <see cref="RedirectDomain"/>.</returns>
         RedirectDomain[] GetDomains();
 
+        int[] GetUserAccessibleNodes(IUser umbracoUser);
+
         /// <summary>
         /// Adds a new redirect with the specified <paramref name="options"/>.
         /// </summary>
@@ -99,13 +103,14 @@ public interface IRedirectsService {
         /// <param name="uri">The inbound URL.</param>
         /// <returns>The destination URL.</returns>
         string GetDestinationUrl(IRedirectBase redirect, Uri uri);
-        
+
         /// <summary>
         /// Returns a paginated list of redirects matching the specified <paramref name="options"/>.
         /// </summary>
         /// <param name="options">The options the returned redirects should match.</param>
+        /// <param name="rootNodes">The user's root nodes that should be visible for security</param>
         /// <returns>An instance of <see cref="RedirectsSearchResult"/>.</returns>
-        RedirectsSearchResult GetRedirects(RedirectsSearchOptions options);
+        RedirectsSearchResult GetRedirects(RedirectsSearchOptions options, Guid[] rootNodes);
 
         /// <summary>
         /// Returns a collection with all redirects.
@@ -117,7 +122,7 @@ public interface IRedirectsService {
         /// Returns an array of all rode nodes configured in Umbraco.
         /// </summary>
         /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
-        RedirectRootNode[] GetRootNodes();
+        RedirectRootNode[] GetRootNodes(IUser user);
         
         /// <summary>
         /// Returns an array of redirects where the destination matches the specified <paramref name="nodeType"/> and <paramref name="nodeId"/>.
diff --git a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
index d3b8c18e..15e362a4 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
@@ -5,6 +5,7 @@
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using NPoco;
+using NUglify.Helpers;
 using Skybrud.Essentials.Strings.Extensions;
 using Skybrud.Essentials.Time;
 using Skybrud.Umbraco.Redirects.Exceptions;
@@ -12,6 +13,7 @@
 using Skybrud.Umbraco.Redirects.Models;
 using Skybrud.Umbraco.Redirects.Models.Dtos;
 using Skybrud.Umbraco.Redirects.Models.Options;
+using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Models.PublishedContent;
 using Umbraco.Cms.Core.Routing;
 using Umbraco.Cms.Core.Services;
@@ -59,6 +61,26 @@ public RedirectDomain[] GetDomains() {
             return _domains.GetAll(false).Select(RedirectDomain.GetFromDomain).ToArray();
         }
 
+        public int[] GetUserAccessibleNodes(IUser umbracoUser)
+        {
+	        var rootNodeIDs = umbracoUser.StartContentIds.ToList();
+	        var groupRootNodes = umbracoUser.Groups
+		        .Where(p => p.StartContentId.HasValue)
+		        .Select(p => p.StartContentId.Value)
+		        .ToArray();
+	        rootNodeIDs.AddRange(groupRootNodes);
+
+            // JH: Admin should access all nodes
+            if (rootNodeIDs.Count == 1 && rootNodeIDs.Single() == -1)
+            {
+	            rootNodeIDs.Clear();
+	            rootNodeIDs = GetDomains().Select(c => c.RootNodeId).ToList();
+            }
+
+            return rootNodeIDs.ToArray();
+
+        }
+
         /// <summary>
         /// Deletes the specified <paramref name="redirect"/>.
         /// </summary>
@@ -353,8 +375,9 @@ public IRedirect SaveRedirect(IRedirect redirect) {
         /// Returns a paginated list of redirects matching the specified <paramref name="options"/>.
         /// </summary>
         /// <param name="options">The options the returned redirects should match.</param>
+		/// <param name="rootNodes">The user's root nodes that should be visible for security</param>
         /// <returns>An instance of <see cref="RedirectsSearchResult"/>.</returns>
-        public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options) {
+        public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options, Guid[] rootNodes) {
 
             if (options == null) throw new ArgumentNullException(nameof(options));
             
@@ -365,7 +388,11 @@ public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options) {
             var sql = scope.SqlContext.Sql().Select<RedirectDto>().From<RedirectDto>();
 
             // Search by the rootNodeId
-            if (options.RootNodeKey != null) sql = sql.Where<RedirectDto>(x => x.RootKey == options.RootNodeKey.Value);
+            if (options.RootNodeKey != null) {
+	            sql = sql.Where<RedirectDto>(x => x.RootKey == options.RootNodeKey);
+            } else if (rootNodes.Any()) {
+	            sql = sql.Where<RedirectDto>(x => rootNodes.Contains(x.RootKey));
+            }
 
             // Search by the type
             if (options.Type != RedirectTypeFilter.All) {
@@ -394,6 +421,7 @@ public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options) {
                         ||
                         (x.Path.Contains(url) && x.QueryString.Contains(query))
                     ));
+
                 }
             }
 
@@ -452,15 +480,17 @@ public IEnumerable<IRedirect> GetAllRedirects()  {
         /// Returns an array of all rode nodes configured in Umbraco.
         /// </summary>
         /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
-        public RedirectRootNode[] GetRootNodes()  {
+        public RedirectRootNode[] GetRootNodes(IUser user)
+        {
 
+	        var rootNodeIDs = GetUserAccessibleNodes(user);
             // Multiple domains may be configured for a single node, so we need to group the domains before proceeding
             var domainsByRootNodeId = GetDomains().GroupBy(x => x.RootNodeId);
 
             return (
                 from domainGroup in domainsByRootNodeId
                 let content =  _contentService.GetById(domainGroup.First().RootNodeId)
-                where content is { Trashed: false }
+                where !content.Trashed && rootNodeIDs.Contains(content.Id)
                 orderby content.Id
                 select RedirectRootNode.GetFromContent(content, domainGroup)
             ).ToArray();

From 350d545612363d6e7d29fb15009364608d757e62 Mon Sep 17 00:00:00 2001
From: Jamie Howarth <hello@jamiehowarth.me>
Date: Mon, 14 Nov 2022 20:22:42 +0000
Subject: [PATCH 2/4] This should hopefully satisfy @abjerner's API spec. Have
 tested on our local multi-tenant instance and all seems to work well.

---
 .../Config/RedirectsContentAppSettings.cs     |  5 ++
 .../Controllers/Api/RedirectsController.cs    | 33 +++-----
 .../Models/RedirectRootNode.cs                |  7 ++
 .../Services/IRedirectsService.cs             | 14 ++--
 .../Services/RedirectsSearchOptions.cs        | 15 +++-
 .../Services/RedirectsService.cs              | 81 ++++++++++---------
 .../Scripts/Controllers/Dashboards/Default.js |  2 +-
 7 files changed, 88 insertions(+), 69 deletions(-)

diff --git a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
index de45ef26..d74e7ec1 100644
--- a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
+++ b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
@@ -10,6 +10,11 @@ public class RedirectsContentAppSettings {
         /// </summary>
         public bool Enabled { get; set; } = true;
 
+        /// <summary>
+        /// Gets or sets whether the user's start nodes should filter which redirects they have access to. Default is <see lanword="true"/>.
+        /// </summary>
+        public bool UserStartNodes { get; set; } = false;
+
     }
 
 }
\ No newline at end of file
diff --git a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
index f1b82e6d..73a2c4fd 100644
--- a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
+++ b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Net;
 using Microsoft.AspNetCore.Mvc;
@@ -16,6 +17,7 @@
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Core.Services;
 using Umbraco.Cms.Core.Web;
 using Umbraco.Cms.Web.BackOffice.Controllers;
@@ -40,7 +42,6 @@ public class RedirectsController : UmbracoAuthorizedApiController {
         private readonly RedirectsBackOfficeHelper _backOffice;
         private readonly IContentService _contentService;
         private readonly IMediaService _mediaService;
-        private readonly IUserService _userService;
         private readonly IUmbracoContextAccessor _umbracoContextAccessor;
 
         #region Constructors
@@ -48,14 +49,12 @@ public class RedirectsController : UmbracoAuthorizedApiController {
         public RedirectsController(ILogger<RedirectsController> logger, IRedirectsService redirectsService, RedirectsBackOfficeHelper backOffice,
             IContentService contentService,
             IMediaService mediaService,
-            IUserService userService,
             IUmbracoContextAccessor umbracoContextAccessor) {
             _logger = logger;
             _redirects = redirectsService;
             _backOffice = backOffice;
             _contentService = contentService;
             _mediaService = mediaService;
-            _userService = userService;
             _umbracoContextAccessor = umbracoContextAccessor;
         }
 
@@ -68,7 +67,9 @@ public RedirectsController(ILogger<RedirectsController> logger, IRedirectsServic
         [HttpGet]
         public ActionResult GetRootNodes() {
 
-	        RedirectRootNode[] rootNodes = _redirects.GetRootNodes(GetUser()).ToArray();
+	        var rootNodes = _backOffice.Settings.ContentApp.UserStartNodes
+		        ? _redirects.GetRootNodes(_backOffice.CurrentUser).ToArray()
+		        : _redirects.GetRootNodes();
 
 	        return new JsonResult(new
 	        {
@@ -262,29 +263,27 @@ public ActionResult DeleteRedirect(Guid redirectId) {
         /// <param name="limit">The maximum amount of redirects to be returned per page.</param>
         /// <param name="type">The type of redirects that should be returned.</param>
         /// <param name="text">The text that the returned redirects should match.</param>
-        /// <param name="rootNodeKey">The root node key that the returned redirects should match. <c>null</c> means all redirects. <see cref="Guid.Empty"/> means all global redirects.</param>
+        /// <param name="rootNodeKeys">An array of root node keys that the returned redirects should match. <c>null</c> means all redirects. <see cref="Guid.Empty"/> means all global redirects.</param>
         /// <returns>A list of redirects.</returns>
         [HttpGet]
-        public ActionResult GetRedirects(int page = 1, int limit = 20, string type = null, string text = null, Guid? rootNodeKey = null) {
+        public ActionResult GetRedirects(int page = 1, int limit = 20, string type = null, string text = null, [FromQuery]Guid[] rootNodeKeys = null) {
 
             try {
+	            var rootKeys = _backOffice.Settings.ContentApp.UserStartNodes
+		            ? _redirects.GetRootNodes(_backOffice.CurrentUser).Select(p => p.Key)
+		            : _redirects.GetRootNodes().Select(p => p.Key);
 
                 // Initialize the search options
                 RedirectsSearchOptions options = new() {
                     Page = page,
                     Limit = limit,
                     Type = EnumUtils.ParseEnum(type, RedirectTypeFilter.All),
-                    Text = text
+                    Text = text,
+                    RootNodeKeys = (rootNodeKeys != null && rootNodeKeys.Any()) ? rootNodeKeys : rootKeys.ToArray()
                 };
 
-                if (rootNodeKey != null) options.RootNodeKey = rootNodeKey;
-
-                var rootKeys = _contentService.GetByIds(_redirects.GetUserAccessibleNodes(GetUser()))
-	                .Select(p => p.Key)
-	                .ToArray();
-                
                 // Make the search for redirects via the redirects service
-                RedirectsSearchResult result = _redirects.GetRedirects(options, rootKeys);
+                RedirectsSearchResult result = _redirects.GetRedirects(options);
 
                 // Map the result for the API
                 return new JsonResult(_backOffice.Map(result));
@@ -381,12 +380,6 @@ private ActionResult Error(RedirectsException ex) {
             };
 
         }
-
-        private IUser GetUser()
-        {
-	        var currentUser = HttpContext.User.GetUmbracoIdentity();
-	        return _userService.GetByUsername(currentUser.Name);
-        }
     }
 
 }
\ No newline at end of file
diff --git a/src/Skybrud.Umbraco.Redirects/Models/RedirectRootNode.cs b/src/Skybrud.Umbraco.Redirects/Models/RedirectRootNode.cs
index 35effa3e..5225fd64 100644
--- a/src/Skybrud.Umbraco.Redirects/Models/RedirectRootNode.cs
+++ b/src/Skybrud.Umbraco.Redirects/Models/RedirectRootNode.cs
@@ -35,6 +35,12 @@ public class RedirectRootNode {
         [JsonProperty("icon")]
         public string Icon { get; }
 
+        /// <summary>
+        /// Gets the path of the root node.
+        /// </summary>
+        [JsonProperty("path")]
+        public IEnumerable<int> Path { get; set; }
+
         /// <summary>
         /// Gets the domains asscoiated with the root node.
         /// </summary>
@@ -44,6 +50,7 @@ private RedirectRootNode(IContent content, IEnumerable<RedirectDomain> domains)
             Id = content.Id;
             Key = content.Key;
             Name = content.Name;
+            Path = content.Path.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse);
             Icon = content.ContentType.Icon;
             Domains = domains?.Select(x => x.Name).ToArray() ?? Array.Empty<string>();
         }
diff --git a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
index 3cb4abbe..892f7b44 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
@@ -19,8 +19,6 @@ public interface IRedirectsService {
         /// <returns>An array of <see cref="RedirectDomain"/>.</returns>
         RedirectDomain[] GetDomains();
 
-        int[] GetUserAccessibleNodes(IUser umbracoUser);
-
         /// <summary>
         /// Adds a new redirect with the specified <paramref name="options"/>.
         /// </summary>
@@ -105,9 +103,8 @@ public interface IRedirectsService {
         /// Returns a paginated list of redirects matching the specified <paramref name="options"/>.
         /// </summary>
         /// <param name="options">The options the returned redirects should match.</param>
-        /// <param name="rootNodes">The user's root nodes that should be visible for security</param>
         /// <returns>An instance of <see cref="RedirectsSearchResult"/>.</returns>
-        RedirectsSearchResult GetRedirects(RedirectsSearchOptions options, Guid[] rootNodes);
+        RedirectsSearchResult GetRedirects(RedirectsSearchOptions options);
 
         /// <summary>
         /// Returns a collection with all redirects.
@@ -119,8 +116,15 @@ public interface IRedirectsService {
         /// Returns an array of all rode nodes configured in Umbraco.
         /// </summary>
         /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
+        RedirectRootNode[] GetRootNodes();
+
+        /// <summary>
+        /// Returns an array of all rode nodes configured in Umbraco.
+        /// </summary>
+        /// <param name="user">An <see cref="IUser"/> object to determine access to root nodes.</param>
+        /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
         RedirectRootNode[] GetRootNodes(IUser user);
-        
+
         /// <summary>
         /// Returns an array of redirects where the destination matches the specified <paramref name="nodeType"/> and <paramref name="nodeId"/>.
         /// </summary>
diff --git a/src/Skybrud.Umbraco.Redirects/Services/RedirectsSearchOptions.cs b/src/Skybrud.Umbraco.Redirects/Services/RedirectsSearchOptions.cs
index 73bbceef..89159d5d 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/RedirectsSearchOptions.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/RedirectsSearchOptions.cs
@@ -33,12 +33,19 @@ public class RedirectsSearchOptions {
         /// </summary>
         public string Text { get; set; }
 
-        /// <summary>
-        /// Gets or sets the key the returned redirects should match. <see cref="Guid.Empty"/> indicates all global
-        /// redirects. Default is <c>null</c>, in which case this filter is disabled.
-        /// </summary>
+		/// <summary>
+		/// Gets or sets the key the returned redirects should match. <see cref="Guid.Empty"/> indicates all global
+		/// redirects. Default is <c>null</c>, in which case this filter is disabled.
+		/// </summary>
+		[Obsolete("Obsoleted in favour of RootNodeKeys, as a user may have access to more than one root node.")]
         public Guid? RootNodeKey { get; set; }
 
+		/// <summary>
+		/// Gets or sets the key the returned redirects should match. <see cref="Guid.Empty"/> indicates all global
+		/// redirects. Default is <c>null</c>, in which case this filter is disabled.
+		/// </summary>
+        public Guid[] RootNodeKeys { get; set; }
+
         #endregion
 
         #region Constructors
diff --git a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
index 57fb7eb4..36313a03 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
@@ -5,7 +5,6 @@
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using NPoco;
-using NUglify.Helpers;
 using Skybrud.Essentials.Strings.Extensions;
 using Skybrud.Essentials.Time;
 using Skybrud.Umbraco.Redirects.Exceptions;
@@ -61,26 +60,6 @@ public RedirectDomain[] GetDomains() {
             return _domains.GetAll(false).Select(RedirectDomain.GetFromDomain).ToArray();
         }
 
-        public int[] GetUserAccessibleNodes(IUser umbracoUser)
-        {
-	        var rootNodeIDs = umbracoUser.StartContentIds.ToList();
-	        var groupRootNodes = umbracoUser.Groups
-		        .Where(p => p.StartContentId.HasValue)
-		        .Select(p => p.StartContentId.Value)
-		        .ToArray();
-	        rootNodeIDs.AddRange(groupRootNodes);
-
-            // JH: Admin should access all nodes
-            if (rootNodeIDs.Count == 1 && rootNodeIDs.Single() == -1)
-            {
-	            rootNodeIDs.Clear();
-	            rootNodeIDs = GetDomains().Select(c => c.RootNodeId).ToList();
-            }
-
-            return rootNodeIDs.ToArray();
-
-        }
-
         /// <summary>
         /// Deletes the specified <paramref name="redirect"/>.
         /// </summary>
@@ -376,9 +355,8 @@ public IRedirect SaveRedirect(IRedirect redirect) {
         /// Returns a paginated list of redirects matching the specified <paramref name="options"/>.
         /// </summary>
         /// <param name="options">The options the returned redirects should match.</param>
-		/// <param name="rootNodes">The user's root nodes that should be visible for security</param>
         /// <returns>An instance of <see cref="RedirectsSearchResult"/>.</returns>
-        public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options, Guid[] rootNodes) {
+        public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options) {
 
             if (options == null) throw new ArgumentNullException(nameof(options));
 
@@ -389,10 +367,10 @@ public RedirectsSearchResult GetRedirects(RedirectsSearchOptions options, Guid[]
             var sql = scope.SqlContext.Sql().Select<RedirectDto>().From<RedirectDto>();
 
             // Search by the rootNodeId
-            if (options.RootNodeKey != null) {
+            if (options.RootNodeKeys != null && options.RootNodeKeys.Any()) {
+	            sql = sql.Where<RedirectDto>(x => options.RootNodeKeys.Contains(x.RootKey));
+            } else if (options.RootNodeKey != null) {
 	            sql = sql.Where<RedirectDto>(x => x.RootKey == options.RootNodeKey);
-            } else if (rootNodes.Any()) {
-	            sql = sql.Where<RedirectDto>(x => rootNodes.Contains(x.RootKey));
             }
 
             // Search by the type
@@ -481,21 +459,46 @@ public IEnumerable<IRedirect> GetAllRedirects()  {
         /// Returns an array of all rode nodes configured in Umbraco.
         /// </summary>
         /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
-        public RedirectRootNode[] GetRootNodes(IUser user)
+        public RedirectRootNode[] GetRootNodes()
         {
+	        var domainsByRootNodeId = GetDomains().GroupBy(x => x.RootNodeId);
+
+	        return (
+		        from domainGroup in domainsByRootNodeId
+		        let content = _contentService.GetById(domainGroup.First().RootNodeId)
+		        where content is { Trashed: false }
+		        orderby content.Id
+		        select RedirectRootNode.GetFromContent(content, domainGroup)
+	        ).ToArray();
+        }
 
-	        var rootNodeIDs = GetUserAccessibleNodes(user);
-            // Multiple domains may be configured for a single node, so we need to group the domains before proceeding
-            var domainsByRootNodeId = GetDomains().GroupBy(x => x.RootNodeId);
-
-            return (
-                from domainGroup in domainsByRootNodeId
-                let content =  _contentService.GetById(domainGroup.First().RootNodeId)
-                where !content.Trashed && rootNodeIDs.Contains(content.Id)
-                orderby content.Id
-                select RedirectRootNode.GetFromContent(content, domainGroup)
-            ).ToArray();
-
+        /// <summary>
+        /// Returns an array of all rode nodes configured in Umbraco.
+        /// </summary>
+        /// <param name="user">An <see cref="IUser"/> with potential root node access restrictions.</param>
+        /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes the user has access to.</returns>
+        public RedirectRootNode[] GetRootNodes(IUser user)
+        {
+	        var rootNodes = GetRootNodes();
+	        HashSet<int> rootNodeIds = new();
+
+	        if (user.StartContentIds != null)
+	        {
+		        foreach (var rootNodeId in user.StartContentIds)
+		        {
+			        rootNodeIds.Add(rootNodeId);
+		        }
+	        }
+
+	        foreach (var group in user.Groups)
+	        {
+		        if (group.StartContentId != null)
+		        {
+			        rootNodeIds.Add(group.StartContentId.Value);
+		        }
+	        }
+
+	        return rootNodes.Where(rootNode => rootNodeIds.Any(x => rootNode.Path.Contains(x))).ToArray();
         }
 
         /// <summary>
diff --git a/src/Skybrud.Umbraco.Redirects/wwwroot/Scripts/Controllers/Dashboards/Default.js b/src/Skybrud.Umbraco.Redirects/wwwroot/Scripts/Controllers/Dashboards/Default.js
index 604a4f5b..7cbf000f 100644
--- a/src/Skybrud.Umbraco.Redirects/wwwroot/Scripts/Controllers/Dashboards/Default.js
+++ b/src/Skybrud.Umbraco.Redirects/wwwroot/Scripts/Controllers/Dashboards/Default.js
@@ -113,7 +113,7 @@
 
         // Any filters?
         if (vm.filters.rootNode && vm.filters.rootNode.key) {
-            args.rootNodeKey = vm.filters.rootNode.key;
+            args.rootNodeKeys = vm.filters.rootNode.key;
             vm.activeFilters++;
         }
 

From 76e3ca960718eb9bfa0e458d460948414943a4ba Mon Sep 17 00:00:00 2001
From: Jamie Howarth <hello@jamiehowarth.me>
Date: Mon, 14 Nov 2022 20:35:22 +0000
Subject: [PATCH 3/4] Added merged props

---
 .../Config/RedirectsContentAppSettings.cs            | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
index d74e7ec1..01a989d9 100644
--- a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
+++ b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
@@ -1,4 +1,6 @@
-namespace Skybrud.Umbraco.Redirects.Config {
+using System.Collections.Generic;
+
+namespace Skybrud.Umbraco.Redirects.Config {
 
     /// <summary>
     /// Class with settings for the redirects content app.
@@ -15,6 +17,14 @@ public class RedirectsContentAppSettings {
         /// </summary>
         public bool UserStartNodes { get; set; } = false;
 
+        /// Gets or sets a list of content types and media types where the content app should or should not be shown.
+        /// The format follows Umbraco's <c>show</c> option - eg. <c>+content/*</c> enables the content app for all
+        /// content.
+        ///
+        /// If empty, the content app will be enabled for all content and media.
+        /// </summary>
+        public HashSet<string> Show { get; set; } = new();
+
     }
 
 }
\ No newline at end of file

From a928c173a6a1b100a0abd1a31fb8c84afa412621 Mon Sep 17 00:00:00 2001
From: Jamie Howarth <hello@jamiehowarth.me>
Date: Mon, 14 Nov 2022 21:44:11 +0000
Subject: [PATCH 4/4] Updated with @abjerner's comments.

---
 .../Config/RedirectsContentAppSettings.cs     |  4 +--
 .../Controllers/Api/RedirectsController.cs    |  6 ++--
 .../Services/IRedirectsService.cs             | 30 +++++++++++++++++--
 .../Services/RedirectsService.cs              | 29 ------------------
 4 files changed, 32 insertions(+), 37 deletions(-)

diff --git a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
index 01a989d9..e8b6cf60 100644
--- a/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
+++ b/src/Skybrud.Umbraco.Redirects/Config/RedirectsContentAppSettings.cs
@@ -13,9 +13,9 @@ public class RedirectsContentAppSettings {
         public bool Enabled { get; set; } = true;
 
         /// <summary>
-        /// Gets or sets whether the user's start nodes should filter which redirects they have access to. Default is <see lanword="true"/>.
+        /// Gets or sets whether the user's start nodes should filter which redirects they have access to. Default is <see langword="false"/>.
         /// </summary>
-        public bool UserStartNodes { get; set; } = false;
+        public bool UseStartNodes { get; set; } = false;
 
         /// Gets or sets a list of content types and media types where the content app should or should not be shown.
         /// The format follows Umbraco's <c>show</c> option - eg. <c>+content/*</c> enables the content app for all
diff --git a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
index 73a2c4fd..0da3a5ce 100644
--- a/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
+++ b/src/Skybrud.Umbraco.Redirects/Controllers/Api/RedirectsController.cs
@@ -67,8 +67,8 @@ public RedirectsController(ILogger<RedirectsController> logger, IRedirectsServic
         [HttpGet]
         public ActionResult GetRootNodes() {
 
-	        var rootNodes = _backOffice.Settings.ContentApp.UserStartNodes
-		        ? _redirects.GetRootNodes(_backOffice.CurrentUser).ToArray()
+	        var rootNodes = _backOffice.Settings.ContentApp.UseStartNodes
+		        ? _redirects.GetRootNodes(_backOffice.CurrentUser)
 		        : _redirects.GetRootNodes();
 
 	        return new JsonResult(new
@@ -269,7 +269,7 @@ public ActionResult DeleteRedirect(Guid redirectId) {
         public ActionResult GetRedirects(int page = 1, int limit = 20, string type = null, string text = null, [FromQuery]Guid[] rootNodeKeys = null) {
 
             try {
-	            var rootKeys = _backOffice.Settings.ContentApp.UserStartNodes
+	            var rootKeys = _backOffice.Settings.ContentApp.UseStartNodes
 		            ? _redirects.GetRootNodes(_backOffice.CurrentUser).Select(p => p.Key)
 		            : _redirects.GetRootNodes().Select(p => p.Key);
 
diff --git a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
index 892f7b44..bbb14b39 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/IRedirectsService.cs
@@ -1,5 +1,7 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+
 using Microsoft.AspNetCore.Http;
 using Skybrud.Umbraco.Redirects.Models;
 using Skybrud.Umbraco.Redirects.Models.Options;
@@ -121,9 +123,31 @@ public interface IRedirectsService {
         /// <summary>
         /// Returns an array of all rode nodes configured in Umbraco.
         /// </summary>
-        /// <param name="user">An <see cref="IUser"/> object to determine access to root nodes.</param>
-        /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes.</returns>
-        RedirectRootNode[] GetRootNodes(IUser user);
+        /// <param name="user">An <see cref="IUser"/> with potential root node access restrictions.</param>
+        /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes the user has access to.</returns>
+        RedirectRootNode[] GetRootNodes(IUser user)
+        {
+	        var rootNodes = GetRootNodes();
+	        HashSet<int> rootNodeIds = new();
+
+	        if (user.StartContentIds != null)
+	        {
+		        foreach (var rootNodeId in user.StartContentIds)
+		        {
+			        rootNodeIds.Add(rootNodeId);
+		        }
+	        }
+
+	        foreach (var group in user.Groups)
+	        {
+		        if (group.StartContentId != null)
+		        {
+			        rootNodeIds.Add(group.StartContentId.Value);
+		        }
+	        }
+
+	        return rootNodes.Where(rootNode => rootNodeIds.Any(x => rootNode.Path.Contains(x))).ToArray();
+        }
 
         /// <summary>
         /// Returns an array of redirects where the destination matches the specified <paramref name="nodeType"/> and <paramref name="nodeId"/>.
diff --git a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
index 36313a03..9e59352f 100644
--- a/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
+++ b/src/Skybrud.Umbraco.Redirects/Services/RedirectsService.cs
@@ -472,35 +472,6 @@ select RedirectRootNode.GetFromContent(content, domainGroup)
 	        ).ToArray();
         }
 
-        /// <summary>
-        /// Returns an array of all rode nodes configured in Umbraco.
-        /// </summary>
-        /// <param name="user">An <see cref="IUser"/> with potential root node access restrictions.</param>
-        /// <returns>An array of <see cref="RedirectRootNode"/> representing the root nodes the user has access to.</returns>
-        public RedirectRootNode[] GetRootNodes(IUser user)
-        {
-	        var rootNodes = GetRootNodes();
-	        HashSet<int> rootNodeIds = new();
-
-	        if (user.StartContentIds != null)
-	        {
-		        foreach (var rootNodeId in user.StartContentIds)
-		        {
-			        rootNodeIds.Add(rootNodeId);
-		        }
-	        }
-
-	        foreach (var group in user.Groups)
-	        {
-		        if (group.StartContentId != null)
-		        {
-			        rootNodeIds.Add(group.StartContentId.Value);
-		        }
-	        }
-
-	        return rootNodes.Where(rootNode => rootNodeIds.Any(x => rootNode.Path.Contains(x))).ToArray();
-        }
-
         /// <summary>
         /// Returns the calculated destination URL for the specified <paramref name="redirect"/>.
         /// </summary>