Refactor manifest dashboards, security

This commit is contained in:
Stephan
2018-12-04 11:34:15 +01:00
parent b361d1c950
commit 73f31dd800
24 changed files with 462 additions and 396 deletions

View File

@@ -7,26 +7,22 @@ namespace Umbraco.Core.Configuration.Dashboard
internal class AccessElement : RawXmlConfigurationElement, IAccess
{
public AccessElement()
{
}
{ }
public AccessElement(XElement rawXml)
:base(rawXml)
{
}
: base(rawXml)
{ }
public IEnumerable<IAccessItem> Rules
public IEnumerable<IAccessRule> Rules
{
get
{
var result = new List<AccessItem>();
if (RawXml != null)
{
result.AddRange(RawXml.Elements("deny").Select(x => new AccessItem {Action = AccessType.Deny, Value = x.Value }));
result.AddRange(RawXml.Elements("grant").Select(x => new AccessItem { Action = AccessType.Grant, Value = x.Value }));
result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessItem { Action = AccessType.GrantBySection, Value = x.Value }));
}
var result = new List<AccessRule>();
if (RawXml == null) return result;
result.AddRange(RawXml.Elements("deny").Select(x => new AccessRule {Type = AccessRuleType.Deny, Value = x.Value }));
result.AddRange(RawXml.Elements("grant").Select(x => new AccessRule { Type = AccessRuleType.Grant, Value = x.Value }));
result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessRule { Type = AccessRuleType.GrantBySection, Value = x.Value }));
return result;
}
}

View File

@@ -1,15 +0,0 @@
namespace Umbraco.Core.Configuration.Dashboard
{
internal class AccessItem : IAccessItem
{
/// <summary>
/// This can be grant, deny or grantBySection
/// </summary>
public AccessType Action { get; set; }
/// <summary>
/// The value of the action
/// </summary>
public string Value { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Core.Configuration.Dashboard
{
/// <summary>
/// Implements <see cref="IAccessRule"/>.
/// </summary>
internal class AccessRule : IAccessRule
{
/// <inheritdoc />
public AccessRuleType Type { get; set; }
/// <inheritdoc />
public string Value { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
namespace Umbraco.Core.Configuration.Dashboard
{
/// <summary>
/// Defines dashboard access rules type.
/// </summary>
public enum AccessRuleType
{
/// <summary>
/// Unknown (default value).
/// </summary>
Unknown = 0,
/// <summary>
/// Grant access to the dashboard if user belongs to the specified user group.
/// </summary>
Grant,
/// <summary>
/// Deny access to the dashboard if user belongs to the specified user group.
/// </summary>
Deny,
/// <summary>
/// Grant access to the dashboard if user has access to the specified section.
/// </summary>
GrantBySection
}
}

View File

@@ -1,9 +0,0 @@
namespace Umbraco.Core.Configuration.Dashboard
{
public enum AccessType
{
Grant,
Deny,
GrantBySection
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Configuration;
using System.Configuration;
using System.Linq;
using System.Xml.Linq;
@@ -8,26 +7,21 @@ namespace Umbraco.Core.Configuration.Dashboard
internal class ControlElement : RawXmlConfigurationElement, IDashboardControl
{
public string PanelCaption
{
get
{
return RawXml.Attribute("panelCaption") == null
? ""
: RawXml.Attribute("panelCaption").Value;
var panelCaption = RawXml.Attribute("panelCaption");
return panelCaption == null ? "" : panelCaption.Value;
}
}
public AccessElement Access
{
get
{
var access = RawXml.Element("access");
if (access == null)
{
return new AccessElement();
}
return new AccessElement(access);
return access == null ? new AccessElement() : new AccessElement(access);
}
}
@@ -45,7 +39,6 @@ namespace Umbraco.Core.Configuration.Dashboard
}
}
IAccess IDashboardControl.AccessRights => Access;
}
}

View File

@@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard
{
public interface IAccess
{
IEnumerable<IAccessItem> Rules { get; }
IEnumerable<IAccessRule> Rules { get; }
}
}

View File

@@ -1,15 +0,0 @@
namespace Umbraco.Core.Configuration.Dashboard
{
public interface IAccessItem
{
/// <summary>
/// This can be grant, deny or grantBySection
/// </summary>
AccessType Action { get; set; }
/// <summary>
/// The value of the action
/// </summary>
string Value { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
namespace Umbraco.Core.Configuration.Dashboard
{
/// <summary>
/// Represents an access rule.
/// </summary>
public interface IAccessRule
{
/// <summary>
/// Gets or sets the rule type.
/// </summary>
AccessRuleType Type { get; set; }
/// <summary>
/// Gets or sets the value for the rule.
/// </summary>
string Value { get; set; }
}
}

View File

@@ -0,0 +1,45 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Implements a json read converter for <see cref="IAccessRule"/>.
/// </summary>
internal class DashboardAccessRuleConverter : JsonReadConverter<IAccessRule>
{
/// <inheritdoc />
protected override IAccessRule Create(Type objectType, string path, JObject jObject)
{
return new AccessRule();
}
/// <inheritdoc />
protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer)
{
// see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property)
if (!(target is AccessRule accessRule))
throw new Exception("panic.");
GetRule(accessRule, jobject, "grant", AccessRuleType.Grant);
GetRule(accessRule, jobject, "deny", AccessRuleType.Deny);
GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection);
if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined.");
}
private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type)
{
var token = jobject[name];
if (token == null) return;
if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule.");
if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string.");
rule.Type = type;
rule.Value = token.Value<string>();
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.IO;
namespace Umbraco.Core.Manifest
{
public class ManifestDashboardDefinition
{
private string _view;
[JsonProperty("name", Required = Required.Always)]
public string Name { get; set; }
[JsonProperty("alias", Required = Required.Always)]
public string Alias { get; set; }
[JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[DefaultValue(100)]
public int Weight { get; set; }
[JsonProperty("view", Required = Required.Always)]
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
[JsonProperty("sections")]
public string[] Sections { get; set; } = Array.Empty<string>();
[JsonProperty("access")]
public IAccessRule[] AccessRules { get; set; } = Array.Empty<IAccessRule>();
}
}

View File

@@ -1,33 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Umbraco.Core.Manifest
{
public class ManifestDashboard
{
public ManifestDashboard()
{
Name = string.Empty;
Alias = string.Empty;
Weight = int.MaxValue; //default so we can check if this value has been explicitly set
View = string.Empty;
Sections = new List<string>();
}
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("aias")]
public string Alias { get; set; }
[JsonProperty("weight")]
public int Weight { get; set; }
[JsonProperty("view")]
public string View { get; set; }
[JsonProperty("sections")]
public List<string> Sections { get; set; }
}
}

View File

@@ -100,7 +100,7 @@ namespace Umbraco.Core.Manifest
var parameterEditors = new List<IDataEditor>();
var gridEditors = new List<GridEditor>();
var contentApps = new List<IContentAppDefinition>();
var dashboards = new List<ManifestDashboard>();
var dashboards = new List<ManifestDashboardDefinition>();
foreach (var manifest in manifests)
{
@@ -110,7 +110,7 @@ namespace Umbraco.Core.Manifest
if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors);
if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors);
if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps);
if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards);
if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards);
}
return new PackageManifest
@@ -132,7 +132,6 @@ namespace Umbraco.Core.Manifest
return new string[0];
return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
}
private static string TrimPreamble(string text)
{
@@ -154,8 +153,8 @@ namespace Umbraco.Core.Manifest
var manifest = JsonConvert.DeserializeObject<PackageManifest>(text,
new DataEditorConverter(_logger),
new ValueValidatorConverter(_validators),
//TODO: DO i need a dashboard one?
new ContentAppDefinitionConverter());
new ContentAppDefinitionConverter(),
new DashboardAccessRuleConverter());
// scripts and stylesheets are raw string, must process here
for (var i = 0; i < manifest.Scripts.Length; i++)
@@ -169,8 +168,6 @@ namespace Umbraco.Core.Manifest
if (ppEditors.Count > 0)
manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray();
//TODO: Do we need to deal with dashboards or are they auto parsed?
return manifest;
}
@@ -179,6 +176,5 @@ namespace Umbraco.Core.Manifest
{
return JsonConvert.DeserializeObject<IEnumerable<GridEditor>>(text);
}
}
}

View File

@@ -1,7 +1,5 @@
using System.Collections.Generic;
using System;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.PropertyEditors;
@@ -23,19 +21,14 @@ namespace Umbraco.Core.Manifest
[JsonProperty("parameterEditors")]
public IDataEditor[] ParameterEditors { get; set; } = Array.Empty<IDataEditor>();
[JsonProperty("gridEditors")]
public GridEditor[] GridEditors { get; set; } = Array.Empty<GridEditor>();
[JsonProperty("contentApps")]
public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty<IContentAppDefinition>();
/// <summary>
/// The dictionary of dashboards
/// </summary>
[JsonProperty("dashboards")]
public ManifestDashboard[] Dashboards { get; set; } = Array.Empty<ManifestDashboard>();
public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty<ManifestDashboardDefinition>();
}
}

View File

@@ -194,8 +194,8 @@
<Compile Include="Configuration\CommaDelimitedConfigurationElement.cs" />
<Compile Include="Configuration\CoreDebug.cs" />
<Compile Include="Configuration\Dashboard\AccessElement.cs" />
<Compile Include="Configuration\Dashboard\AccessItem.cs" />
<Compile Include="Configuration\Dashboard\AccessType.cs" />
<Compile Include="Configuration\Dashboard\AccessRule.cs" />
<Compile Include="Configuration\Dashboard\AccessRuleType.cs" />
<Compile Include="Configuration\Dashboard\AreaCollection.cs" />
<Compile Include="Configuration\Dashboard\AreaElement.cs" />
<Compile Include="Configuration\Dashboard\AreasElement.cs" />
@@ -203,7 +203,7 @@
<Compile Include="Configuration\Dashboard\ControlElement.cs" />
<Compile Include="Configuration\Dashboard\DashboardSection.cs" />
<Compile Include="Configuration\Dashboard\IAccess.cs" />
<Compile Include="Configuration\Dashboard\IAccessItem.cs" />
<Compile Include="Configuration\Dashboard\IAccessRule.cs" />
<Compile Include="Configuration\Dashboard\IArea.cs" />
<Compile Include="Configuration\Dashboard\IDashboardControl.cs" />
<Compile Include="Configuration\Dashboard\IDashboardSection.cs" />
@@ -334,8 +334,9 @@
<Compile Include="Logging\Serilog\LoggerConfigExtensions.cs" />
<Compile Include="Logging\Serilog\Enrichers\Log4NetLevelMapperEnricher.cs" />
<Compile Include="Manifest\ContentAppDefinitionConverter.cs" />
<Compile Include="Manifest\DashboardAccessRuleConverter.cs" />
<Compile Include="Manifest\ManifestContentAppDefinition.cs" />
<Compile Include="Manifest\ManifestDashboardSection.cs" />
<Compile Include="Manifest\ManifestDashboardDefinition.cs" />
<Compile Include="Migrations\IncompleteMigrationExpressionException.cs" />
<Compile Include="Migrations\MigrationBase_Extra.cs" />
<Compile Include="Migrations\Upgrade\V_7_10_0\RenamePreviewFolder.cs" />

View File

@@ -56,11 +56,11 @@ namespace Umbraco.Tests.Configurations.DashboardSettings
Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.Count());
Assert.AreEqual("translator", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Value);
Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Action);
Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Type);
Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Value);
Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Action);
Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Type);
Assert.AreEqual("world", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Value);
Assert.AreEqual(AccessType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Action);
Assert.AreEqual(AccessRuleType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Type);
}
[Test]
@@ -94,7 +94,7 @@ namespace Umbraco.Tests.Configurations.DashboardSettings
public void Test_Tab_Access()
{
Assert.AreEqual(1, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.Count());
Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Action);
Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Type);
Assert.AreEqual("admin", SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Value);
}
@@ -114,9 +114,9 @@ namespace Umbraco.Tests.Configurations.DashboardSettings
public void Test_Control_Access()
{
Assert.AreEqual(2, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.Count());
Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Action);
Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Type);
Assert.AreEqual("editor", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Value);
Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Action);
Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Type);
Assert.AreEqual("writer", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Value);
}
}

View File

@@ -8,6 +8,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.PropertyEditors;
@@ -383,5 +384,53 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2
Assert.AreEqual("icon-bar", app1.Icon);
Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp2.html", app1.View);
}
[Test]
public void CanParseManifest_Dashboards()
{
const string json = @"{'dashboards': [
{
'name': 'First One',
'alias': 'something',
'view': '~/App_Plugins/MyPackage/Dashboards/one.html',
'sections': [ 'content' ],
'access': [ {'grant':'user'}, {'deny':'foo'} ]
},
{
'name': 'Second-One',
'alias': 'something.else',
'weight': -1,
'view': '~/App_Plugins/MyPackage/Dashboards/two.html',
'sections': [ 'forms' ],
}
]}";
var manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Dashboards.Length);
Assert.IsInstanceOf<ManifestDashboardDefinition>(manifest.Dashboards[0]);
var db0 = manifest.Dashboards[0];
Assert.AreEqual("something", db0.Alias);
Assert.AreEqual("First One", db0.Name);
Assert.AreEqual(100, db0.Weight);
Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/one.html", db0.View);
Assert.AreEqual(1, db0.Sections.Length);
Assert.AreEqual("content", db0.Sections[0]);
Assert.AreEqual(2, db0.AccessRules.Length);
Assert.AreEqual(AccessRuleType.Grant, db0.AccessRules[0].Type);
Assert.AreEqual("user", db0.AccessRules[0].Value);
Assert.AreEqual(AccessRuleType.Deny, db0.AccessRules[1].Type);
Assert.AreEqual("foo", db0.AccessRules[1].Value);
Assert.IsInstanceOf<ManifestDashboardDefinition>(manifest.Dashboards[1]);
var db1 = manifest.Dashboards[1];
Assert.AreEqual("something.else", db1.Alias);
Assert.AreEqual("Second-One", db1.Name);
Assert.AreEqual(-1, db1.Weight);
Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/two.html", db1.View);
Assert.AreEqual(1, db1.Sections.Length);
Assert.AreEqual("forms", db1.Sections[0]);
}
}
}

View File

@@ -25,11 +25,11 @@ namespace Umbraco.Web.Editors
[WebApi.UmbracoAuthorize]
public class DashboardController : UmbracoApiController
{
private readonly DashboardHelper _dashboardHelper;
private readonly Dashboards _dashboards;
public DashboardController(DashboardHelper dashboardHelper)
public DashboardController(Dashboards dashboards)
{
_dashboardHelper = dashboardHelper;
_dashboards = dashboards;
}
//we have just one instance of HttpClient shared for the entire application
@@ -120,7 +120,7 @@ namespace Umbraco.Web.Editors
[ValidateAngularAntiForgeryToken]
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
{
return _dashboardHelper.GetDashboard(section, Security.CurrentUser);
return _dashboards.GetDashboards(section, Security.CurrentUser);
}
}
}

View File

@@ -1,176 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
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;
namespace Umbraco.Web.Editors
{
public class DashboardHelper
{
private readonly ISectionService _sectionService;
private readonly IDashboardSection _dashboardSection;
private readonly ManifestParser _manifestParser;
public DashboardHelper(ISectionService sectionService, IDashboardSection dashboardSection, ManifestParser manifestParser)
{
_sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
_dashboardSection = dashboardSection;
_manifestParser = manifestParser;
}
/// <summary>
/// Returns the dashboard models per section for the current user and it's access
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
{
var result = new Dictionary<string, IEnumerable<Tab<DashboardControl>>>();
foreach (var section in _sectionService.GetSections())
{
result[section.Alias] = GetDashboard(section.Alias, currentUser);
}
return result;
}
/// <summary>
/// Returns the dashboard model for the given section based on the current user and it's access
/// </summary>
/// <param name="section"></param>
/// <param name="currentUser"></param>
/// <returns></returns>
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section, IUser currentUser)
{
var configDashboards = GetDashboardsFromConfig(1, section, currentUser);
var pluginDashboards = GetDashboardsFromPlugins(configDashboards.Count + 1, section, currentUser);
//now we need to merge them, the plugin ones would replace anything matched in the config one where the tab alias matches
var added = new List<Tab<DashboardControl>>(); //to track the ones we'll add
foreach (var configDashboard in configDashboards)
{
var matched = pluginDashboards.Where(x => string.Equals(x.Alias, configDashboard.Alias, StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (var tab in matched)
{
configDashboard.Label = tab.Label; //overwrite
configDashboard.Properties = configDashboard.Properties.Concat(tab.Properties).ToList(); //combine
added.Add(tab); //track this
}
}
//now add the plugin dashboards to the config dashboards that have not already been added
var toAdd = pluginDashboards.Where(pluginDashboard => added.Contains(pluginDashboard) == false).ToList();
configDashboards.AddRange(toAdd);
//last thing is to re-sort and ID the tabs
configDashboards.Sort((tab, tab1) => tab.Id > tab1.Id ? 1 : 0);
for (var index = 0; index < configDashboards.Count; index++)
{
var tab = configDashboards[index];
tab.Id = (index + 1);
if (tab.Id == 1)
tab.IsActive = true;
}
return configDashboards;
}
private List<Tab<DashboardControl>> GetDashboardsFromConfig(int startTabId, string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
var i = startTabId;
//disable packages section dashboard
if (section == "packages") return tabs;
foreach (var dashboardSection in _dashboardSection.Sections.Where(x => x.Areas.Contains(section)))
{
//we need to validate access to this section
if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false)
continue;
//User is authorized
foreach (var tab in dashboardSection.Tabs)
{
//we need to validate access to this tab
if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false)
continue;
var dashboardControls = new List<DashboardControl>();
foreach (var control in tab.Controls)
{
if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false)
continue;
var dashboardControl = new DashboardControl();
var controlPath = control.ControlPath.Trim();
dashboardControl.Caption = control.PanelCaption;
dashboardControl.Path = IOHelper.FindFile(controlPath);
if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant()))
throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported");
dashboardControls.Add(dashboardControl);
}
tabs.Add(new Tab<DashboardControl>
{
Id = i,
Alias = tab.Caption.ToSafeAlias(),
Label = tab.Caption,
Properties = dashboardControls
});
i++;
}
}
//In case there are no tabs or a user doesn't have access the empty tabs list is returned
return tabs;
}
private List<Tab<DashboardControl>> GetDashboardsFromPlugins(int startTabId, string section, IUser currentUser)
{
//TODO: Need to integrate the security with the manifest dashboards
var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins));
var tabs = new List<Tab<DashboardControl>>();
var i = startTabId;
foreach (var dashboard in _manifestParser.Manifest.Dashboards.Where(x => x.Sections.InvariantContains(section)))
{
var dashboardControls = new List<DashboardControl>();
var view = dashboard.View.Trim();
var dashboardControl = new DashboardControl
{
Path = IOHelper.FindFile(view)
};
if (view.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant()))
throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported");
dashboardControls.Add(dashboardControl);
tabs.Add(new Tab<DashboardControl>
{
//assign the Id to the value of the index if one was defined, then we'll use the Id to sort later
Id = dashboard.Weight == int.MaxValue ? i : dashboard.Weight,
Alias = dashboard.Alias.ToSafeAlias(),
Label = dashboard.Name,
Properties = dashboardControls
});
i++;
}
return tabs;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Umbraco.Core;
@@ -17,105 +18,97 @@ namespace Umbraco.Web.Editors
public static bool AuthorizeAccess(ISection dashboardSection, IUser user, ISectionService sectionService)
{
if (user.Id.ToString(CultureInfo.InvariantCulture) == 0.ToInvariantString())
{
return true;
}
var denyTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray();
var grantedTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray();
var grantedBySectionTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray();
return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes);
return CheckUserAccessByRules(user, sectionService, dashboardSection.AccessRights.Rules);
}
public static bool AuthorizeAccess(IDashboardTab dashboardTab, IUser user, ISectionService sectionService)
{
if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString())
{
return true;
}
var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray();
var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray();
var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray();
return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes);
return CheckUserAccessByRules(user, sectionService, dashboardTab.AccessRights.Rules);
}
public static bool AuthorizeAccess(IDashboardControl dashboardTab, IUser user, ISectionService sectionService)
public static bool AuthorizeAccess(IDashboardControl dashboardControl, IUser user, ISectionService sectionService)
{
if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString())
{
return true;
}
var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray();
var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray();
var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray();
return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes);
return CheckUserAccessByRules(user, sectionService, dashboardControl.AccessRights.Rules);
}
public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IAccessItem[] denyTypes, IAccessItem[] grantedTypes, IAccessItem[] grantedBySectionTypes)
private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable<IAccessRule> rules)
{
var allowedSoFar = false;
IAccessRule[] denyRules = null, grantRules = null, grantBySectionRules = null;
// if there's no grantBySection or grant rules defined - we allow access so far and skip to checking deny rules
if (grantedBySectionTypes.Any() == false && grantedTypes.Any() == false)
var groupedRules = rules.GroupBy(x => x.Type);
foreach (var group in groupedRules)
{
allowedSoFar = true;
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");
}
}
// else we check the rules and only allow if one matches
else
return (denyRules ?? Array.Empty<IAccessRule>(), grantRules ?? Array.Empty<IAccessRule>(), grantBySectionRules ?? Array.Empty<IAccessRule>());
}
public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable<IAccessRule> rules)
{
if (user.Id == Constants.Security.SuperUserId)
return true;
var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules);
var hasAccess = true;
string[] assignedUserGroups = null;
// if there are no grant rules, then access is granted by default, unless denied
// otherwise, grant rules determine if access can be granted at all
if (grantBySectionRules.Length > 0 || grantRules.Length > 0)
{
hasAccess = false;
// check if this item has any grant-by-section arguments.
// if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
if (grantedBySectionTypes.Any())
if (grantBySectionRules.Length > 0)
{
var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id))
.Select(x => x.Alias)
.ToArray();
var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray();
var wantedSections = grantBySectionRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
if (allApprovedSections.Any(allowedApps.Contains))
{
allowedSoFar = true;
}
if (wantedSections.Intersect(allowedSections).Any())
hasAccess = true;
}
// if not already granted access, check if this item as any grant arguments.
// if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
if (allowedSoFar == false && grantedTypes.Any())
if (hasAccess == false && grantRules.Any())
{
var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
foreach (var userGroup in user.Groups)
{
if (allApprovedUserTypes.InvariantContains(userGroup.Alias))
{
allowedSoFar = true;
break;
}
}
assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray();
var wantedUserGroups = grantRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
if (wantedUserGroups.Intersect(assignedUserGroups).Any())
hasAccess = true;
}
}
if (!hasAccess || denyRules.Length == 0)
return false;
// check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
// be denied to see it no matter what
if (denyTypes.Any())
{
var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
foreach (var userGroup in user.Groups)
{
if (allDeniedUserTypes.InvariantContains(userGroup.Alias))
{
allowedSoFar = false;
break;
}
}
}
assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray();
var deniedUserGroups = denyRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
return allowedSoFar;
if (deniedUserGroups.Intersect(assignedUserGroups).Any())
hasAccess = false;
return hasAccess;
}
}
}

View File

@@ -0,0 +1,159 @@
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;
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

@@ -17,16 +17,15 @@ namespace Umbraco.Web.Editors
[PluginController("UmbracoApi")]
public class SectionController : UmbracoAuthorizedJsonController
{
private readonly DashboardHelper _dashboardHelper;
private readonly Dashboards _dashboards;
public SectionController(DashboardHelper dashboardHelper)
public SectionController(Dashboards dashboards)
{
_dashboardHelper = dashboardHelper;
_dashboards = dashboards;
}
public IEnumerable<Section> GetSections()
{
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0));
var sectionModels = sections.Select(Mapper.Map<Core.Models.Section, Section>).ToArray();
@@ -39,24 +38,18 @@ namespace Umbraco.Web.Editors
Current.Container.InjectProperties(appTreeController);
appTreeController.ControllerContext = ControllerContext;
var dashboards = _dashboardHelper.GetDashboards(Security.CurrentUser);
var dashboards = _dashboards.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)
foreach (var section in sectionModels)
{
var hasDashboards = false;
if (dashboards.TryGetValue(section.Alias, out var dashboardsForSection))
{
if (dashboardsForSection.Any())
hasDashboards = true;
}
var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any();
if (hasDashboards) continue;
if (hasDashboards == false)
{
//get the first tree in the section and get it's root node route path
var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result;
section.RoutePath = GetRoutePathForFirstTree(sectionRoot);
}
// get the first tree in the section and get its root node route path
var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result;
section.RoutePath = GetRoutePathForFirstTree(sectionRoot);
}
return sectionModels;

View File

@@ -122,7 +122,7 @@ namespace Umbraco.Web.Runtime
composition.Container.EnableMvc(); // does container.EnablePerWebRequestScope()
composition.Container.ScopeManagerProvider = smp; // reverts - we will do it last (in WebRuntime)
composition.Container.RegisterSingleton<DashboardHelper>();
composition.Container.RegisterSingleton<Dashboards>();
composition.Container.RegisterUmbracoControllers(typeLoader, GetType().Assembly);
composition.Container.EnableWebApi(GlobalConfiguration.Configuration);

View File

@@ -190,7 +190,7 @@
<Compile Include="Models\ContentEditing\IContentSave.cs" />
<Compile Include="WebApi\TrimModelBinder.cs" />
<Compile Include="Editors\CodeFileController.cs" />
<Compile Include="Editors\DashboardHelper.cs" />
<Compile Include="Editors\Dashboards.cs" />
<Compile Include="Editors\DictionaryController.cs" />
<Compile Include="Editors\EditorModelEventArgs.cs" />
<Compile Include="Editors\EditorValidatorCollection.cs" />