Refactor of dashboards

* Remove dashboards from Configs().Dashboards
* Remove dashboard.config XML file & associated code reading from XML
* Load Dashboards from TypeLoader into a CollectionBuilder
* Load merge/concat C# Dashboard Types with package.manifest
* Add buildercolletion to an extension method on Composition
This commit is contained in:
Warren Buckley
2019-01-23 14:37:33 +00:00
parent 9b9c9ef455
commit 69a3c85bbc
53 changed files with 678 additions and 844 deletions

View File

@@ -10,6 +10,7 @@ using Umbraco.Web.Routing;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Tour;
using Umbraco.Web.Trees;
using Umbraco.Web.Dashboards;
// the namespace here is intentional - although defined in Umbraco.Web assembly,
// this class should be visible when using Umbraco.Core.Components, alongside
@@ -92,6 +93,13 @@ namespace Umbraco.Core.Components
public static BackOfficeSectionCollectionBuilder Sections(this Composition composition)
=> composition.WithCollectionBuilder<BackOfficeSectionCollectionBuilder>();
/// <summary>
/// Gets the backoffice dashboards collection builder.
/// </summary>
/// <param name="composition">The composition.</param>
public static DashboardCollectionBuilder Dashboards(this Composition composition)
=> composition.WithCollectionBuilder<DashboardCollectionBuilder>();
#endregion
#region Uniques

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(10)]
//[HideFromTypeFinder]
[DataContract]
public class ContentDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Get Started";
[DataMember(Name = "alias")]
public string Alias => "contentIntro";
[IgnoreDataMember]
public string[] Sections => new string[] { "content" };
[DataMember(Name = "view")]
public string View => "views/dashboard/default/startupdashboardintro.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules
{
get
{
//TODO: WB Not convinced these rules work correctly?!
var rules = new List<IAccessRule>();
rules.Add(new AccessRule { Type = AccessRuleType.Deny, Value = "translator" });
rules.Add(new AccessRule { Type = AccessRuleType.Grant, Value = "admin" });
return rules.ToArray();
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
public class DashboardCollection : BuilderCollectionBase<IDashboardSection>
{
public DashboardCollection(IEnumerable<IDashboardSection> items)
: base(items)
{ }
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Manifest;
namespace Umbraco.Web.Dashboards
{
public class DashboardCollectionBuilder : WeightedCollectionBuilderBase<DashboardCollectionBuilder, DashboardCollection, IDashboardSection>
{
protected override DashboardCollectionBuilder This => this;
protected override IEnumerable<IDashboardSection> CreateItems(IFactory factory)
{
// get the manifest parser just-in-time - injecting it in the ctor would mean that
// simply getting the builder in order to configure the collection, would require
// its dependencies too, and that can create cycles or other oddities
var manifestParser = factory.GetInstance<ManifestParser>();
//TODO WB: We will need to re-sort items from package manifest with the C# Types
return base.CreateItems(factory).Concat(manifestParser.Manifest.Dashboards);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(20)]
[DataContract]
public class ExamineDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Examine Management";
[DataMember(Name = "alias")]
public string Alias => "settingsExamine";
[IgnoreDataMember]
public string[] Sections => new string[] { "settings" };
[DataMember(Name = "view")]
public string View => "views/dashboard/settings/examinemanagement.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(10)]
[DataContract]
public class FormsDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Install Umbraco Forms";
[DataMember(Name = "alias")]
public string Alias => "formsInstall";
[IgnoreDataMember]
public string[] Sections => new string[] { "forms" };
[DataMember(Name = "view")]
public string View => "views/dashboard/forms/formsdashboardintro.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(50)]
[DataContract]
public class HealthCheckDashboard : IDashboardSection
{
[DataMember(Name="name")]
public string Name => "Health Check";
[DataMember(Name = "alias")]
public string Alias => "settingsHealthCheck";
[IgnoreDataMember]
public string[] Sections => new string[] { "settings" };
[DataMember(Name = "view")]
public string View => "views/dashboard/settings/healthcheck.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(10)]
[DataContract]
public class MediaDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Content";
[DataMember(Name = "alias")]
public string Alias => "mediaFolderBrowser";
[IgnoreDataMember]
public string[] Sections => new string[] { "media" };
[DataMember(Name = "view")]
public string View => "views/dashboard/media/mediafolderbrowser.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(10)]
[DataContract]
public class MembersDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Get Started";
[DataMember(Name = "alias")]
public string Alias => "memberIntro";
[IgnoreDataMember]
public string[] Sections => new string[] { "member" };
[DataMember(Name = "view")]
public string View => "views/dashboard/members/membersdashboardvideos.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(40)]
[DataContract]
public class ModelsBuilderDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Models Builder";
[DataMember(Name = "alias")]
public string Alias => "settingsModelsBuilder";
[IgnoreDataMember]
public string[] Sections => new string[] { "settings" };
[DataMember(Name = "view")]
public string View => "/App_Plugins/ModelsBuilder/modelsbuilder.htm";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(30)]
[DataContract]
public class PublishedStatusDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Published Status";
[DataMember(Name = "alias")]
public string Alias => "settingsPublishedStatus";
[IgnoreDataMember]
public string[] Sections => new string[] { "settings" };
[DataMember(Name = "view")]
public string View => "views/dashboard/settings/publishedstatus.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(20)]
[DataContract]
public class RedirectUrlDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Redirect URL Management";
[DataMember(Name = "alias")]
public string Alias => "contentRedirectManager";
[IgnoreDataMember]
public string[] Sections => new string[] { "content" };
[DataMember(Name = "view")]
public string View => "views/dashboard/content/redirecturls.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.Web.Dashboards
{
[Weight(10)]
[DataContract]
public class SettingsDashboard : IDashboardSection
{
[DataMember(Name = "name")]
public string Name => "Welcome";
[DataMember(Name = "alias")]
public string Alias => "settingsWelcome";
[IgnoreDataMember]
public string[] Sections => new string[] { "settings" };
[DataMember(Name = "view")]
public string View => "views/dashboard/settings/settingsdashboardintro.html";
[IgnoreDataMember]
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -16,6 +16,8 @@ using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Core.Dashboards;
using Umbraco.Web.Services;
namespace Umbraco.Web.Editors
{
@@ -27,7 +29,7 @@ namespace Umbraco.Web.Editors
[WebApi.UmbracoAuthorize]
public class DashboardController : UmbracoApiController
{
private readonly Dashboards _dashboards;
private readonly IDashboardService _dashboardService;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> with auto dependencies.
@@ -38,10 +40,10 @@ namespace Umbraco.Web.Editors
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> with all its dependencies.
/// </summary>
public DashboardController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, Dashboards dashboards)
public DashboardController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, IDashboardService dashboardService)
: base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
{
_dashboards = dashboards;
_dashboardService = dashboardService;
}
//we have just one instance of HttpClient shared for the entire application
@@ -79,7 +81,7 @@ namespace Umbraco.Web.Editors
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard content from '{Url}'", url);
Logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url);
//it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings
AppCaches.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 5, 0));
@@ -117,7 +119,7 @@ namespace Umbraco.Web.Editors
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard CSS from '{Url}'", url);
Logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard CSS from {Url}", url);
//it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings
AppCaches.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 5, 0));
@@ -132,9 +134,9 @@ namespace Umbraco.Web.Editors
[ValidateAngularAntiForgeryToken]
[OutgoingEditorModelEvent]
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
public IEnumerable<Tab<IDashboardSection>> GetDashboard(string section)
{
return _dashboards.GetDashboards(section, Security.CurrentUser);
return _dashboardService.GetDashboards(section, Security.CurrentUser);
}
}
}

View File

@@ -1,160 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.IO;
using Umbraco.Core.Manifest;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Services;
namespace Umbraco.Web.Editors
{
public class Dashboards
{
private readonly ISectionService _sectionService;
private readonly IDashboardSection _dashboardSection;
private readonly ManifestParser _manifestParser;
public Dashboards(ISectionService sectionService, IDashboardSection dashboardSection, ManifestParser manifestParser)
{
_sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
_dashboardSection = dashboardSection;
_manifestParser = manifestParser;
}
/// <summary>
/// Gets all dashboards, organized by section, for a user.
/// </summary>
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
{
return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
}
/// <summary>
/// Returns dashboards for a specific section, for a user.
/// </summary>
public IEnumerable<Tab<DashboardControl>> GetDashboards(string section, IUser currentUser)
{
var tabId = 1;
var configDashboards = GetDashboardsFromConfig(ref tabId, section, currentUser);
var pluginDashboards = GetDashboardsFromPlugins(ref tabId, section, currentUser);
// merge dashboards
// both collections contain tab.alias -> controls
var dashboards = configDashboards;
// until now, it was fine to have duplicate tab.aliases in configDashboard
// so... the rule should be - just merge whatever we get, don't be clever
dashboards.AddRange(pluginDashboards);
// re-sort by id
dashboards.Sort((tab1, tab2) => tab1.Id > tab2.Id ? 1 : 0);
// re-assign ids (why?)
var i = 1;
foreach (var tab in dashboards)
{
tab.Id = i++;
tab.IsActive = tab.Id == 1;
}
return configDashboards;
}
// note:
// in dashboard.config we have 'sections' which define 'tabs' for 'areas'
// and 'areas' are the true UI sections - and each tab can have more than
// one control
// in a manifest, we directly have 'dashboards' which map to a unique
// control in a tab
// gets all tabs & controls from the config file
private List<Tab<DashboardControl>> GetDashboardsFromConfig(ref int tabId, string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
// disable packages section dashboard
if (section == "packages") return tabs;
foreach (var dashboardSection in _dashboardSection.Sections.Where(x => x.Areas.InvariantContains(section)))
{
// validate access to this section
if (!DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService))
continue;
foreach (var tab in dashboardSection.Tabs)
{
// validate access to this tab
if (!DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService))
continue;
var dashboardControls = new List<DashboardControl>();
foreach (var control in tab.Controls)
{
// validate access to this control
if (!DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService))
continue;
// create and add control
var dashboardControl = new DashboardControl
{
Caption = control.PanelCaption,
Path = IOHelper.FindFile(control.ControlPath.Trim())
};
if (dashboardControl.Path.InvariantEndsWith(".ascx"))
throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
dashboardControls.Add(dashboardControl);
}
// create and add tab
tabs.Add(new Tab<DashboardControl>
{
Id = tabId++,
Alias = tab.Caption.ToSafeAlias(),
Label = tab.Caption,
Properties = dashboardControls
});
}
}
return tabs;
}
private List<Tab<DashboardControl>> GetDashboardsFromPlugins(ref int tabId, string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
foreach (var dashboard in _manifestParser.Manifest.Dashboards.Where(x => x.Sections.InvariantContains(section)).OrderBy(x => x.Weight))
{
// validate access
if (!DashboardSecurity.CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
continue;
var dashboardControl = new DashboardControl
{
Caption = "",
Path = IOHelper.FindFile(dashboard.View.Trim())
};
if (dashboardControl.Path.InvariantEndsWith(".ascx"))
throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
tabs.Add(new Tab<DashboardControl>
{
Id = tabId++,
Alias = dashboard.Alias.ToSafeAlias(),
Label = dashboard.Name,
Properties = new[] { dashboardControl }
});
}
return tabs;
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Web.Http.Filters;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Events;
using Umbraco.Web.Models.ContentEditing;
@@ -14,9 +15,9 @@ namespace Umbraco.Web.Editors
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<MediaItemDisplay>> SendingMediaModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<MemberDisplay>> SendingMemberModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<UserDisplay>> SendingUserModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<IEnumerable<Tab<DashboardControl>>>> SendingDashboardModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<IEnumerable<Tab<IDashboardSection>>>> SendingDashboardModel;
private static void OnSendingDashboardModel(HttpActionExecutedContext sender, EditorModelEventArgs<IEnumerable<Tab<DashboardControl>>> e)
private static void OnSendingDashboardModel(HttpActionExecutedContext sender, EditorModelEventArgs<IEnumerable<Tab<IDashboardSection>>> e)
{
var handler = SendingDashboardModel;
handler?.Invoke(sender, e);
@@ -65,8 +66,8 @@ namespace Umbraco.Web.Editors
if (e.Model is UserDisplay)
OnSendingUserModel(sender, new EditorModelEventArgs<UserDisplay>(e));
if (e.Model is IEnumerable<Tab<DashboardControl>>)
OnSendingDashboardModel(sender, new EditorModelEventArgs<IEnumerable<Tab<DashboardControl>>>(e));
if (e.Model is IEnumerable<IDashboardSection>)
OnSendingDashboardModel(sender, new EditorModelEventArgs<IEnumerable<Tab<IDashboardSection>>>(e));
}
}
}

View File

@@ -22,15 +22,15 @@ namespace Umbraco.Web.Editors
[PluginController("UmbracoApi")]
public class SectionController : UmbracoAuthorizedJsonController
{
private readonly Dashboards _dashboards;
private readonly IDashboardService _dashboardService;
private readonly ISectionService _sectionService;
private readonly ITreeService _treeService;
public SectionController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState,
Dashboards dashboards, ISectionService sectionService, ITreeService treeService)
IDashboardService dashboardService, ISectionService sectionService, ITreeService treeService)
: base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
{
_dashboards = dashboards;
_dashboardService = dashboardService;
_sectionService = sectionService;
_treeService = treeService;
}
@@ -48,7 +48,7 @@ namespace Umbraco.Web.Editors
ControllerContext = ControllerContext
};
var dashboards = _dashboards.GetDashboards(Security.CurrentUser);
var dashboards = _dashboardService.GetDashboards(Security.CurrentUser);
//now we can add metadata for each section so that the UI knows if there's actually anything at all to render for
//a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree)

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.Web.Models.ContentEditing
{
[DataContract(Name = "control", Namespace = "")]
public class DashboardControl
{
[DataMember(Name = "path")]
public string Path { get; set; }
[DataMember(Name = "caption")]
public string Caption { get; set; }
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNet.SignalR;
using Umbraco.Core;
using Umbraco.Core.Components;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Events;
using Umbraco.Core.Models.PublishedContent;
@@ -17,6 +18,7 @@ using Umbraco.Web.Actions;
using Umbraco.Web.Cache;
using Umbraco.Web.Composing.Composers;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Dashboards;
using Umbraco.Web.Dictionary;
using Umbraco.Web.Editors;
using Umbraco.Web.Features;
@@ -95,14 +97,13 @@ namespace Umbraco.Web.Runtime
composition.RegisterUnique<ITreeService, TreeService>();
composition.RegisterUnique<ISectionService, SectionService>();
composition.RegisterUnique<IDashboardService, DashboardService>();
composition.RegisterUnique<IExamineManager>(factory => ExamineManager.Instance);
// configure the container for web
composition.ConfigureForWeb();
composition.RegisterUnique<Dashboards>();
composition
.ComposeUmbracoControllers(GetType().Assembly)
.SetDefaultRenderMvcController<RenderMvcController>(); // default controller for template views
@@ -202,6 +203,25 @@ namespace Umbraco.Web.Runtime
.Append<MembersBackOfficeSection>()
.Append<TranslationBackOfficeSection>();
// register core CMS dashboards as types - will be ordered by weight attribute & merged with package.manifest dashboards
// TODO WB Maybe use typeloader?!
composition.WithCollectionBuilder<DashboardCollectionBuilder>()
.Add(composition.TypeLoader.GetTypes<IDashboardSection>());
//.Add<ContentDashboard>()
//.Add<RedirectUrlDashboard>()
//.Add<MediaDashboard>()
//.Add<SettingsDashboard>()
//.Add<ExamineDashboard>()
//.Add<PublishedStatusDashboard>()
//.Add<ModelsBuilderDashboard>()
//.Add<HealthCheckDashboard>()
//.Add<MembersDashboard>()
//.Add<FormsDashboard>();
// register back office trees
foreach (var treeControllerType in umbracoApiControllerTypes
.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x)))

View File

@@ -1,65 +1,63 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Services;
using Umbraco.Web.Dashboards;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Editors
namespace Umbraco.Web.Services
{
/// <summary>
/// A utility class for determine dashboard security
/// </summary>
internal class DashboardSecurity
internal class DashboardService : IDashboardService
{
//TODO: Unit test all this!!! :/
private readonly ISectionService _sectionService;
private readonly DashboardCollection _dashboardCollection;
public static bool AuthorizeAccess(ISection dashboardSection, IUser user, ISectionService sectionService)
public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection)
{
return CheckUserAccessByRules(user, sectionService, dashboardSection.AccessRights.Rules);
_sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
_dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
}
public static bool AuthorizeAccess(IDashboardTab dashboardTab, IUser user, ISectionService sectionService)
/// <inheritdoc />
public IEnumerable<Tab<IDashboardSection>> GetDashboards(string section, IUser currentUser)
{
return CheckUserAccessByRules(user, sectionService, dashboardTab.AccessRights.Rules);
}
var tabs = new List<Tab<IDashboardSection>>();
var tabId = 0;
public static bool AuthorizeAccess(IDashboardControl dashboardControl, IUser user, ISectionService sectionService)
{
return CheckUserAccessByRules(user, sectionService, dashboardControl.AccessRights.Rules);
}
private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable<IAccessRule> rules)
{
IAccessRule[] denyRules = null, grantRules = null, grantBySectionRules = null;
var groupedRules = rules.GroupBy(x => x.Type);
foreach (var group in groupedRules)
foreach (var dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
{
var a = group.ToArray();
switch (group.Key)
// validate access
if (!CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
continue;
if (dashboard.View.InvariantEndsWith(".ascx"))
throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
var dashboards = new List<IDashboardSection>();
dashboards.Add(dashboard);
tabs.Add(new Tab<IDashboardSection>()
{
case AccessRuleType.Deny:
denyRules = a;
break;
case AccessRuleType.Grant:
grantRules = a;
break;
case AccessRuleType.GrantBySection:
grantBySectionRules = a;
break;
default:
throw new Exception("panic");
}
Id = tabId++,
Label = dashboard.Name,
Alias = dashboard.Alias,
Properties = dashboards
});
}
return (denyRules ?? Array.Empty<IAccessRule>(), grantRules ?? Array.Empty<IAccessRule>(), grantBySectionRules ?? Array.Empty<IAccessRule>());
return tabs;
}
public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable<IAccessRule> rules)
/// <inheritdoc />
public IDictionary<string, IEnumerable<Tab<IDashboardSection>>> GetDashboards(IUser currentUser)
{
return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
}
private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable<IAccessRule> rules)
{
if (user.Id == Constants.Security.SuperUserId)
return true;
@@ -111,5 +109,32 @@ namespace Umbraco.Web.Editors
return hasAccess;
}
private (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable<IAccessRule> rules)
{
IAccessRule[] denyRules = null, grantRules = null, grantBySectionRules = null;
var groupedRules = rules.GroupBy(x => x.Type);
foreach (var group in groupedRules)
{
var a = group.ToArray();
switch (group.Key)
{
case AccessRuleType.Deny:
denyRules = a;
break;
case AccessRuleType.Grant:
grantRules = a;
break;
case AccessRuleType.GrantBySection:
grantBySectionRules = a;
break;
default:
throw new Exception("panic");
}
}
return (denyRules ?? Array.Empty<IAccessRule>(), grantRules ?? Array.Empty<IAccessRule>(), grantBySectionRules ?? Array.Empty<IAccessRule>());
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Services
{
public interface IDashboardService
{
/// <summary>
/// Gets dashboard for a specific section/application
/// For a specific backoffice user
/// </summary>
/// <param name="section"></param>
/// <param name="currentUser"></param>
/// <returns></returns>
IEnumerable<Tab<IDashboardSection>> GetDashboards(string section, IUser currentUser);
/// <summary>
/// Gets all dashboards, organized by section, for a user.
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
IDictionary<string, IEnumerable<Tab<IDashboardSection>>> GetDashboards(IUser currentUser);
}
}

View File

@@ -122,6 +122,18 @@
<Compile Include="Composing\LightInject\LightInjectContainer.cs" />
<Compile Include="Components\BackOfficeUserAuditEventsComponent.cs" />
<Compile Include="ContentApps\ListViewContentAppFactory.cs" />
<Compile Include="Dashboards\ContentDashboard.cs" />
<Compile Include="Dashboards\DashboardCollection.cs" />
<Compile Include="Dashboards\DashboardCollectionBuilder.cs" />
<Compile Include="Dashboards\ExamineDashboard.cs" />
<Compile Include="Dashboards\FormsDashboard.cs" />
<Compile Include="Dashboards\HealthCheckDashboard.cs" />
<Compile Include="Dashboards\MediaDashboard.cs" />
<Compile Include="Dashboards\MembersDashboard.cs" />
<Compile Include="Dashboards\ModelsBuilderDashboard.cs" />
<Compile Include="Dashboards\PublishedStatusDashboard.cs" />
<Compile Include="Dashboards\RedirectUrlDashboard.cs" />
<Compile Include="Dashboards\SettingsDashboards.cs" />
<Compile Include="Editors\BackOfficePreviewModel.cs" />
<Compile Include="Editors\PackageController.cs" />
<Compile Include="Editors\KeepAliveController.cs" />
@@ -169,6 +181,8 @@
<Compile Include="Media\UploadAutoFillProperties.cs" />
<Compile Include="Models\ContentEditing\MacroDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroParameterDisplay.cs" />
<Compile Include="Services\DashboardService.cs" />
<Compile Include="Services\IDashboardService.cs" />
<Compile Include="Trees\BackOfficeSectionCollectionBuilder.cs" />
<Compile Include="Trees\MediaBackOfficeSection.cs" />
<Compile Include="Trees\MembersBackOfficeSection.cs" />
@@ -244,7 +258,6 @@
<Compile Include="WebApi\SerializeVersionAttribute.cs" />
<Compile Include="WebApi\TrimModelBinder.cs" />
<Compile Include="Editors\CodeFileController.cs" />
<Compile Include="Editors\Dashboards.cs" />
<Compile Include="Editors\DictionaryController.cs" />
<Compile Include="Editors\EditorModelEventArgs.cs" />
<Compile Include="Editors\EditorValidatorCollection.cs" />
@@ -705,7 +718,6 @@
<Compile Include="Editors\Filters\ContentSaveValidationAttribute.cs" />
<Compile Include="Editors\ContentTypeControllerBase.cs" />
<Compile Include="Editors\DashboardController.cs" />
<Compile Include="Editors\DashboardSecurity.cs" />
<Compile Include="Editors\DataTypeController.cs" />
<Compile Include="Editors\DataTypeValidateAttribute.cs" />
<Compile Include="Editors\ImagesController.cs" />
@@ -826,7 +838,6 @@
<Compile Include="Editors\EntityController.cs" />
<Compile Include="Editors\MemberController.cs" />
<Compile Include="Editors\CurrentUserController.cs" />
<Compile Include="Models\ContentEditing\DashboardControl.cs" />
<Compile Include="Models\ContentEditing\DataTypeDisplay.cs" />
<Compile Include="Models\ContentEditing\DataTypeSave.cs" />
<Compile Include="Models\ContentEditing\DataTypeConfigurationFieldDisplay.cs" />